eight-hourr/frontend/compbase.js

202 lines
6.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
}