优化课程小结自动绑定处理
This commit is contained in:
+235
-2
@@ -1439,6 +1439,29 @@ def duration_minutes_from_time_range(time_range: str) -> int | None:
|
||||
return end - start if end > start else None
|
||||
|
||||
|
||||
def time_range_bounds_minutes(time_range: str) -> tuple[int, int] | None:
|
||||
normalized = normalize_time_range_text(time_range)
|
||||
if not normalized:
|
||||
return None
|
||||
match = TIME_RANGE_RE.fullmatch(normalized)
|
||||
if not match:
|
||||
return None
|
||||
start = int(match.group("sh")) * 60 + int(match.group("sm"))
|
||||
end = int(match.group("eh")) * 60 + int(match.group("em"))
|
||||
return (start, end) if end > start else None
|
||||
|
||||
|
||||
def time_range_close_enough(left: str, right: str, tolerance_minutes: int = 20) -> bool:
|
||||
left_bounds = time_range_bounds_minutes(left)
|
||||
right_bounds = time_range_bounds_minutes(right)
|
||||
if left_bounds is None or right_bounds is None:
|
||||
return False
|
||||
return (
|
||||
abs(left_bounds[0] - right_bounds[0]) <= tolerance_minutes
|
||||
and abs(left_bounds[1] - right_bounds[1]) <= tolerance_minutes
|
||||
)
|
||||
|
||||
|
||||
def duration_minutes_from_summary(summary: dict) -> int | None:
|
||||
raw_duration = summary.get("duration") or summary.get("duration_text") or ""
|
||||
if raw_duration:
|
||||
@@ -2229,6 +2252,24 @@ def course_summary_record_key(student: str, teacher: str, subject: str, date_iso
|
||||
)
|
||||
|
||||
|
||||
def course_summary_identity_key(item: dict) -> tuple[str, str, str, str]:
|
||||
return (
|
||||
canonical_name(str(item.get("student") or "").strip()),
|
||||
canonical_teacher_name(str(item.get("teacher") or "").strip()),
|
||||
normalize_subject(str(item.get("subject") or "").strip()),
|
||||
str(item.get("date_iso") or "").strip(),
|
||||
)
|
||||
|
||||
|
||||
def class_record_identity_key(record: ClassRecord) -> tuple[str, str, str, str]:
|
||||
return (
|
||||
record.student,
|
||||
record.teacher,
|
||||
normalize_subject(record.subject),
|
||||
record.date.replace(".", "-"),
|
||||
)
|
||||
|
||||
|
||||
def course_summary_duplicate_key(item: dict) -> tuple[str, str, str, str, str]:
|
||||
return course_summary_record_key(
|
||||
str(item.get("student") or ""),
|
||||
@@ -2456,8 +2497,14 @@ def course_summary_to_public(item: dict, teachers: list[Teacher]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def course_summary_index_for_records(root: Path) -> dict[tuple[str, str, str, str, str], list[dict]]:
|
||||
def course_summary_index_for_records(
|
||||
root: Path,
|
||||
records: list[ClassRecord] | None = None,
|
||||
) -> dict[tuple[str, str, str, str, str], list[dict]]:
|
||||
index: dict[tuple[str, str, str, str, str], list[dict]] = defaultdict(list)
|
||||
records_by_identity: defaultdict[tuple[str, str, str, str], list[ClassRecord]] = defaultdict(list)
|
||||
for record in records or []:
|
||||
records_by_identity[class_record_identity_key(record)].append(record)
|
||||
for item in iter_course_summary_markdown(root):
|
||||
time_range = str(item.get("time_range") or "")
|
||||
if not time_range:
|
||||
@@ -2469,6 +2516,12 @@ def course_summary_index_for_records(root: Path) -> dict[tuple[str, str, str, st
|
||||
str(item.get("date_iso") or ""),
|
||||
time_range,
|
||||
)
|
||||
close_record = close_time_binding_record(
|
||||
item,
|
||||
records_by_identity.get(course_summary_identity_key(item), []),
|
||||
)
|
||||
if close_record is not None:
|
||||
key = class_record_binding_key(close_record)
|
||||
index[key].append(item)
|
||||
for key, values in list(index.items()):
|
||||
values.sort(key=lambda item: (str(item.get("title") or ""), str(item.get("id") or "")))
|
||||
@@ -2487,6 +2540,38 @@ def class_record_binding_key(record: ClassRecord) -> tuple[str, str, str, str, s
|
||||
)
|
||||
|
||||
|
||||
def close_time_binding_record(item: dict, records: list[ClassRecord]) -> ClassRecord | None:
|
||||
item_time = str(item.get("time_range") or "")
|
||||
if not item_time:
|
||||
return None
|
||||
item_bounds = time_range_bounds_minutes(item_time)
|
||||
if item_bounds is None:
|
||||
return None
|
||||
item_identity = course_summary_identity_key(item)
|
||||
if not all(item_identity):
|
||||
return None
|
||||
candidates = [
|
||||
record
|
||||
for record in records
|
||||
if class_record_identity_key(record) == item_identity
|
||||
and record.time != normalize_time_range_text(item_time)
|
||||
and time_range_close_enough(item_time, record.time)
|
||||
]
|
||||
if not candidates:
|
||||
return None
|
||||
def distance(record: ClassRecord) -> tuple[int, str]:
|
||||
record_bounds = time_range_bounds_minutes(record.time)
|
||||
if record_bounds is None:
|
||||
return (9999, record.time)
|
||||
return (
|
||||
abs(item_bounds[0] - record_bounds[0]) + abs(item_bounds[1] - record_bounds[1]),
|
||||
record.time,
|
||||
)
|
||||
|
||||
candidates.sort(key=distance)
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def course_summary_record_snapshot(record: ClassRecord) -> dict:
|
||||
return {
|
||||
"record_id": record_identity(record),
|
||||
@@ -2571,6 +2656,21 @@ def course_summary_binding_status(item: dict, record_keys: dict[tuple[str, str,
|
||||
"differences": [],
|
||||
}
|
||||
|
||||
close_record = close_time_binding_record(item, records)
|
||||
if close_record is not None:
|
||||
return {
|
||||
"status": "matched",
|
||||
"label": "已绑定",
|
||||
"record": course_summary_record_snapshot(close_record),
|
||||
"candidates": [],
|
||||
"reason": "只有时间段不一致,起止时间差均在20分钟内,已自动绑定",
|
||||
"differences": ["时间不一致"],
|
||||
"auto_bound": True,
|
||||
"auto_bind_reason": "time_range_within_20_minutes",
|
||||
"summary_time_range": str(item.get("time_range") or ""),
|
||||
"record_time_range": close_record.time,
|
||||
}
|
||||
|
||||
item_student = canonical_name(str(item.get("student") or ""))
|
||||
item_date = str(item.get("date_iso") or "")
|
||||
same_student_date = [
|
||||
@@ -3184,6 +3284,83 @@ def course_summary_to_class_record_line(summary: dict) -> str:
|
||||
)
|
||||
|
||||
|
||||
def close_time_existing_class_record(summary: dict, classnotes_path: Path) -> ClassRecord | None:
|
||||
if not classnotes_path.exists() or not summary.get("time_range"):
|
||||
return None
|
||||
try:
|
||||
records = read_classnotes(classnotes_path)
|
||||
except ValueError:
|
||||
return None
|
||||
return close_time_binding_record(summary, records)
|
||||
|
||||
|
||||
def auto_bound_class_record_context(summary: dict, classnotes_path: Path) -> dict | None:
|
||||
record = close_time_existing_class_record(summary, classnotes_path)
|
||||
if record is None:
|
||||
return None
|
||||
summary_time = normalize_time_range_text(summary.get("time_range"))
|
||||
return {
|
||||
"record": record,
|
||||
"record_line": class_record_to_line(record),
|
||||
"summary_time_range": summary_time,
|
||||
"record_time_range": record.time,
|
||||
"status": "自动绑定",
|
||||
"reason": "只有时间段不一致,起止时间差均在20分钟内,已自动绑定",
|
||||
}
|
||||
|
||||
|
||||
def auto_bound_course_summary_item(
|
||||
operation_logs_path: Path,
|
||||
operation: str,
|
||||
source_id: str,
|
||||
normalized: dict,
|
||||
saved: dict,
|
||||
bound_context: dict,
|
||||
*,
|
||||
batch_id: str = "",
|
||||
duplicate_tasks: dict | None = None,
|
||||
) -> tuple[str, list[str], dict]:
|
||||
log_id = append_operation_log(
|
||||
operation_logs_path,
|
||||
operation,
|
||||
"自动绑定",
|
||||
batch_id=batch_id,
|
||||
source_id=source_id,
|
||||
student=normalized["student"],
|
||||
teacher=normalized.get("teacher", ""),
|
||||
subject=normalized.get("subject", ""),
|
||||
proposed_line=str(bound_context.get("record_line") or ""),
|
||||
saved_path=str(saved.get("path") or ""),
|
||||
summary_time_range=str(bound_context.get("summary_time_range") or ""),
|
||||
record_time_range=str(bound_context.get("record_time_range") or ""),
|
||||
reasons=[str(bound_context.get("reason") or "")],
|
||||
)
|
||||
operation_log_ids = [log_id]
|
||||
if duplicate_tasks and duplicate_tasks.get("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"],
|
||||
)
|
||||
operation_log_ids.append(scan_log_id)
|
||||
return (
|
||||
"自动绑定",
|
||||
operation_log_ids,
|
||||
{
|
||||
"source_id": source_id,
|
||||
"status": "自动绑定",
|
||||
"task_id": None,
|
||||
"backup_id": "",
|
||||
"reasons": [str(bound_context.get("reason") or "")],
|
||||
"record_line": str(bound_context.get("record_line") or ""),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def auto_register_reasons(summary: dict, classnotes_path: Path, accounts_path: Path) -> tuple[list[str], str]:
|
||||
reasons: list[str] = []
|
||||
confidence = str(summary.get("confidence") or "").lower()
|
||||
@@ -3723,6 +3900,44 @@ def register_course_summary_texts(
|
||||
summaries_root,
|
||||
target_key=course_summary_duplicate_key(normalized),
|
||||
)
|
||||
bound_context = auto_bound_class_record_context(normalized, classnotes_path)
|
||||
if bound_context is not None:
|
||||
seen_source_ids.add(source_id)
|
||||
seen_semantic_keys.add(semantic_key)
|
||||
state["seen_source_ids"] = sorted(seen_source_ids)
|
||||
state["seen_semantic_keys"] = sorted(seen_semantic_keys)
|
||||
state.setdefault("batches", []).append(
|
||||
{
|
||||
"batch_id": f"manual-{datetime.now().strftime('%Y%m%d%H%M%S')}-{sha1_text(source_id, 8)}",
|
||||
"received_at": now,
|
||||
"window": {"source": "admin_register", "submitted_at": now},
|
||||
"students": [normalized["student"]],
|
||||
"result": {
|
||||
"received": 1,
|
||||
"saved": 1 if saved.get("added") else 0,
|
||||
"auto_registered": 1,
|
||||
"review_pending": 0,
|
||||
"duplicates": 0,
|
||||
"rejected": 0,
|
||||
},
|
||||
}
|
||||
)
|
||||
state["batches"] = state["batches"][-200:]
|
||||
write_course_summary_state(state_path, state)
|
||||
_status_value, log_ids, item = auto_bound_course_summary_item(
|
||||
operation_logs_path,
|
||||
"课程小结登记",
|
||||
source_id,
|
||||
normalized,
|
||||
saved,
|
||||
bound_context,
|
||||
duplicate_tasks=duplicate_tasks,
|
||||
)
|
||||
result["saved"] += 1 if saved.get("added") else 0
|
||||
result["auto_registered"] += 1
|
||||
result["operation_log_ids"].extend(log_ids)
|
||||
result["items"].append(item)
|
||||
continue
|
||||
register_result = register_class_record_lines(classnotes_path, accounts_path, line=proposed_line)
|
||||
seen_source_ids.add(source_id)
|
||||
seen_semantic_keys.add(semantic_key)
|
||||
@@ -3916,6 +4131,24 @@ def ingest_course_summaries(
|
||||
reason = f"脚本追问:{question}"
|
||||
if reason not in reasons:
|
||||
reasons.append(reason)
|
||||
bound_context = auto_bound_class_record_context(normalized, classnotes_path)
|
||||
if bound_context is not None and not reasons:
|
||||
status_value, log_ids, item = auto_bound_course_summary_item(
|
||||
operation_logs_path,
|
||||
"课程小结接收",
|
||||
source_id,
|
||||
normalized,
|
||||
saved,
|
||||
bound_context,
|
||||
batch_id=batch_id,
|
||||
duplicate_tasks=duplicate_tasks,
|
||||
)
|
||||
seen_source_ids.add(source_id)
|
||||
seen_semantic_keys.add(semantic_key)
|
||||
result["auto_registered"] += 1
|
||||
result["operation_log_ids"].extend(log_ids)
|
||||
result["items"].append(item)
|
||||
continue
|
||||
if reasons:
|
||||
task = create_course_summary_review_task(
|
||||
tasks_path,
|
||||
@@ -4472,7 +4705,7 @@ def query_public_records(
|
||||
matched = filter_records(records, spec) if has_filter_condition(spec) else []
|
||||
shown = matched[:limit] if limit > 0 else matched
|
||||
display_names = teacher_alias_map(teachers)
|
||||
summary_index = course_summary_index_for_records(summaries_root) if summaries_root is not None else {}
|
||||
summary_index = course_summary_index_for_records(summaries_root, records) if summaries_root is not None else {}
|
||||
return {
|
||||
"query": {
|
||||
"raw_query": spec.raw_query,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<button class="admin-tab is-active" data-admin-tab="accounts" type="button">课时账户</button>
|
||||
<button class="admin-tab" data-admin-tab="teachers" type="button">老师档案</button>
|
||||
<button class="admin-tab" data-admin-tab="reviews" type="button">纠错审核</button>
|
||||
<button class="admin-tab" data-admin-tab="summarySearch" type="button">课程小结查询</button>
|
||||
<button class="admin-tab" data-admin-tab="summarySearch" type="button">课程小结</button>
|
||||
<button class="admin-tab" data-admin-tab="logs" type="button">操作记录</button>
|
||||
<button class="admin-tab" data-admin-tab="register" type="button">登记</button>
|
||||
</nav>
|
||||
@@ -293,8 +293,9 @@
|
||||
|
||||
<section id="summarySearchPanel" class="panel admin-panel" hidden>
|
||||
<div class="section-head">
|
||||
<h2>课程小结查询</h2>
|
||||
<h2>课程小结</h2>
|
||||
<div class="quick-actions">
|
||||
<button id="quickMismatchBtn" class="chip" type="button">快速处理字段不一致</button>
|
||||
<button class="chip duplicate-summary-scan" type="button">扫描重复小结</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,6 +397,7 @@
|
||||
<option value="">全部结果</option>
|
||||
<option value="完成">完成</option>
|
||||
<option value="自动入账">自动入账</option>
|
||||
<option value="自动绑定">自动绑定</option>
|
||||
<option value="待审核">待审核</option>
|
||||
<option value="重复">重复</option>
|
||||
<option value="已驳回">失败/驳回</option>
|
||||
@@ -467,6 +469,6 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/static/admin.js?v=20260620-admin-health-summary"></script>
|
||||
<script src="/static/admin.js?v=20260620-course-summary-auto-bind"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+111
-5
@@ -84,6 +84,7 @@ const summarySearchBindingStatus = document.querySelector("#summarySearchBinding
|
||||
const summarySearchHasCandidate = document.querySelector("#summarySearchHasCandidate");
|
||||
const summarySearchMeta = document.querySelector("#summarySearchMeta");
|
||||
const summarySearchRows = document.querySelector("#summarySearchRows");
|
||||
const quickMismatchBtn = document.querySelector("#quickMismatchBtn");
|
||||
const logOperation = document.querySelector("#logOperation");
|
||||
const logStatus = document.querySelector("#logStatus");
|
||||
const logFilterForm = document.querySelector("#logFilterForm");
|
||||
@@ -116,6 +117,8 @@ let currentSummarySearchItems = [];
|
||||
let expandedSummarySearchId = "";
|
||||
let editingSummarySearchId = "";
|
||||
let editingSummaryIdentityId = "";
|
||||
let quickSummaryMismatchMode = false;
|
||||
let quickSummaryProcessing = false;
|
||||
let currentOperationLogs = [];
|
||||
let expandedOperationLogId = "";
|
||||
const registerPreviewState = {
|
||||
@@ -161,7 +164,7 @@ function statusClass(status) {
|
||||
}
|
||||
|
||||
function taskStatusClass(status) {
|
||||
if (["approved", "auto_registered", "已批准", "自动入账", "完成", "已更新", "已删除"].includes(status)) return "normal";
|
||||
if (["approved", "auto_registered", "已批准", "自动入账", "自动绑定", "完成", "已更新", "已删除"].includes(status)) return "normal";
|
||||
if (["rejected", "duplicate", "已驳回", "重复"].includes(status)) return "closed";
|
||||
if (status === "conflict" || status === "冲突") return "debt";
|
||||
return "warning";
|
||||
@@ -999,6 +1002,44 @@ function summarizeSummaryBindings(items) {
|
||||
);
|
||||
}
|
||||
|
||||
function mismatchItemsWithCandidates() {
|
||||
return currentSummarySearchItems.filter((item) => {
|
||||
const binding = summaryBinding(item);
|
||||
return binding.status === "mismatch" && Array.isArray(binding.candidates) && binding.candidates.length;
|
||||
});
|
||||
}
|
||||
|
||||
function firstMismatchWithCandidate() {
|
||||
return mismatchItemsWithCandidates()[0] || null;
|
||||
}
|
||||
|
||||
function currentQuickMismatchItem() {
|
||||
const items = mismatchItemsWithCandidates();
|
||||
return items.find((item) => String(item.id) === String(expandedSummarySearchId)) || items[0] || null;
|
||||
}
|
||||
|
||||
function updateQuickMismatchButton() {
|
||||
if (!quickMismatchBtn) return;
|
||||
const remaining = mismatchItemsWithCandidates().length;
|
||||
quickMismatchBtn.disabled = quickSummaryProcessing || (quickSummaryMismatchMode && remaining === 0);
|
||||
if (quickSummaryProcessing) {
|
||||
quickMismatchBtn.textContent = "正在处理";
|
||||
} else if (!quickSummaryMismatchMode) {
|
||||
quickMismatchBtn.textContent = "快速处理字段不一致";
|
||||
} else if (remaining) {
|
||||
quickMismatchBtn.textContent = `按首个候选修正并下一条(剩 ${remaining})`;
|
||||
} else {
|
||||
quickMismatchBtn.textContent = "字段不一致已处理完";
|
||||
}
|
||||
}
|
||||
|
||||
function selectFirstQuickMismatchItem() {
|
||||
const item = firstMismatchWithCandidate();
|
||||
expandedSummarySearchId = item ? String(item.id) : "";
|
||||
editingSummarySearchId = "";
|
||||
editingSummaryIdentityId = "";
|
||||
}
|
||||
|
||||
function renderSummaryBindingDetail(item) {
|
||||
const binding = summaryBinding(item);
|
||||
if (binding.status === "matched" && binding.record) {
|
||||
@@ -1085,6 +1126,12 @@ async function loadSummarySearch() {
|
||||
if (!currentSummarySearchItems.some((item) => String(item.id) === expandedSummarySearchId)) expandedSummarySearchId = "";
|
||||
if (!currentSummarySearchItems.some((item) => String(item.id) === editingSummarySearchId)) editingSummarySearchId = "";
|
||||
if (!currentSummarySearchItems.some((item) => String(item.id) === editingSummaryIdentityId)) editingSummaryIdentityId = "";
|
||||
if (
|
||||
quickSummaryMismatchMode
|
||||
&& !mismatchItemsWithCandidates().some((item) => String(item.id) === String(expandedSummarySearchId))
|
||||
) {
|
||||
selectFirstQuickMismatchItem();
|
||||
}
|
||||
const bindingSummary = summarizeSummaryBindings(currentSummarySearchItems);
|
||||
summarySearchMeta.innerHTML = [
|
||||
metric("命中小结", `${data.count} 条`),
|
||||
@@ -1094,9 +1141,11 @@ async function loadSummarySearch() {
|
||||
metric("有候选", `${bindingSummary.withCandidate} 条`),
|
||||
].join("");
|
||||
renderCurrentSummarySearch();
|
||||
updateQuickMismatchButton();
|
||||
} catch (error) {
|
||||
currentSummarySearchItems = [];
|
||||
summarySearchRows.innerHTML = `<tr><td colspan="5" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
|
||||
updateQuickMismatchButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1150,7 +1199,7 @@ function findCurrentSummary(summaryId) {
|
||||
return currentSummarySearchItems.find((item) => String(item.id) === String(summaryId));
|
||||
}
|
||||
|
||||
async function applySummaryBindingCandidate(summaryId, record) {
|
||||
async function applySummaryBindingCandidate(summaryId, record, options = {}) {
|
||||
const item = findCurrentSummary(summaryId);
|
||||
if (!item) throw new Error("未找到当前课程小结");
|
||||
let activeSummaryId = summaryId;
|
||||
@@ -1180,11 +1229,53 @@ async function applySummaryBindingCandidate(summaryId, record) {
|
||||
}
|
||||
editingSummaryIdentityId = "";
|
||||
editingSummarySearchId = "";
|
||||
expandedSummarySearchId = activeSummaryId;
|
||||
expandedSummarySearchId = options.next ? "" : activeSummaryId;
|
||||
await loadSummarySearch();
|
||||
await loadOperationLogs();
|
||||
}
|
||||
|
||||
async function startQuickMismatchMode() {
|
||||
quickSummaryMismatchMode = true;
|
||||
quickSummaryProcessing = false;
|
||||
summarySearchBindingStatus.value = "mismatch";
|
||||
summarySearchHasCandidate.value = "true";
|
||||
await loadSummarySearch();
|
||||
selectFirstQuickMismatchItem();
|
||||
renderCurrentSummarySearch();
|
||||
updateQuickMismatchButton();
|
||||
}
|
||||
|
||||
async function processQuickMismatchItem() {
|
||||
if (!quickSummaryMismatchMode) {
|
||||
await startQuickMismatchMode();
|
||||
return;
|
||||
}
|
||||
const item = currentQuickMismatchItem();
|
||||
const binding = item ? summaryBinding(item) : null;
|
||||
const candidate = binding && Array.isArray(binding.candidates) ? binding.candidates[0] : null;
|
||||
if (!item || !candidate) {
|
||||
selectFirstQuickMismatchItem();
|
||||
renderCurrentSummarySearch();
|
||||
updateQuickMismatchButton();
|
||||
return;
|
||||
}
|
||||
quickSummaryProcessing = true;
|
||||
updateQuickMismatchButton();
|
||||
try {
|
||||
await applySummaryBindingCandidate(item.id, {
|
||||
student: candidate.student,
|
||||
teacher: candidate.teacher,
|
||||
time: candidate.time,
|
||||
subject: candidate.subject,
|
||||
}, { next: true });
|
||||
selectFirstQuickMismatchItem();
|
||||
renderCurrentSummarySearch();
|
||||
} finally {
|
||||
quickSummaryProcessing = false;
|
||||
updateQuickMismatchButton();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSummaryReviewEdits() {
|
||||
if (!activeSummaryReview) return;
|
||||
try {
|
||||
@@ -1770,10 +1861,24 @@ document.addEventListener("keydown", (event) => {
|
||||
});
|
||||
summarySearchForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
quickSummaryMismatchMode = false;
|
||||
loadSummarySearch();
|
||||
});
|
||||
summarySearchBindingStatus.addEventListener("change", loadSummarySearch);
|
||||
summarySearchHasCandidate.addEventListener("change", loadSummarySearch);
|
||||
summarySearchBindingStatus.addEventListener("change", () => {
|
||||
quickSummaryMismatchMode = false;
|
||||
loadSummarySearch();
|
||||
});
|
||||
summarySearchHasCandidate.addEventListener("change", () => {
|
||||
quickSummaryMismatchMode = false;
|
||||
loadSummarySearch();
|
||||
});
|
||||
quickMismatchBtn.addEventListener("click", () => {
|
||||
processQuickMismatchItem().catch((error) => {
|
||||
quickSummaryProcessing = false;
|
||||
updateQuickMismatchButton();
|
||||
alert(error.message);
|
||||
});
|
||||
});
|
||||
summarySearchRows.addEventListener("click", (event) => {
|
||||
const saveTime = event.target.closest(".summary-time-save");
|
||||
const deleteSummary = event.target.closest(".summary-delete");
|
||||
@@ -1928,4 +2033,5 @@ refreshBtn.addEventListener("click", () => {
|
||||
});
|
||||
|
||||
loadAdminHealth();
|
||||
updateQuickMismatchButton();
|
||||
loadAccounts();
|
||||
|
||||
Reference in New Issue
Block a user