eight-hourr/frontend/compbase.js

202 lines
6.3 KiB
JavaScript
Raw Permalink Normal View History

2026-07-17 06:55:36 +00:00
/**
* CompBase attendance: scan missing clock-out (應刷未刷) and auto-fill via RID page.
*/
function setActionStatus(text, ok) {
const el = document.getElementById("actionStatus");
if (!el) return;
el.textContent = text || "";
el.className = "settings-status" + (ok === true ? " ok" : ok === false ? " err" : "");
}
function escapeHtml(text) {
return String(text)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function getDays() {
const n = Number(document.getElementById("days")?.value || 14);
if (!Number.isFinite(n) || n < 1) return 14;
return Math.min(62, Math.floor(n));
}
function getOutTimeMode() {
return document.getElementById("outTimeMode")?.value || "expected";
}
function isDryRun() {
return Boolean(document.getElementById("dryRun")?.checked);
}
async function refreshConn() {
const line = document.getElementById("connLine");
try {
const data = await api("/api/compbase/status");
if (data.default_days && document.getElementById("days")) {
document.getElementById("days").value = data.default_days;
}
if (data.out_time_mode && document.getElementById("outTimeMode")) {
document.getElementById("outTimeMode").value = data.out_time_mode;
}
if (!line) return;
if (!data.has_credentials) {
line.textContent = "未設定帳密(請到設定登入 PTS或填 CompBase 帳密)";
return;
}
const name = data.display_name || data.username || "—";
const emp = data.emp_id ? ` · ${data.emp_id}` : "";
const src = data.credential_source === "compbase" ? "CompBase 帳密" : "PTS 帳密";
line.textContent = data.connected
? `CompBase ✓ · ${name}${emp} · ${src}`
: `CompBase ✗ · ${data.error || "連線失敗"} · ${src}`;
} catch (err) {
if (line) line.textContent = err.message;
}
}
function rowStatusLabel(row, kind) {
if (kind === "filled") {
if (row.dry_run) return `試跑 · 將送 ${escapeHtml(row.chosen_out_time || "—")}`;
return `已送出 · ${escapeHtml(row.chosen_out_time || "—")}${row.message ? " · " + escapeHtml(row.message) : ""}`;
}
if (kind === "error") {
return `失敗 · ${escapeHtml(row.error || row.message || "unknown")}`;
}
const reason = row.reason || row.status || "skipped";
const map = {
no_rid: "無 RID可能缺刷進",
missing_in: "缺刷進",
missing_out: "缺刷退但無連結",
other: "其他異常",
};
return map[reason] || escapeHtml(String(reason));
}
function renderResults(data, mode) {
const root = document.getElementById("resultView");
if (!root) return;
const filled = data.filled || data.fillable || [];
const skipped = data.skipped || [];
const errors = data.errors || [];
const isScan = mode === "scan";
const summaryParts = [
`區間 ${escapeHtml(data.start || "—")}${escapeHtml(data.end || "—")}`,
isScan
? `可補 ${data.fillable_count ?? filled.length}`
: `完成 ${data.filled_count ?? filled.length}`,
`略過 ${data.skipped_count ?? skipped.length}`,
];
if (!isScan) {
summaryParts.push(`失敗 ${data.error_count ?? errors.length}`);
if (data.dry_run) summaryParts.push("試跑");
}
const blocks = [];
blocks.push(`<p class="muted-inline">${summaryParts.join(" · ")}</p>`);
function table(title, rows, kind) {
if (!rows.length) return "";
const body = rows
.map((r) => {
return `<tr>
<td>${escapeHtml(r.date || "—")}</td>
<td><code>${escapeHtml(r.rid || "—")}</code></td>
<td>${escapeHtml(r.actual_in || r.in_time || "—")}</td>
<td>${escapeHtml(r.exp_out || "—")}</td>
<td>${rowStatusLabel(r, kind)}</td>
</tr>`;
})
.join("");
return `<div class="plan-day">
<div class="plan-day-head"><strong>${escapeHtml(title)}</strong><span class="muted-inline">${rows.length}</span></div>
<div class="table-wrap">
<table class="data-table">
<thead><tr><th>日期</th><th>RID</th><th></th><th></th><th></th></tr></thead>
<tbody>${body}</tbody>
</table>
</div>
</div>`;
}
if (isScan) {
blocks.push(table("可補填(有 RID", filled, "filled"));
blocks.push(table("略過", skipped, "skipped"));
} else {
blocks.push(table(data.dry_run ? "試跑將送出" : "已送出", filled, "filled"));
blocks.push(table("略過", skipped, "skipped"));
blocks.push(table("失敗", errors, "error"));
}
if (blocks.length === 1) {
blocks.push(`<p class="hint">近 ${escapeHtml(String(data.days || ""))} 天沒有可處理的漏刷退。</p>`);
}
root.innerHTML = blocks.join("");
}
async function runScan() {
const days = getDays();
setActionStatus(`掃描近 ${days} 天…`);
try {
const data = await api("/api/compbase/scan", {
method: "POST",
body: JSON.stringify({ days }),
});
renderResults(data, "scan");
setActionStatus(
`掃描完成:可補 ${data.fillable_count ?? 0}、略過 ${data.skipped_count ?? 0}`,
true
);
} catch (err) {
setActionStatus(err.message, false);
}
}
async function runFill() {
const days = getDays();
const dryRun = isDryRun();
const outTimeMode = getOutTimeMode();
if (!dryRun) {
const ok = window.confirm(
`將對近 ${days} 天的「應刷未刷」送出刷退補填到 CompBase。\n確定繼續?`
);
if (!ok) return;
}
setActionStatus(dryRun ? `試跑近 ${days} 天…` : `補填近 ${days} 天…`);
try {
const data = await api("/api/compbase/fill", {
method: "POST",
body: JSON.stringify({
days,
dry_run: dryRun,
out_time_mode: outTimeMode,
}),
});
renderResults(data, "fill");
const ok = (data.error_count ?? 0) === 0;
setActionStatus(
`${dryRun ? "試跑" : "補填"}完成:成功 ${data.filled_count ?? 0}、略過 ${data.skipped_count ?? 0}、失敗 ${data.error_count ?? 0}`,
ok
);
} catch (err) {
setActionStatus(err.message, false);
}
}
function init() {
document.getElementById("scanBtn")?.addEventListener("click", runScan);
document.getElementById("fillBtn")?.addEventListener("click", runFill);
refreshConn();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}