From 4356160cc9ba5e1ecf95e5c511776537dbd5de25 Mon Sep 17 00:00:00 2001 From: Wyndham ARR Date: Fri, 9 Oct 2026 13:20:13 +0800 Subject: [PATCH] Add daily report calendar to the header --- .../tasks/20261009-report-calendar-d19a.md | 66 ++++ arr_web/DIRECT_DATA_ENTRY.md | 6 +- arr_web/README.md | 2 + arr_web/static/app.js | 232 ++++++++++++- arr_web/static/i18n.js | 18 + arr_web/static/index.html | 28 +- arr_web/static/styles.css | 41 ++- tests/javascript/daily_report_lifecycle.cjs | 4 - tests/javascript/helpers/arr_ui_harness.cjs | 6 +- tests/javascript/report_calendar.cjs | 326 ++++++++++++++++++ tests/test_arr_web_daily_visual_ui.py | 34 +- 11 files changed, 735 insertions(+), 28 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20261009-report-calendar-d19a.md create mode 100644 tests/javascript/report_calendar.cjs diff --git a/.project-docs/30-worklog/tasks/20261009-report-calendar-d19a.md b/.project-docs/30-worklog/tasks/20261009-report-calendar-d19a.md new file mode 100644 index 0000000..1dc696c --- /dev/null +++ b/.project-docs/30-worklog/tasks/20261009-report-calendar-d19a.md @@ -0,0 +1,66 @@ +# Task: 顶部报表日历与有效日报日期展示 + +## Identity + +- Task ID: 20261009-report-calendar-d19a +- Mode: Feature +- Branch: codex/20261009-report-calendar-d19a-report-calendar +- Worktree: /Users/chillishark/.codex/worktrees/arr-report-calendar +- Base commit: 2a54838d59466139c41a9c7fe79593242d6da982 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Replace the daily header processing-duration card with an always-visible report calendar after ARRIVAL DATE and NO. OF ROOM. Default to current business month, distinguish effective generated/ungenerated dates, support month browsing and selecting a date without acquiring/processing data. + +## Intent And Constraints + +- Preserve current blue/white product style, shared XML/OHIP business rules, valid daily-version identity, authenticated read-only APIs and original tasks/data. Only an actual current effective daily report counts as generated; historical/deleted/pending-only records do not. Pending replacement retains generated status if a current daily exists. Month/date viewing never submits acquisition. Preserve unsaved review decisions and concurrency guards. No Oracle calls or actual report mutation. Safely activate only with no active task; prior repository publication authorization persists. + +## Outcome + +- Header now presents automatic download, XML upload, ARRIVAL DATE, NO. OF ROOM and Reports in that order. The header duration card is removed; daily history duration remains available. +- The calendar defaults to the Bangkok business month, browses months, shows full-month effective report coverage and selects a date for read-only overview/list viewing. Current accepted/succeeded, non-retired reports count as generated even with zero rooms or a pending replacement; pending-only, historical and deleted-only versions do not. Loading/error states remain unknown, with retry. +- Three locales, visible status labels, keyboard month/date navigation, focus/selected/today states and responsive layouts are implemented. Independent calendar loading does not depend on daily-list pagination or add a polling timer. +- Date selection is protected against unsaved prices, active submissions/reviews, stale month responses and same-date environment changes. XML upload and price/field review completion restore calendar availability immediately. Independent review found and verified fixes for two concurrency/busy-state defects before activation. +- Local 8875 now runs this retained worktree. Activation changed only the launcher/project paths, with a cold backup at /Users/chillishark/Library/Application Support/ARR2.0/report-calendar-backup-20261009-051154. Schema remains 008-022, with no business-data mutation or Oracle query. Repository publication is authorized to arr0918/main; the final push receipt is stored privately after publication. + +## Verification + +- 132 JavaScript UI tests passed, including 27 new calendar cases for effective coverage, empty/leap months, incomplete/error/late responses, safe selection, unsaved prices, environment changes, refresh after generation/deletion, XML upload and review completion/cancellation/failure. +- 15 relevant daily visual/price-review static checks passed; 4 readonly preview tests passed. JavaScript syntax and git diff --check passed. +- Independent readonly review closed both identified defects and reconfirmed GET-only selection, current-report authority, and unchanged XML/OHIP/monthly processing rules. +- Browser proof: isolated readonly synthetic fixtures confirmed default October, September current/pending/history differences, calendar/list date synchronization, English/Thai Gregorian labels, keyboard crossing October31 to November1 and a complete empty month. Actual CSS widths 375, 767 (near768), 1024 and1440 had no page horizontal overflow; mobile day targets were approximately44px. +- Actual 8875 proof: October7 generated, September16/17 generated, September13/15 ungenerated. Selecting September17 shows 2026/09/17 and43 rooms and switches the daily list to September, while both existing field-review dates remain. Saved live-report-calendar.png privately under report-calendar-validation-20261009; no report download, acquisition, review submission or deletion was performed. +- Before/after private comparison preserved all27 table fingerprints, 3 current report pointers, 4896 source files, monthly workbook/API records and queue statuses. Pending field-review dates remain September15 and13; retirement audit unchanged. +- An additional existing history/task-log/BI static group has 3 stale source-string assertions already inconsistent with base2a54838 (history direct expression, old renderJobs signature, single openTaskLog call). They are not introduced by this feature; files remain unchanged. This is not a blanket full-suite success claim. + +## Follow-ups + +- No product work remains. Keep this worktree because the local service runs from it. Canonical memory reconciliation remains a separate serialized integration task. + +## Promotion Candidates + +- Target: current-state.md and portal architecture/domain memory. Proposal: record effective-report month coverage, header order without duration, read-only calendar selection, and separate viewed date/review context. Evidence: explicit user instructions, 132 JS/15 UI checks and actual8875 preservation/browser proof. Future impact: prevents historical or pending tasks from being shown as generated, and prevents future view controls from submitting acquisition. No semantic conflict with accepted processing rules; user approval of this product behavior already supplied. Feature mode records this candidate without canonical writes. + +## Concurrent Task Gate + +- Passed: task_context.py start exited0 and isolated task20261009-report-calendar-d19a, feature/codex, branchcodex/20261009-report-calendar-d19a-report-calendar, managed worktree/Users/chillishark/.codex/worktrees/arr-report-calendar, base2a54838d59466139c41a9c7fe79593242d6da982. status confirms ownership and peer scopes. production-review and daily-lifecycle tasks Ready for Integration, inspected read-only; current8875 runs daily-lifecycle checkout. No write overlap; dirty primary checkout untouched. + +## Project Context Loaded + +- Read: entry/planning gate, own task record, memory-index, positioning, current-state, decision-index, system-overview, ADR007, domain/success-criteria, evidence/commitments/stale context and both peer task Scope/Constraints/Promotion Candidates. +- Project goal: finance operator XML/OHIP inputs processed by shared deterministic rules into effective daily facts and automatically updated monthly workbooks. +- Current focus: report coverage visible as a compact header month calendar and consistent selected-date overview. +- Constraints/decisions: effective current daily pointer is authoritative; source/history/audit remain retained. Only successful new version replaces current; no changes to business scope/price/review/Finance/monthly processing. View actions remain GET-only. +- Evidence/conflicts: canonical September state is stale versus direct user instructions and completed October9 lifecycle task (hotel57106, processor4.4.0, schema022, multiple agents allowed). This task extends that completed base and does not modify canonical memory. User explicitly approved calendar direction and requested continuation. +- Modules: static index/styles/i18n/app, existing authenticated GET /api/daily-reports month list and exact-day overview, synthetic JS/readonly preview verification, private8875 launcher path. +- Unknowns: desktop card density, small-screen layout and asynchronous month/date refresh require implementation/browser proof; no business decision blocks work. +- Gate: Passed. + +## Plan + +1. Implement header order and accessible calendar with independent month state, whole-month effective-day reads and safe date selection. +2. Verify empty/partial months, pending/replaced/deleted days, month/year transitions, late responses and no automatic acquisition; check desktop and narrow layouts. +3. Safely update local code path while preserving current schema/data, capture browser proof, commit and push to approved repository. diff --git a/arr_web/DIRECT_DATA_ENTRY.md b/arr_web/DIRECT_DATA_ENTRY.md index d17e109..dc5dc8b 100644 --- a/arr_web/DIRECT_DATA_ENTRY.md +++ b/arr_web/DIRECT_DATA_ENTRY.md @@ -33,7 +33,11 @@ 日报列表按业务日期展示,一天只展示一行。接口取数和XML上传遵循相同的覆盖规则:新版本成功生成并入库后,才替换该日有效日报并更新月报;新版本尚待核对或失败时,原有效日报仍可下载并继续计入月报。 -点击列表中的日期,可查看该日概览;上方日期、房数和耗时始终对应当前查看日期。待处理新版本会单独提示,尚无有效日报时显示空值,不借用其他日期的成功结果。 +点击列表中的日期,可查看该日概览;上方日期与房数始终对应当前查看日期。待处理新版本会单独提示,尚无有效日报时显示空值,不借用其他日期的成功结果。 + +顶部概览提供“自动下载 ARR、上传 ARR.XML、ARRIVAL DATE、NO. OF ROOM、报表”五个区域,顶部不再展示处理耗时。报表日历默认显示当前业务月份,支持切换月份;每一天按是否存在当前有效日报标为“已生成”或“未生成”。历史版本、已删除版本和仅待核对的任务不算已生成;已有有效日报、另有待核对新版本时,仍标为已生成。读取中或读取失败会单独显示,不把未知状态当作未生成。 + +点击日历日期只切换查看日期和日报列表月份,不会获取Oracle数据、生成报表或提交人工核对值。需要下载新数据时,仍须在自动下载入口主动提交;未保存的人工价格继续受切换保护。日历月份与列表分页独立,显示整月状态;生成或删除后的刷新会更新标记。 “历史记录”保留同一天的旧版本和未完成版本。删除前页面会说明影响: diff --git a/arr_web/README.md b/arr_web/README.md index c8b3a7c..7594d04 100644 --- a/arr_web/README.md +++ b/arr_web/README.md @@ -2,6 +2,8 @@ 2026-10-09:日报按日期展示一个有效版本,支持查看历史、删除及月报同步;概览跟随所选日期。发布需要数据库022及配套月报worker,详见[交付说明](../deploy/OHIP_RELEASE_HANDOVER.md#日报版本管理升级2026-10-09)。 +顶部新增“报表”月历,按当前有效日报显示已生成/未生成日期,可切换月份和选择日期查看;顶部处理耗时已移除。日历只读取ARR已保存状态,不查询Oracle或自动创建报表。 + 2026-09-18:[直接数据入口](DIRECT_DATA_ENTRY.md)已接通日期按钮、日报、复核和月报;独立本机模拟8875,未查询实际Oracle。 2026-09-17:[本机接口模拟全流程及按钮联调](LOCAL_API_SIMULATION.md)已在独立8874验证;[原文件重放](LOCAL_XML_REPLAY.md)8873保留。两个入口均明确未连接Oracle,不改变默认运行配置。 diff --git a/arr_web/static/app.js b/arr_web/static/app.js index d41e3d0..380d139 100644 --- a/arr_web/static/app.js +++ b/arr_web/static/app.js @@ -59,6 +59,16 @@ dailyOverview: null, dailyOverviewTimer: null, dailyOverviewRequest: 0, + reportDateSelectionRequest: 0, + reportCalendarMonth: "", + reportCalendarDataMonth: "", + reportCalendarContext: "", + reportCalendarRows: [], + reportCalendarLoading: false, + reportCalendarError: false, + reportCalendarRequest: 0, + reportCalendarFocusDate: "", + reportCalendarInitialized: false, dailyHistoryDate: "", dailyHistoryItems: [], dailyHistoryTotal: 0, @@ -647,6 +657,7 @@ } const storageKey = `arr:last-download:v2:${encodeURIComponent(config.context_id)}:${encodeURIComponent(state.arrDownloadUsername)}`; if (state.arrDownloadStorageKey !== storageKey) { + state.reportDateSelectionRequest += 1; const contextChanged = state.arrDownloadContextId && state.arrDownloadContextId !== config.context_id; state.arrDownloadStorageKey = storageKey; state.arrDownloadContextId = config.context_id; @@ -669,6 +680,12 @@ state.dailyHistoryDate = ""; renderJobs(); renderDailyHistory(); + state.reportCalendarRequest += 1; + state.reportCalendarRows = []; + state.reportCalendarDataMonth = ""; + state.reportCalendarError = false; + state.reportCalendarLoading = true; + loadReportCalendar(); } state.manualReviewMode = ""; state.dailyPriceReview = null; @@ -850,6 +867,7 @@ } function renderARRDataReview() { + renderReportCalendar(); const panel = $("#arr-data-review-panel"); const review = state.arrDataReview; renderManualReviewWorkspace(); @@ -1319,6 +1337,187 @@ updateCompanyReportControls(); } + function reportCalendarSelectionLocked() { + return Boolean(arrDownloadActive() || state.arrDownloadBusy || state.arrDownloadLoading + || state.arrDataReviewLoading || state.arrDataReviewMutating || state.arrDataReviewFinalizing + || state.dailyPriceReviewLoading || state.dailyPriceReviewMutating + || state.uploadInFlight || state.dailyDeleting); + } + + function currentReportGenerated(row) { + const job = row?.current_job; + return Boolean(job && job.is_current === true && !job.is_retired + && ["succeeded", "accepted"].includes(job.status)); + } + + function reportCalendarReady() { + return !state.reportCalendarLoading && !state.reportCalendarError + && state.reportCalendarDataMonth === state.reportCalendarMonth + && state.reportCalendarContext === state.arrDownloadContextId; + } + + function renderReportCalendar(focusDay = false) { + const month = state.reportCalendarMonth || localMonth(); + state.reportCalendarMonth = month; + const first = new Date(`${month}-01T12:00:00Z`); + const dayCount = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate(); + const leading = (first.getUTCDay() + 6) % 7; + const selected = state.dailyOverviewDate; + const today = localDate(); + if (!validARRDate(state.reportCalendarFocusDate) || state.reportCalendarFocusDate.slice(0, 7) !== month) { + state.reportCalendarFocusDate = selected?.slice(0, 7) === month ? selected + : today.slice(0, 7) === month ? today : `${month}-01`; + } + const activeDay = document.activeElement?.closest?.("[data-report-calendar-day]"); + const restoreDayFocus = focusDay || Boolean(activeDay); + const ready = reportCalendarReady(); + const locked = reportCalendarSelectionLocked(); + const format = (options) => new Intl.DateTimeFormat(I18N?.locale || "zh-CN", { timeZone: "UTC", calendar: "gregory", ...options }); + const dateLabel = format({ year: "numeric", month: "long", day: "numeric", weekday: "long" }); + const weekday = format({ weekday: "short" }); + const weekdays = []; + for (let index = 0; index < 7; index += 1) { + const monday = new Date("2026-09-07T12:00:00Z"); + monday.setUTCDate(monday.getUTCDate() + index); + weekdays.push(``); + } + const rows = new Map(state.reportCalendarRows.map((row) => [row.business_date, row])); + const days = Array.from({ length: leading }, () => ''); + let generated = 0; + for (let number = 1; number <= dayCount; number += 1) { + const value = `${month}-${String(number).padStart(2, "0")}`; + const exists = ready && currentReportGenerated(rows.get(value)); + if (exists) generated += 1; + const status = !ready ? state.reportCalendarLoading ? "loading_day" : "unknown" : exists ? "generated" : "not_generated"; + const statusLabel = I18N.t(`report_calendar.${status}`); + const shortLabel = I18N.t(`report_calendar.day_${status}`); + const symbol = exists ? "✓" : ready ? "—" : "·"; + const selectedDay = value === selected; + days.push(``); + } + $("#report-calendar-month").textContent = I18N.formatMonth(month); + $("#report-calendar-previous").disabled = month === "2020-01"; + $("#report-calendar-next").disabled = month === "2099-12"; + $("#report-calendar-previous").setAttribute("aria-label", I18N.t("history.previous_month")); + $("#report-calendar-next").setAttribute("aria-label", I18N.t("history.next_month")); + $("#report-calendar-current").textContent = I18N.t("history.current_month"); + $("#report-calendar-weekdays").innerHTML = weekdays.join(""); + $("#report-calendar-days").innerHTML = days.join(""); + $("#report-calendar").setAttribute("aria-busy", String(state.reportCalendarLoading)); + $("#report-calendar-status").textContent = I18N.t(state.reportCalendarLoading ? "report_calendar.loading" + : state.reportCalendarError ? "report_calendar.failed" : ready ? "report_calendar.coverage" : "report_calendar.loading", + { count: formatInteger(generated), total: formatInteger(dayCount) }); + $("#report-calendar-help").textContent = I18N.t(locked ? "report_calendar.locked" : "report_calendar.help"); + $("#report-calendar-retry").hidden = !state.reportCalendarError; + $("#report-calendar-legend-generated").textContent = I18N.t("report_calendar.generated"); + $("#report-calendar-legend-empty").textContent = I18N.t("report_calendar.not_generated"); + if (restoreDayFocus) $(`[data-report-calendar-day="${state.reportCalendarFocusDate}"]`, $("#report-calendar-days"))?.focus(); + } + + async function loadReportCalendar() { + if (!state.reportCalendarInitialized) return; + const month = state.reportCalendarMonth || localMonth(); + const context = state.arrDownloadContextId; + const sequence = ++state.reportCalendarRequest; + state.reportCalendarLoading = true; + state.reportCalendarError = false; + renderReportCalendar(); + const isCurrent = () => state.reportCalendarMonth === month + && state.arrDownloadContextId === context && state.reportCalendarRequest === sequence; + try { + const envelope = await api(`/api/daily-reports?month=${encodeURIComponent(month)}&limit=200&offset=0`, { returnEnvelope: true }); + if (!isCurrent()) return; + const rows = envelope?.data; + const total = Number(envelope?.pagination?.total); + // Only a complete successful month snapshot can establish an ungenerated day. + if (!Array.isArray(rows) || !Number.isInteger(total) || total !== rows.length || total > 31 + || rows.some((row) => !validARRDate(row?.business_date) || row.business_date.slice(0, 7) !== month + || !Object.prototype.hasOwnProperty.call(row, "current_job")) + || new Set(rows.map((row) => row.business_date)).size !== rows.length) throw new Error("Incomplete calendar snapshot"); + state.reportCalendarRows = rows; + state.reportCalendarDataMonth = month; + state.reportCalendarContext = context; + } catch (_) { + if (!isCurrent()) return; + state.reportCalendarError = true; + state.reportCalendarRows = []; + state.reportCalendarDataMonth = ""; + } finally { + if (isCurrent()) { + state.reportCalendarLoading = false; + renderReportCalendar(); + } + } + } + + async function changeReportCalendarMonth(month, focusDate = "") { + if (!validMonth(month) || month < "2020-01" || month > "2099-12") return; + state.reportCalendarMonth = month; + state.reportCalendarFocusDate = focusDate; + state.reportCalendarRequest += 1; + state.reportCalendarRows = []; + state.reportCalendarDataMonth = ""; + state.reportCalendarError = false; + state.reportCalendarLoading = true; + renderReportCalendar(Boolean(focusDate)); + await loadReportCalendar(); + } + + async function selectReportCalendarDate(date) { + if (!validARRDate(date) || date.slice(0, 7) !== state.reportCalendarMonth || reportCalendarSelectionLocked()) return false; + const context = state.arrDownloadContextId; + const month = state.reportCalendarMonth; + const pending = selectDailyReportDate(date, { syncHistory: true }); + const sequence = state.reportDateSelectionRequest; + const selected = await pending; + if (selected && context === state.arrDownloadContextId && sequence === state.reportDateSelectionRequest + && month === state.reportCalendarMonth && state.dailyOverviewDate === date) { + state.reportCalendarFocusDate = date; + renderReportCalendar(true); + } + return selected; + } + + async function moveReportCalendarFocus(key, date) { + if (!validARRDate(date) || reportCalendarSelectionLocked()) return; + const day = new Date(`${date}T12:00:00Z`); + if (["PageUp", "PageDown"].includes(key)) { + const month = shiftMonth(date.slice(0, 7), key === "PageUp" ? -1 : 1); + const last = new Date(`${month}-01T12:00:00Z`); + last.setUTCMonth(last.getUTCMonth() + 1); + last.setUTCDate(0); + await changeReportCalendarMonth(month, `${month}-${String(Math.min(day.getUTCDate(), last.getUTCDate())).padStart(2, "0")}`); + return; + } + const weekday = (day.getUTCDay() + 6) % 7; + const deltas = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7, Home: -weekday, End: 6 - weekday }; + if (!Object.prototype.hasOwnProperty.call(deltas, key)) return; + day.setUTCDate(day.getUTCDate() + deltas[key]); + const value = day.toISOString().slice(0, 10); + if (value.slice(0, 7) !== state.reportCalendarMonth) await changeReportCalendarMonth(value.slice(0, 7), value); + else { + state.reportCalendarFocusDate = value; + renderReportCalendar(true); + } + } + + function bindReportCalendar() { + $("#report-calendar-previous").addEventListener("click", () => changeReportCalendarMonth(shiftMonth(state.reportCalendarMonth, -1))); + $("#report-calendar-next").addEventListener("click", () => changeReportCalendarMonth(shiftMonth(state.reportCalendarMonth, 1))); + $("#report-calendar-current").addEventListener("click", () => changeReportCalendarMonth(localMonth())); + $("#report-calendar-retry").addEventListener("click", loadReportCalendar); + $("#report-calendar-days").addEventListener("click", (event) => { + const day = event.target.closest("[data-report-calendar-day]"); + if (day) selectReportCalendarDate(day.dataset.reportCalendarDay); + }); + $("#report-calendar-days").addEventListener("keydown", (event) => { + const day = event.target.closest("[data-report-calendar-day]"); + if (!day || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"].includes(event.key)) return; + event.preventDefault(); + moveReportCalendarFocus(event.key, day.dataset.reportCalendarDay); + }); + } + function dailyJobSource(job) { return job?.filename === "ARR.json" ? I18N.t("arr_download.source_label") : job?.filename || "—"; } @@ -1365,15 +1564,15 @@ $("#metric-current-version").hidden = !current || (!pending && !failed); $("#metric-current-version").textContent = I18N.t("daily.current_preserved"); const showResult = Boolean(current && !pending); - $("#metric-duration").textContent = showResult ? duration(current) : "—"; $("#metric-rooms").textContent = showResult ? formatInteger(current.no_of_rooms) : "—"; - $("#metric-duration-note").textContent = I18N.t(showResult ? "metric.duration_from_submit" : "daily.after_generation"); $("#metric-rooms-note").textContent = I18N.t(showResult ? "metric.rooms" : "daily.after_generation"); + renderReportCalendar(); } function syncDailyOverviewDate(date) { if (!validARRDate(date)) { state.dailyOverviewDate = ""; + state.reportDateSelectionRequest += 1; state.dailyOverview = null; state.dailyOverviewRequest += 1; window.clearTimeout(state.dailyOverviewTimer); @@ -1381,6 +1580,7 @@ return; } if (state.dailyOverviewDate !== date) { + state.reportDateSelectionRequest += 1; state.dailyOverviewDate = date; state.dailyOverview = null; state.dailyOverviewRequest += 1; @@ -1406,15 +1606,28 @@ } catch (_) { /* Do not substitute a different date's result when this day cannot be read. */ } } - async function selectDailyReportDate(date) { - if (!validARRDate(date) || state.dailyDeleting || state.arrDownloadBusy || dailyPriceReviewHasUnsavedChanges()) { + async function selectDailyReportDate(date, { syncHistory = false } = {}) { + if (!validARRDate(date) || reportCalendarSelectionLocked() || dailyPriceReviewHasUnsavedChanges()) { if (dailyPriceReviewHasUnsavedChanges()) showToast(I18N.t("daily_review.save_before_switch"), true); - return; + return false; } $("#arr-download-date").value = date; state.arrDownloadDateEdited = true; renderARRDownload(); + const context = state.arrDownloadContextId; + const sequence = ++state.reportDateSelectionRequest; + const isCurrent = () => state.dailyOverviewDate === date && state.arrDownloadContextId === context + && state.reportDateSelectionRequest === sequence; if (!dailyOverviewRow()) await loadDailyOverview(date); + if (!isCurrent()) return false; + if (syncHistory && state.jobsMonth !== date.slice(0, 7)) { + state.jobsMonth = date.slice(0, 7); + state.jobsOffset = 0; + renderHistoryMonthControl("jobs"); + await loadJobs(false, false); + if (!isCurrent()) return false; + } + return isCurrent(); } function renderJobs() { @@ -1676,6 +1889,7 @@ } function renderDailyPriceReview(review = state.dailyPriceReview) { + renderReportCalendar(); const panel = $("#daily-price-review-panel"); const body = $("#daily-price-review-body"); const empty = $("#daily-price-review-empty"); @@ -2261,6 +2475,7 @@ ); setTraceLiveState(I18N?.t("task.live_unselected") || "未选择任务"); } + await loadReportCalendar(); } catch (error) { if (!isCurrent()) return; $("#jobs-body").innerHTML = `${escapeHtml(I18N?.t("daily.unavailable") || "日报记录暂时无法读取")}`; @@ -2633,6 +2848,7 @@ showToast(error.message, true); } finally { state.uploadInFlight = false; + renderReportCalendar(); fileInput.disabled = !state.health.processing_ready; dropzone.classList.toggle("is-disabled", !state.health.processing_ready); button.removeAttribute("aria-busy"); @@ -3638,6 +3854,7 @@ renderARRDownload(); renderARRDataReview(); renderARRCalendar(); + renderReportCalendar(); renderJobs(state.jobs); renderDailyHistory(); if (state.dailyDeleteRequest) { @@ -3724,6 +3941,7 @@ function bindEvents() { bindHistoryMonthControls(); bindARRCalendar(); + bindReportCalendar(); $$("[data-tab]").forEach((button) => button.addEventListener("click", () => activateTab(button.dataset.tab))); $("#xml-file").addEventListener("change", (event) => { if (state.uploadInFlight) return; @@ -4016,6 +4234,8 @@ async function boot() { $("#arr-download-date").value = defaultARRDate(); $("#company-report-month").value = localMonth(); + state.reportCalendarMonth = localMonth(); + renderReportCalendar(); populateMonths([]); bindEvents(); activateTab(location.hash.slice(1) || "daily", false); @@ -4036,6 +4256,8 @@ loadCompanyDraft(), loadCompanyReportHistory(location.hash.slice(1) === "usage"), ]); + state.reportCalendarInitialized = true; + await loadReportCalendar(); } boot(); diff --git a/arr_web/static/i18n.js b/arr_web/static/i18n.js index c698bf5..372b0ca 100644 --- a/arr_web/static/i18n.js +++ b/arr_web/static/i18n.js @@ -254,6 +254,24 @@ "metric.duration": ["处理耗时", "Processing time", "เวลาประมวลผล"], "metric.duration_from_submit": ["从提交至完成", "From submission to completion", "ตั้งแต่ส่งจนเสร็จ"], "metric.rooms": ["保留记录的售出房数", "Sold rooms in retained records", "จำนวนห้องที่ขายในรายการที่เก็บไว้"], + "report_calendar.title": ["报表", "Reports", "รายงาน"], + "report_calendar.widget_aria": ["报表日历", "Report calendar", "ปฏิทินรายงาน"], + "report_calendar.generated": ["已生成", "Generated", "สร้างแล้ว"], + "report_calendar.not_generated": ["未生成", "Not generated", "ยังไม่สร้าง"], + "report_calendar.loading_day": ["读取中", "Loading", "กำลังอ่าน"], + "report_calendar.unknown": ["未确认", "Not confirmed", "ยังไม่ยืนยัน"], + "report_calendar.day_generated": ["已生成", "Ready", "มีแล้ว"], + "report_calendar.day_not_generated": ["未生成", "None", "ยังไม่มี"], + "report_calendar.day_loading_day": ["读取中", "Loading", "กำลังอ่าน"], + "report_calendar.day_unknown": ["未确认", "Unknown", "ไม่ทราบ"], + "report_calendar.loading": ["正在读取本月报表状态…", "Loading report status for this month…", "กำลังอ่านสถานะรายงานของเดือนนี้…"], + "report_calendar.failed": ["状态读取失败,暂不能确认哪些日期已生成。", "Status could not be read. Generated dates are not confirmed.", "อ่านสถานะไม่สำเร็จ ยังยืนยันวันที่สร้างรายงานแล้วไม่ได้"], + "report_calendar.coverage": ["本月已生成 {count} / {total} 天", "Reports generated for {count} of {total} days", "สร้างรายงานแล้ว {count} / {total} วัน"], + "report_calendar.help": ["选择日期查看日报;不会自动下载或处理。", "Select a date to view its report. This does not fetch or process data.", "เลือกวันที่เพื่อดูรายงาน จะไม่ดึงหรือประมวลผลข้อมูลอัตโนมัติ"], + "report_calendar.locked": ["任务进行中,结束后可切换查看日期。", "A task is in progress. Change the viewed date after it finishes.", "งานกำลังดำเนินการ เปลี่ยนวันที่ดูได้เมื่อเสร็จแล้ว"], + "report_calendar.day_group": ["选择日期查看日报", "Choose a report date", "เลือกวันที่รายงาน"], + "report_calendar.legend": ["日报生成状态", "Daily report generation status", "สถานะการสร้างรายงานรายวัน"], + "report_calendar.retry": ["重新读取", "Read again", "อ่านอีกครั้ง"], "daily.current": ["当前有效日报", "Current daily report", "รายงานรายวันที่ใช้อยู่"], "daily.no_current": ["无当前有效日报", "No current daily report", "ไม่มีรายงานรายวันที่ใช้อยู่"], "daily.history_only": ["仅保留历史版本", "Historical versions only", "มีเฉพาะรุ่นในประวัติ"], diff --git a/arr_web/static/index.html b/arr_web/static/index.html index 23ef146..033c54c 100644 --- a/arr_web/static/index.html +++ b/arr_web/static/index.html @@ -121,22 +121,38 @@ -
+
ARRIVAL DATE — 当前查看日期
-
- 处理耗时 - — - 从提交至完成 -
NO. OF ROOM — 保留记录的售出房数
+ +
+
+

报表

+ +
+
+ + + +
+ +
+
+ 已生成 + 未生成 +
+

正在读取本月报表状态…

+

选择日期查看日报;不会自动下载或处理。

+ +
", start) + calendar = self.html[start:end] + self.assertIn('aria-label="报表日历" aria-describedby="report-calendar-help"', calendar) + self.assertIn('

报表

', calendar) + for control in ("current", "previous", "next", "retry"): + self.assertRegex(calendar, rf']*id="report-calendar-{control}"[^>]*type="button"') + self.assertIn('id="report-calendar-days" role="group" aria-label="选择日期查看日报"', calendar) + self.assertIn('id="report-calendar-legend-generated">已生成', calendar) + self.assertIn('id="report-calendar-legend-empty">未生成', calendar) + self.assertIn('id="report-calendar-status" role="status" aria-live="polite"', calendar) + self.assertIn('id="report-calendar-retry" type="button" hidden', calendar) + self.assertIn("repeat(7, minmax(0, 1fr))", self.styles) + self.assertIn(".report-calendar button:focus-visible", self.styles) + self.assertIn(".report-calendar button { min-height: 44px;", self.styles) + self.assertIn("@media (min-width: 701px) and (max-width: 1399px)", self.styles) + + def test_duration_is_removed_only_from_the_top_overview(self) -> None: + app = (STATIC_ROOT / "app.js").read_text(encoding="utf-8") + self.assertNotIn('id="metric-duration"', self.html) + self.assertNotIn('$("#metric-duration")', app) + self.assertIn("处理耗时", self.html[self.html.index('id="daily-history-title"'):]) + self.assertIn('current ? duration(current) : "—"', app) + def test_download_card_exposes_accessible_retrieval_progress_with_reduced_motion(self) -> None: start = self.html.index('id="arr-acquisition-progress"') end = self.html.index('
', start)