eight-hourr/frontend/app.js

287 lines
10 KiB
JavaScript
Raw Permalink Normal View History

2026-07-17 06:55:36 +00:00
/**
* Home flow:
* - range mode: start~end, skip TW holidays/weekends
* - list mode: exact dates, fill even on holidays
* Preview/fill: Teams meetings + GitLab issues PTS
*/
function setDefaultDates() {
const today = todayStr();
document.getElementById("startDate").value = today;
document.getElementById("endDate").value = today;
}
function getDateMode() {
const checked = document.querySelector('input[name="dateMode"]:checked');
return checked ? checked.value : "range";
}
function getDateRange() {
const start = document.getElementById("startDate")?.value || todayStr();
let end = document.getElementById("endDate")?.value || start;
if (end < start) {
document.getElementById("endDate").value = start;
document.getElementById("startDate").value = end;
return { start: end, end: start };
}
return { start, end };
}
function getDateListText() {
return (document.getElementById("dateList")?.value || "").trim();
}
function parseDateList(text) {
const dates = [];
for (const line of text.split(/\n+/)) {
const t = line.trim();
if (!t || t.startsWith("#")) continue;
for (const part of t.split(/[\s,;]+/)) {
const d = part.trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(d)) dates.push(d);
}
}
return [...new Set(dates)].sort();
}
function setRangeToToday() {
const today = todayStr();
document.getElementById("startDate").value = today;
document.getElementById("endDate").value = today;
updateRangeMeta();
}
function updateDateModeUI() {
const mode = getDateMode();
document.getElementById("rangePanel")?.classList.toggle("hidden", mode !== "range");
document.getElementById("listPanel")?.classList.toggle("hidden", mode !== "list");
updateRangeMeta();
}
function updateRangeMeta() {
const el = document.getElementById("rangeMeta");
if (!el) return;
if (getDateMode() === "list") {
const dates = parseDateList(getDateListText());
el.textContent = dates.length
? `指定 ${dates.length} 天(含假日也填):${dates.join("、")}`
: "請輸入至少一個日期";
return;
}
const { start, end } = getDateRange();
el.textContent = `區間 ${start}${end}(自動跳過週末/台灣假日)`;
}
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
async function refreshConn() {
const line = document.getElementById("connLine");
try {
const data = await api("/api/status");
const teams = data.teams_calendar || {};
const parts = [
data.has_pts_session ? "工時 ✓" : "工時 ✗",
data.gitlab_configured && !data.gitlab_error ? "任務 ✓" : "任務 ✗",
data.teams_calendar_enabled ? (teams.connected ? "行事曆 ✓" : "行事曆 ✗") : "行事曆 關",
];
if (line) {
line.textContent = parts.join(" · ");
if (data.gitlab_error) line.textContent += " — " + data.gitlab_error;
}
} catch (err) {
if (line) line.textContent = err.message;
}
}
function renderPlan(data, { filled } = {}) {
const root = document.getElementById("planView");
if (!root) return;
const workdays = data.workdays || [];
const plan = data.plan || {};
const meetings = data.analysis?.meetings || data.meetings || {};
const source = data.source || "";
const gitlabN = data.gitlab_task_count ?? "—";
const meetingNote =
meetings.enabled === false
? "會議未啟用"
: meetings.error
? `會議:${meetings.error}`
: meetings.meeting_count != null
? `會議 ${meetings.meeting_count} 場 · ${meetings.meeting_hours_total ?? 0}h`
: "";
if (!workdays.length && !Object.keys(plan).length) {
root.innerHTML = `<p class="hint">沒有可填日期。區間模式會跳過假日;指定多日請確認日期格式。</p>`;
return;
}
const days = workdays.length ? workdays : Object.keys(plan).sort();
const dayBlocks = days
.map((day) => {
const entries = plan[day] || [];
if (!entries.length) {
return `<div class="plan-day">
<div class="plan-day-head"><strong>${escapeHtml(day)}</strong><span class="muted-inline"></span></div>
</div>`;
}
const hours = entries.reduce((s, e) => s + Number(e.take_hours || 0), 0);
const rows = entries
.map((e) => {
const desc = String(e.description || "");
const isMeeting =
(e.pts_task_name || "").toLowerCase().includes("meet") ||
desc.toLowerCase().includes("meeting") ||
desc.toLowerCase().includes("attended");
const tag = isMeeting
? '<span class="tag">會議</span>'
: e.gitlab_task_id
? '<span class="tag">GitLab</span>'
: "";
return `<li class="plan-row">
<span class="meeting-hours">${escapeHtml(String(e.take_hours))}h</span>
<span class="meeting-subject">${tag}${escapeHtml(e.pts_task_name || "")} · ${escapeHtml(desc)}</span>
</li>`;
})
.join("");
return `<div class="plan-day">
<div class="plan-day-head">
<strong>${escapeHtml(day)}</strong>
<span class="muted-inline">${entries.length} · ${hours}h</span>
</div>
<ul class="meeting-items">${rows}</ul>
</div>`;
})
.join("");
const title = filled ? "已填寫" : "預覽";
const dry = data.dry_run ? "(試跑)" : "";
root.innerHTML = `
<div class="meetings-summary">
${pill(title + dry, source || "ok", true)}
${pill("天數", String(days.length), null)}
${pill("GitLab", String(gitlabN), null)}
${meetingNote ? pill("Teams", meetingNote, meetings.error ? false : null) : ""}
</div>
<div class="meetings-list">${dayBlocks}</div>
`;
}
async function runPreview() {
const mode = getDateMode();
setActionStatus("預覽中…");
document.getElementById("planView").innerHTML = `<p class="hint">會議 + GitLab 分配中…</p>`;
try {
let data;
if (mode === "list") {
const dates = parseDateList(getDateListText());
if (!dates.length) throw new Error("請至少輸入一個日期YYYY-MM-DD");
data = await api("/api/preview/dates", {
method: "POST",
body: JSON.stringify({ dates_text: dates.join("\n") }),
});
document.getElementById("rangeMeta").textContent =
`指定 ${dates.length} 天(含假日):${dates.join("、")}`;
} else {
const { start, end } = getDateRange();
data = await api(
`/api/preview?start_date=${encodeURIComponent(start)}&end_date=${encodeURIComponent(end)}`
);
document.getElementById("rangeMeta").textContent =
`區間 ${start}${end} · 工作日 ${(data.workdays || []).length} 天(已跳過假日)`;
}
renderPlan(data, { filled: false });
setActionStatus("預覽完成(尚未送出)", true);
} catch (err) {
document.getElementById("planView").innerHTML =
`<p class="settings-status err">${escapeHtml(err.message)}</p>`;
setActionStatus(err.message, false);
}
}
async function runFill() {
const mode = getDateMode();
const dryRun = document.getElementById("dryRun").checked;
setActionStatus(`${dryRun ? "試跑" : "填寫"}中…`);
document.getElementById("planView").innerHTML = `<p class="hint">處理中…</p>`;
try {
let data;
if (mode === "list") {
const dates = parseDateList(getDateListText());
if (!dates.length) throw new Error("請至少輸入一個日期YYYY-MM-DD");
data = await api("/api/fill/dates", {
method: "POST",
body: JSON.stringify({
dates_text: dates.join("\n"),
dry_run: dryRun,
skip_existing: true,
}),
});
document.getElementById("rangeMeta").textContent =
`指定 ${dates.length}` + (dryRun ? " · 試跑" : " · 已送出");
} else {
const { start, end } = getDateRange();
data = await api("/api/fill/range", {
method: "POST",
body: JSON.stringify({
start_date: start,
end_date: end,
dry_run: dryRun,
skip_existing: true,
}),
});
document.getElementById("rangeMeta").textContent =
`區間 ${start}${end}` + (dryRun ? " · 試跑" : " · 已送出");
}
if (data.plan || data.workdays) {
renderPlan({ ...data, dry_run: dryRun }, { filled: !dryRun });
} else {
document.getElementById("planView").innerHTML = `
<div class="meetings-summary">
${pill(dryRun ? "試跑完成" : "填寫完成", "ok", true)}
${pill("新增", String((data.created || []).length), null)}
${pill("略過", String((data.skipped || []).length), null)}
</div>
<pre class="preview">${escapeHtml(JSON.stringify(data, null, 2))}</pre>
`;
}
setActionStatus(dryRun ? "試跑完成" : "填寫完成", true);
await refreshConn();
} catch (err) {
document.getElementById("planView").innerHTML =
`<p class="settings-status err">${escapeHtml(err.message)}</p>`;
setActionStatus(err.message, false);
}
}
document.querySelectorAll('input[name="dateMode"]').forEach((el) => {
el.addEventListener("change", updateDateModeUI);
});
document.getElementById("setTodayBtn")?.addEventListener("click", setRangeToToday);
document.getElementById("previewBtn")?.addEventListener("click", () => runPreview().catch(console.error));
document.getElementById("fillBtn")?.addEventListener("click", () => runFill().catch(console.error));
document.getElementById("startDate")?.addEventListener("change", updateRangeMeta);
document.getElementById("endDate")?.addEventListener("change", updateRangeMeta);
document.getElementById("dateList")?.addEventListener("input", updateRangeMeta);
setDefaultDates();
updateDateModeUI();
refreshConn().catch(console.error);