202 lines
6.3 KiB
JavaScript
202 lines
6.3 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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, "<")
|
|||
|
|
.replace(/>/g, ">")
|
|||
|
|
.replace(/"/g, """);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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();
|
|||
|
|
}
|