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