Add daily report calendar to the header

This commit is contained in:
Wyndham ARR committed 2026-10-09 13:20:13 +08:00
1 parent 2a54838d59
commit 4356160cc9
11 files changed
+735 -28

No files matched your search

+227 -5
View File
@@ -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(`<span aria-hidden="true">${escapeHtml(weekday.format(monday))}</span>`);
}
const rows = new Map(state.reportCalendarRows.map((row) => [row.business_date, row]));
const days = Array.from({ length: leading }, () => '<span class="report-calendar-spacer" aria-hidden="true"></span>');
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(`<button type="button" class="report-calendar-day ${exists ? "is-generated" : ready ? "is-ungenerated" : "is-unknown"}${value === today ? " is-today" : ""}" data-report-calendar-day="${value}" aria-label="${escapeHtml(`${dateLabel.format(new Date(`${value}T12:00:00Z`))} · ${statusLabel}`)}" aria-pressed="${selectedDay}" ${value === today ? 'aria-current="date"' : ""} tabindex="${value === state.reportCalendarFocusDate ? 0 : -1}" ${locked ? "disabled" : ""}><span class="report-calendar-day-number">${number}</span><span class="report-calendar-day-state"><span aria-hidden="true">${symbol}</span><span>${escapeHtml(shortLabel)}</span></span></button>`);
}
$("#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 = `<tr><td class="empty-cell" colspan="7">${escapeHtml(I18N?.t("daily.unavailable") || "日报记录暂时无法读取")}</td></tr>`;
@@ -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();
+18
View File
@@ -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", "มีเฉพาะรุ่นในประวัติ"],
+22 -6
View File
@@ -121,22 +121,38 @@
</div>
</section>
<article class="metric-card">
<article class="metric-card arrival-metric">
<span>ARRIVAL DATE</span>
<strong id="metric-arrival">—</strong>
<small id="metric-arrival-note">当前查看日期</small>
<small id="metric-current-version" hidden>当前有效日报仍保留</small>
</article>
<article class="metric-card">
<span>处理耗时</span>
<strong id="metric-duration">—</strong>
<small id="metric-duration-note">从提交至完成</small>
</article>
<article class="metric-card accent-card">
<span>NO. OF ROOM</span>
<strong id="metric-rooms">—</strong>
<small id="metric-rooms-note">保留记录的售出房数</small>
</article>
<section class="panel report-calendar" id="report-calendar" aria-label="报表日历" aria-describedby="report-calendar-help">
<header class="report-calendar-heading">
<h2 id="report-calendar-title">报表</h2>
<button id="report-calendar-current" type="button">本月</button>
</header>
<div class="report-calendar-navigation">
<button id="report-calendar-previous" type="button" aria-label="上一个月">‹</button>
<strong id="report-calendar-month"></strong>
<button id="report-calendar-next" type="button" aria-label="下一个月">›</button>
</div>
<div class="report-calendar-weekdays" id="report-calendar-weekdays" aria-hidden="true"></div>
<div class="report-calendar-days" id="report-calendar-days" role="group" aria-label="选择日期查看日报"></div>
<div class="report-calendar-legend" aria-label="日报生成状态">
<span><b aria-hidden="true">✓</b><span id="report-calendar-legend-generated">已生成</span></span>
<span><b aria-hidden="true">—</b><span id="report-calendar-legend-empty">未生成</span></span>
</div>
<p class="report-calendar-status" id="report-calendar-status" role="status" aria-live="polite">正在读取本月报表状态…</p>
<p class="report-calendar-help" id="report-calendar-help">选择日期查看日报;不会自动下载或处理。</p>
<button class="report-calendar-retry" id="report-calendar-retry" type="button" hidden>重新读取</button>
</section>
</section>
<section class="panel arr-download-pending" id="arr-download-pending" aria-labelledby="arr-download-pending-title" hidden>
+36 -5
View File
@@ -158,7 +158,7 @@ button { color: inherit; }
.panel-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 22px 24px 0; }
.panel-heading h2 { margin: 4px 0 0; font-size: 18px; line-height: 1.3; letter-spacing: -.025em; }
.daily-overview { display: grid; grid-template-columns: minmax(240px, 1.25fr) minmax(280px, 1.45fr) repeat(3, minmax(0, .8fr)); gap: 16px; align-items: stretch; margin-bottom: 20px; }
.daily-overview { display: grid; grid-template-columns: minmax(220px, 1.1fr) minmax(230px, 1.15fr) minmax(150px, .75fr) minmax(110px, .55fr) minmax(360px, 1.8fr); gap: 16px; align-items: stretch; margin-bottom: 20px; }
.daily-overview > * { min-width: 0; }
.daily-overview > .upload-panel { display: flex; flex-direction: column; }
.daily-overview .upload-footer { margin-top: auto; }
@@ -272,6 +272,36 @@ button { color: inherit; }
.upload-panel { min-height: 0; overflow: hidden; border-color: #dbe3ef; box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 18px 44px rgba(31, 51, 82, .07); }
.daily-overview > .metric-card { align-self: stretch; display: flex; flex-direction: column; justify-content: center; height: auto; min-height: 0; padding: 24px 22px; }
.daily-overview > .metric-card::after { right: -24px; bottom: -30px; width: 82px; height: 82px; border-width: 12px; }
.daily-overview > .arrival-metric { padding-left: 16px; padding-right: 16px; }
.daily-overview > .arrival-metric strong { font-size: clamp(20px, 1.5vw, 24px); }
.report-calendar { padding: 14px 12px; overflow: hidden; }
.report-calendar-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.report-calendar-heading h2 { margin: 0; font-size: 17px; letter-spacing: -.02em; }
.report-calendar button { min-height: 44px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink); background: #fff; cursor: pointer; }
.report-calendar-heading button { padding: 0 10px; color: var(--blue); font-size: 12px; }
.report-calendar-navigation { display: grid; grid-template-columns: 44px minmax(0, 1fr) 44px; align-items: center; gap: 8px; margin: 8px 0; }
.report-calendar-navigation strong { text-align: center; font-size: 13px; font-weight: 700; }
.report-calendar-navigation button { padding: 0; font-size: 22px; }
.report-calendar-weekdays, .report-calendar-days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
.report-calendar-weekdays { margin-bottom: 5px; color: var(--muted); text-align: center; font-size: 10px; }
.report-calendar-days .report-calendar-day { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; min-width: 0; padding: 3px 0; border-color: transparent; background: var(--surface-soft); font-variant-numeric: tabular-nums; }
.report-calendar-day-number { font-size: 12px; font-weight: 650; line-height: 1.3; }
.report-calendar-day-state { display: flex; align-items: center; gap: 1px; font-size: 9px; line-height: 1.4; white-space: nowrap; }
.report-calendar-days .is-generated { color: var(--blue-dark); background: var(--blue-soft); }
.report-calendar-days .is-ungenerated { color: var(--muted); }
.report-calendar-days .is-unknown { color: var(--muted); border-style: dashed; border-color: var(--line); }
.report-calendar-days .is-today { border-color: #a3b9e8; }
.report-calendar-days [aria-pressed="true"] { border-color: var(--blue); box-shadow: inset 0 0 0 1px var(--blue); color: var(--blue-dark); }
.report-calendar button:hover:not(:disabled) { border-color: var(--blue); background: var(--blue-soft); }
.report-calendar button:focus-visible { position: relative; z-index: 1; outline: 3px solid rgba(37, 99, 235, .45); outline-offset: 1px; }
.report-calendar button:disabled { cursor: not-allowed; opacity: .55; }
.report-calendar-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-top: 11px; color: var(--muted); font-size: 10px; }
.report-calendar-legend > span { display: inline-flex; align-items: center; gap: 4px; }
.report-calendar-legend > span:first-child b { color: var(--blue-dark); }
.report-calendar-status, .report-calendar-help { margin: 6px 0 0; color: var(--muted); font-size: 10px; line-height: 1.6; }
.report-calendar-status { color: var(--blue-dark); }
.report-calendar-help { color: var(--subtle); }
.report-calendar .report-calendar-retry { margin-top: 8px; padding: 0 10px; color: var(--blue); font-size: 12px; }
.upload-heading { align-items: center; padding: 24px 28px 0; }
.upload-heading h2 { margin-top: 0; font-size: 20px; letter-spacing: -.03em; }
.process-panel { min-height: 640px; overflow: hidden; }
@@ -767,10 +797,11 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
:focus-visible { outline: 3px solid rgba(37, 99, 235, .35); outline-offset: 3px; }
@media (min-width: 701px) and (max-width: 1199px) {
.daily-overview { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.daily-overview > .arr-download-panel, .daily-overview > .upload-panel { grid-column: span 3; }
.daily-overview > .metric-card { grid-column: span 2; }
@media (min-width: 701px) and (max-width: 1399px) {
.daily-overview { grid-template-columns: repeat(12, minmax(0, 1fr)); }
.daily-overview > .arr-download-panel, .daily-overview > .upload-panel { grid-column: span 6; }
.daily-overview > .metric-card { grid-column: span 3; }
.daily-overview > .report-calendar { grid-column: span 6; }
}
@media (max-width: 960px) {