Simplify daily review UI and compact header cards

This commit is contained in:
Wyndham ARR committed 2026-10-09 14:30:41 +08:00
1 parent 6d1a98743f
commit 64f4a0d3de
13 files changed
+185 -133

No files matched your search

@@ -0,0 +1,63 @@
# Task: 精简日报界面与顶部卡片布局
## Identity
- Task ID: 20261009-ui-simplify-b724
- Mode: Feature
- Branch: codex/20261009-ui-simplify-b724-ui-simplify
- Worktree: /Users/chillishark/.codex/worktrees/arr-ui-simplify
- Base commit: 6d1a98743f61fd6216a38a6641bbb1df56619d56
- Owner: codex
- Status: Ready for Integration
## Scope
- User-requested UI cleanup: remove download retention hint, rename acquisition status to status, remove review explanation/count/exclusion summaries and per-issue explanatory metadata, suppress zero-history operation, retain only the blue field-review action. Compress the desktop header card row to about two thirds of its current width and align upload/calendar headings and content.
## Intent And Constraints
- Preserve existing field/price inputs, save/confirm/generate actions, nonempty history access, task identity/queues, XML/OHIP/Finance/monthly business rules and all real data. Keep current blue/white style, generated-green popup calendar, responsive usable controls and accessible names. No Oracle query, report mutation or actual review changes. Local activation must preserve service configuration/data and avoid active jobs. Existing publication authorization to arr0918/main persists.
## Outcome
- Removed the pending-retention hint, review instructions, confirmation/exclusion summary and field reason/rule/source-row/company/rate metadata. Field review retains reservation identity, field name, input, confirmation status and actions. Price review retains the company/rate/Opera-price key, manual price and actions; candidate/affected-record/room-night explanation columns and top metrics are removed. Removed DOM references and associated presentational helpers, while required/optional enforcement and error feedback remain unchanged.
- Renamed acquisition label to “状态” in all three locales; retained progress/counts. The download card has only its blue main field-review action; price-review access remains available. Daily rows render a history button only when history_count>0, preserving nonempty history, download and delete operations.
- Desktop width>=1440 uses two thirds of the original header width, with compact flexible columns. Upload content is vertically centered and title/control starts align with the report calendar. Smaller widths keep full-width responsive grids or a single column. Existing green generated dates and popup/month navigation remain.
- Local8875 now runs this worktree, with a cold backup at /Users/chillishark/Library/Application Support/ARR2.0/ui-simplify-backup-20261009-062152. Activation changed only private code paths; processor4.4.0, schema008-022, credentials/hotel/routing and all business data remain. No Oracle request, acquisition, upload, report mutation or real review submission performed. Code publication is authorized to arr0918/main, with verified commit recorded privately after push.
## Verification
- 139 JavaScript UI tests passed; 15 relevant daily visual/price-review static checks passed, including root rerun after final CSS. JavaScript syntax and git diff --check passed. Independent review reran83 overlapping relevant cases, checked removed IDs have no dangling queries/bindings, and exercised real i18n.setLocale plus field/price rendering in zh-CN/en/th without API calls.
- Synthetic browser proof: explanations/metadata absent; one blue field action opens review; price0 enables save and is retained when unsaved-switch guard rejects changing dates. Five-column price rendering, neutral empty history and nonzero history access remain. English/Thai review/status render without errors. No synthetic mutation was submitted; private preview accepts only GET.
- Browser layout proof at actual375/767/1024/1440 widths: no page horizontal overflow or arrival-date clipping. Desktop1600 row933.33px versus shell1400px gives ratio0.666664; upload/report title and control start differ by less than1px. Popup is fully inside the viewport. Small screens naturally stack cards rather than force the compressed desktop width.
- Actual8875 readonly proof: September13 retains one room issue for reservation300622945 with only field/input/status/save operation, no explanation or exclusion counts; the main blue field button still opens the same review. “状态” progress shows completed135/135. September17 has no history button, September16 retains “历史记录(1)”. Screenshot evidence saved privately as live-compact-header.png and live-compact-review.png.
- Before/after comparison, repeated after browser actions, preserved all27 table fingerprints, 3 current report pointers, 4896 source files, monthly workbooks/API records, queue statuses, pending datesSeptember15/13 and retirement audit. No business/API/Python/schema source changed.
## Follow-ups
- No requested product work remains. Retain this worktree because the local service runs from it. Canonical reconciliation remains a separate serialized integration task; old clean worktrees were left untouched.
## Promotion Candidates
- Target: portal architecture/current-state. Proposal: record compact two-thirds desktop header, upload/calendar alignment and simplified field/price review presentation; required validation/audit facts remain stored but explanatory prose is omitted from operator forms. Evidence: explicit seven-point user instruction, 139 JS/15 UI checks, independent review and actual readonly browser/data-preservation proof. Future impact: avoid reintroducing duplicate field-review or zero-history actions and implementation/audit explanations into the product flow. No business-rule conflict; this product revision was explicitly authorized. Feature mode does not write canonical memory.
## Concurrent Task Gate
- Passed: bundled check_project_docs succeeded (canonical migration pending integration); start succeeded with task20261009-ui-simplify-b724, feature/codex, managed worktree/Users/chillishark/.codex/worktrees/arr-ui-simplify, branchcodex/20261009-ui-simplify-b724-ui-simplify, base6d1a98743f61fd6216a38a6641bbb1df56619d56. Status JSON confirms matching identity. Calendar/lifecycle/production-review peers are Ready for Integration, Scope/Constraints/Promotion Candidates read-only. Original primary remains untouched; only this new task changes its checkout.
## Project Context Loaded
- Read: read-before-planning/planning-gate, own task, memory-index, positioning, current-state active snapshot, decision-index, system-overview, domain rules, success criteria, ADR006/007, relevant evidence/commitments/stale indexes, and peer scopes/constraints/promotions.
- Project goal: same deterministic XML/OHIP processing into daily Finance facts and automatic monthly results.
- Current focus: user-facing clarity and compact aligned daily header; remove surplus prose and duplicate actions.
- Constraints/decisions: display-only changes; non-negative reviewed prices including0 and required field enforcement remain unchanged. Current effective daily date/pointer determines calendar status. No actual submissions/deletions/Oracle queries.
- Evidence/conflicts: canonical September docs are stale versus direct October instructions and completed source-task evidence (productionhotel57106, schema022, processor4.4.0, multiple agents permitted). Direct user instruction supersedes the old visible explanations and duplicated links; no business-rule conflict. Current8875 runs calendar worktree; this isolated checkout needs safe activation.
- Modules: static app/index/i18n/styles and relevant existing UI tests; local static serving composition only if needed.
- Unknowns: interpretation of two-thirds width is implemented literally for desktop, with responsive fallback to avoid clipping. Align upload/calendar title/control positions using common spacing. No blocking user decision.
- Gate: Passed. Design guidance adapted to the established blue/white interface; no glass effects or new typography/dependencies.
## Plan
1. Remove requested surplus content and duplicate/zero-history actions while preserving operational state and input controls.
2. Implement compact header width and upload/calendar alignment with usable responsive breakpoints.
3. Verify rendering/interaction using synthetic and read-only live evidence, preserve all data, activate locally and publish the scoped change.
+4 -2
View File
@@ -14,6 +14,8 @@
任务结果待确认时,先检查或继续按钮上标明日期的原任务,避免重复提交。 任务结果待确认时,先检查或继续按钮上标明日期的原任务,避免重复提交。
某一天等待人工完善不阻塞其他日期:另选日期后可点击“下载并处理”,此前字段或价格任务仍保存在“需处理日期”中。 某一天等待人工完善不阻塞其他日期:另选日期后可点击“下载并处理”,此前字段或价格任务仍保存在“需处理日期”中。
点击需处理日期可返回该日的“人工核对”,刷新后列表仍保留;同一天的未完成任务沿用原编号。 点击需处理日期可返回该日的“人工核对”,刷新后列表仍保留;同一天的未完成任务沿用原编号。
人工核对页面只展示预订编号、字段名、输入和确认操作;价格核对保留公司、费率代码、Opera价、人工价格和保存操作。页面不展示来源/规则解释或取消、PM排除数量,相关原始数据和审计仍保留。字段完善在下载卡片仅保留蓝色主按钮,必填/留空限制、保存校验和生成门槛不变。
切换字段任务保留各自草稿,未保存的价格会阻止切换核对任务;刷新或切换语言前仍应保存输入,已保存决定长期保留。 切换字段任务保留各自草稿,未保存的价格会阻止切换核对任务;刷新或切换语言前仍应保存输入,已保存决定长期保留。
完整采集中的缺字段、多个候选或不满足原规则的字段先进入“待完善数据”(`needs_data_review`)。 完整采集中的缺字段、多个候选或不满足原规则的字段先进入“待完善数据”(`needs_data_review`)。
全部阻塞字段通过并确认后,原任务自动继续处理;随后只有缺少pureprice时进入原有价格复核(`needs_review`)。 全部阻塞字段通过并确认后,原任务自动继续处理;随后只有缺少pureprice时进入原有价格复核(`needs_review`)。
@@ -35,11 +37,11 @@
点击列表中的日期,可查看该日概览;上方日期与房数始终对应当前查看日期。待处理新版本会单独提示,尚无有效日报时显示空值,不借用其他日期的成功结果。 点击列表中的日期,可查看该日概览;上方日期与房数始终对应当前查看日期。待处理新版本会单独提示,尚无有效日报时显示空值,不借用其他日期的成功结果。
顶部概览提供“自动下载 ARR、上传 ARR.XML、ARRIVAL DATE、NO. OF ROOM、本月报表情况”五个区域,顶部不再展示处理耗时。“本月报表情况”默认收起,显示月份和已生成天数;点击带日历图标的月份按钮展开月历,支持切换月份。每一天按是否存在当前有效日报标为“已生成”或“未生成”,已有报表的日期显示为绿色。历史版本、已删除版本和仅待核对的任务不算已生成;已有有效日报、另有待核对新版本时,仍标为已生成。读取中或读取失败会单独显示,不把未知状态当作未生成。 顶部概览提供“自动下载 ARR、上传 ARR.XML、ARRIVAL DATE、NO. OF ROOM、本月报表情况”五个区域,顶部不再展示处理耗时。宽屏卡片行宽度约为原来的2/3,上传与日历标题、内容对齐,小屏按可用宽度自动排列。“本月报表情况”默认收起,显示月份和已生成天数;点击带日历图标的月份按钮展开月历,支持切换月份。每一天按是否存在当前有效日报标为“已生成”或“未生成”,已有报表的日期显示为绿色。历史版本、已删除版本和仅待核对的任务不算已生成;已有有效日报、另有待核对新版本时,仍标为已生成。读取中或读取失败会单独显示,不把未知状态当作未生成。
点击日历日期只切换查看日期和日报列表月份,不会获取Oracle数据、生成报表或提交人工核对值。需要下载新数据时,仍须在自动下载入口主动提交;未保存的人工价格继续受切换保护。日历月份与列表分页独立,显示整月状态;生成或删除后的刷新会更新标记。 点击日历日期只切换查看日期和日报列表月份,不会获取Oracle数据、生成报表或提交人工核对值。需要下载新数据时,仍须在自动下载入口主动提交;未保存的人工价格继续受切换保护。日历月份与列表分页独立,显示整月状态;生成或删除后的刷新会更新标记。
“历史记录”保留同一天的旧版本和未完成版本。删除前页面会说明影响: 存在历史版本时显示“历史记录”入口,保留同一天的旧版本和未完成版本;数量为0时不显示该按钮。删除前页面会说明影响:
- 删除当前有效日报:该日从月报中移除,系统自动生成剩余日期的月报;整月已无有效日报时,撤下该月报。旧日报不会自动恢复。 - 删除当前有效日报:该日从月报中移除,系统自动生成剩余日期的月报;整月已无有效日报时,撤下该月报。旧日报不会自动恢复。
- 删除历史版本或待价格核对版本:保留当前有效日报,月报不变。运行中的版本须等任务停止后再删除。 - 删除历史版本或待价格核对版本:保留当前有效日报,月报不变。运行中的版本须等任务停止后再删除。
+2
View File
@@ -4,6 +4,8 @@
顶部“本月报表情况”提供可展开的日历按钮,有当前有效日报的日期显示为绿色,可切换月份和选择日期查看;顶部处理耗时已移除。日历只读取ARR已保存状态,不查询Oracle或自动创建报表。 顶部“本月报表情况”提供可展开的日历按钮,有当前有效日报的日期显示为绿色,可切换月份和选择日期查看;顶部处理耗时已移除。日历只读取ARR已保存状态,不查询Oracle或自动创建报表。
日报界面已精简:下载卡片显示“状态”,字段完善保留一个主按钮;人工核对只展示定位和填写所需信息及操作,不显示来源说明或排除统计。宽屏顶部卡片行约占原宽度的2/3,上传与日历内容对齐,小屏保持自适应。日报仅在存在历史版本时提供历史记录入口。
2026-09-18:[直接数据入口](DIRECT_DATA_ENTRY.md)已接通日期按钮、日报、复核和月报;独立本机模拟8875,未查询实际Oracle。 2026-09-18:[直接数据入口](DIRECT_DATA_ENTRY.md)已接通日期按钮、日报、复核和月报;独立本机模拟8875,未查询实际Oracle。
2026-09-17:[本机接口模拟全流程及按钮联调](LOCAL_API_SIMULATION.md)已在独立8874验证;[原文件重放](LOCAL_XML_REPLAY.md)8873保留。两个入口均明确未连接Oracle,不改变默认运行配置。 2026-09-17:[本机接口模拟全流程及按钮联调](LOCAL_API_SIMULATION.md)已在独立8874验证;[原文件重放](LOCAL_XML_REPLAY.md)8873保留。两个入口均明确未连接Oracle,不改变默认运行配置。
+12 -68
View File
@@ -522,11 +522,12 @@
dateInput.disabled = locked; dateInput.disabled = locked;
$("#arr-date-toggle").disabled = locked; $("#arr-date-toggle").disabled = locked;
$("#arr-date-toggle").setAttribute("aria-label", I18N.t("arr_download.choose_date")); $("#arr-date-toggle").setAttribute("aria-label", I18N.t("arr_download.choose_date"));
$("#arr-download-help").textContent = I18N.t(locked ? "arr_download.submitting" const helpKey = locked ? "arr_download.submitting"
: arrDownloadActive() ? "arr_download.date_next" : arrDownloadActive() ? "arr_download.date_next"
: arrDownloadNeedsDataReview() || task?.status === "needs_review" ? "arr_download.date_independent" : arrDownloadNeedsDataReview() || task?.status === "needs_review" ? ""
: reconciling ? "arr_download.date_resume" : "arr_download.help", : reconciling ? "arr_download.date_resume" : "arr_download.help";
{ date: state.arrDownloadIntent?.report_date }); $("#arr-download-help").textContent = helpKey ? I18N.t(helpKey, { date: state.arrDownloadIntent?.report_date }) : "";
$("#arr-download-help").hidden = !helpKey;
if (locked) closeARRCalendar(); if (locked) closeARRCalendar();
button.disabled = !state.arrDownloadLoaded || !state.arrDownloadReady || locked || arrDownloadActive() || state.arrDataReviewFinalizing button.disabled = !state.arrDownloadLoaded || !state.arrDownloadReady || locked || arrDownloadActive() || state.arrDataReviewFinalizing
|| (!reconciling && !arrDownloadCanResume() && !validARRDate(dateInput.value)) || (!reconciling && !arrDownloadCanResume() && !validARRDate(dateInput.value))
@@ -555,8 +556,6 @@
const taskDate = viewingAnotherDate ? dateInput.value : task?.report_date || state.arrDownloadIntent?.report_date; const taskDate = viewingAnotherDate ? dateInput.value : task?.report_date || state.arrDownloadIntent?.report_date;
$("#arr-download-status").textContent = taskDate ? `${taskDate} · ${text}` : text; $("#arr-download-status").textContent = taskDate ? `${taskDate} · ${text}` : text;
$("#arr-download-review").hidden = viewingAnotherDate || task?.status !== "needs_review" || !task.job_id; $("#arr-download-review").hidden = viewingAnotherDate || task?.status !== "needs_review" || !task.job_id;
$("#arr-download-data-review").hidden = !selectedARRDateNeedsDataReview();
$("#arr-download-data-review").disabled = locked;
renderARRAcquisitionProgress(task); renderARRAcquisitionProgress(task);
renderARRPendingReviews(); renderARRPendingReviews();
syncDailyOverviewDate(dateInput.value); syncDailyOverviewDate(dateInput.value);
@@ -815,25 +814,6 @@
return Array.isArray(item.value) ? item.value.join("\n") : String(item.value ?? ""); return Array.isArray(item.value) ? item.value.join("\n") : String(item.value ?? "");
} }
function arrDataReviewReasonKey(item) {
if (item.confirmed) return "confirmed_value";
const sourceState = String(item.source_state || "").toLowerCase();
const reason = String(item.reason_code || "").toUpperCase();
// Older review snapshots do not expose source_state. Recognize only the
// collector's explicit failure codes; a missing optional field is not a
// failed request and does not make the field required.
const failedReasons = ["HTTP_FAILURE", "HTTP_PERMISSION_DENIED", "TRANSPORT_RETRY_EXHAUSTED",
"REQUEST_LIMIT_EXCEEDED", "RESPONSE_TOO_LARGE", "OPERATION_MISMATCH", "HOTEL_MISMATCH",
"INVALID_DATA_ENVELOPE", "MISSING_ORACLE_REQUEST_ID", "SECRET_IN_RESPONSE", "INVALID_JSON",
"UPSTREAM_WARNING_OR_ERROR"];
if (sourceState === "failed" || failedReasons.includes(reason)) return "fetch_failed";
if (sourceState === "ambiguous" || /(?:^|_)(?:CONFLICT|AMBIGUOUS|MISMATCH|DISAGREEMENT|CHANGED)(?:_|$)/.test(reason)) return "conflict_value";
if (/(?:^|_)(?:INVALID|MALFORMED)(?:_|$)/.test(reason) || ["XML_NEGATIVE_NIGHTS", "DATA_NEGATIVE_NIGHTS"].includes(reason)) return "invalid_value";
if (item.field === "DISP_ROOM_NO" && !item.can_be_empty
&& ["CALENDAR_MISSING_ROOMS", "DATA_ROOM_MISSING", "XML_ROOM_MISSING"].includes(reason)) return "room_missing_in_scope";
return item.can_be_empty ? "source_unconfirmed" : "required_value_missing";
}
function arrDataReviewCanFinalize(review = state.arrDataReview) { function arrDataReviewCanFinalize(review = state.arrDataReview) {
return Boolean(arrDownloadNeedsDataReview() && state.arrDownloadTask.request_id === state.arrDataReviewRequestId return Boolean(arrDownloadNeedsDataReview() && state.arrDownloadTask.request_id === state.arrDataReviewRequestId
&& !state.arrDownloadBusy && review && review.status === "editing" && review.can_finalize && Number(review.pending_count) === 0 && !state.arrDownloadBusy && review && review.status === "editing" && review.can_finalize && Number(review.pending_count) === 0
@@ -879,16 +859,6 @@
const editable = review?.status === "editing" && !busy && !state.arrDataReviewDisconnected && !state.arrDataReviewFinalizing; const editable = review?.status === "editing" && !busy && !state.arrDataReviewDisconnected && !state.arrDataReviewFinalizing;
panel.setAttribute("aria-busy", String(busy)); panel.setAttribute("aria-busy", String(busy));
$("#arr-data-review-title").textContent = I18N.t("data_review.title"); $("#arr-data-review-title").textContent = I18N.t("data_review.title");
$("#arr-data-review-copy").textContent = I18N.t(state.arrDataReviewFinalizing || review?.status === "finalized" ? "data_review.finalizing_copy" : "data_review.copy");
$("#arr-data-review-progress").textContent = review ? I18N.t("data_review.progress", {
date: review.report_date, completed: Number(review.total_count) - Number(review.pending_count), total: review.total_count,
}) : I18N.t("data_review.loading");
if (Number.isInteger(review?.excluded_cancelled_count) && review.excluded_cancelled_count > 0) {
$("#arr-data-review-progress").textContent += ` · ${I18N.t("data_review.excluded_cancelled", { count: review.excluded_cancelled_count })}`;
}
if (Number.isInteger(review?.excluded_pm_count) && review.excluded_pm_count > 0) {
$("#arr-data-review-progress").textContent += ` · ${I18N.t("data_review.excluded_pm", { count: review.excluded_pm_count })}`;
}
$("#arr-data-review-refresh").textContent = I18N.t("data_review.refresh"); $("#arr-data-review-refresh").textContent = I18N.t("data_review.refresh");
$("#arr-data-review-refresh").disabled = busy; $("#arr-data-review-refresh").disabled = busy;
const finalize = $("#arr-data-review-finalize"); const finalize = $("#arr-data-review-finalize");
@@ -901,8 +871,6 @@
const labelKey = `data_review.field_${field.toLowerCase()}`; const labelKey = `data_review.field_${field.toLowerCase()}`;
const translatedLabel = I18N.t(labelKey); const translatedLabel = I18N.t(labelKey);
const label = translatedLabel === labelKey ? (item.field_label || field) : translatedLabel; const label = translatedLabel === labelKey ? (item.field_label || field) : translatedLabel;
const reasonKey = arrDataReviewReasonKey(item);
const ruleKey = item.can_be_empty ? "optional_field_rule" : "required_field_rule";
const inputId = `arr-data-review-value-${index}`; const inputId = `arr-data-review-value-${index}`;
const value = Object.prototype.hasOwnProperty.call(state.arrDataReviewDrafts, item.item_id) const value = Object.prototype.hasOwnProperty.call(state.arrDataReviewDrafts, item.item_id)
? state.arrDataReviewDrafts[item.item_id] : arrDataReviewInputValue(item); ? state.arrDataReviewDrafts[item.item_id] : arrDataReviewInputValue(item);
@@ -912,8 +880,8 @@
? `<textarea id="${inputId}" data-arr-data-review-value rows="2" placeholder="${escapeHtml(placeholder)}" ${editable ? "" : "disabled"}>${escapeHtml(value)}</textarea>` ? `<textarea id="${inputId}" data-arr-data-review-value rows="2" placeholder="${escapeHtml(placeholder)}" ${editable ? "" : "disabled"}>${escapeHtml(value)}</textarea>`
: `<input id="${inputId}" data-arr-data-review-value type="text" inputmode="${inputMode}" autocomplete="off" value="${escapeHtml(value)}" placeholder="${escapeHtml(placeholder)}" ${editable ? "" : "disabled"} />`; : `<input id="${inputId}" data-arr-data-review-value type="text" inputmode="${inputMode}" autocomplete="off" value="${escapeHtml(value)}" placeholder="${escapeHtml(placeholder)}" ${editable ? "" : "disabled"} />`;
return `<tr data-arr-data-review-item-id="${escapeHtml(item.item_id)}" class="${item.confirmed ? "is-complete" : ""}"> return `<tr data-arr-data-review-item-id="${escapeHtml(item.item_id)}" class="${item.confirmed ? "is-complete" : ""}">
<td data-label="${escapeHtml(I18N.t("data_review.reservation"))}"><strong>${escapeHtml(item.confirmation_no || "—")}</strong><small>${escapeHtml(I18N.t("data_review.source_row", { sequence: item.source_sequence }))} · ${escapeHtml(I18N.t("data_review.room", { room: item.room_no || "—" }))}</small><small>${escapeHtml(item.company_name || "—")} · ${escapeHtml(item.rate_code || "—")}</small></td> <td data-label="${escapeHtml(I18N.t("data_review.reservation"))}"><strong>${escapeHtml(item.confirmation_no || "—")}</strong></td>
<td data-label="${escapeHtml(I18N.t("data_review.field"))}"><label for="${inputId}">${escapeHtml(label)}</label><small>${escapeHtml(I18N.t(`data_review.${ruleKey}`))}</small><small>${escapeHtml(I18N.t(`data_review.${reasonKey}`))}</small></td> <td data-label="${escapeHtml(I18N.t("data_review.field"))}"><label for="${inputId}">${escapeHtml(label)}</label></td>
<td data-label="${escapeHtml(I18N.t("data_review.value"))}">${input}</td> <td data-label="${escapeHtml(I18N.t("data_review.value"))}">${input}</td>
<td class="arr-data-review-row-actions" data-label="${escapeHtml(I18N.t("common.operation"))}"><span class="arr-data-review-item-status">${escapeHtml(I18N.t(item.confirmed ? "data_review.confirmed" : "data_review.pending"))}</span><button type="button" data-arr-data-review-save ${editable ? "" : "disabled"}>${escapeHtml(I18N.t("data_review.save"))}</button>${item.can_be_empty ? `<button type="button" data-arr-data-review-empty ${editable ? "" : "disabled"}>${escapeHtml(I18N.t("data_review.confirm_empty"))}</button>` : ""}</td> <td class="arr-data-review-row-actions" data-label="${escapeHtml(I18N.t("common.operation"))}"><span class="arr-data-review-item-status">${escapeHtml(I18N.t(item.confirmed ? "data_review.confirmed" : "data_review.pending"))}</span><button type="button" data-arr-data-review-save ${editable ? "" : "disabled"}>${escapeHtml(I18N.t("data_review.save"))}</button>${item.can_be_empty ? `<button type="button" data-arr-data-review-empty ${editable ? "" : "disabled"}>${escapeHtml(I18N.t("data_review.confirm_empty"))}</button>` : ""}</td>
</tr>`; </tr>`;
@@ -1695,6 +1663,8 @@
? `<button class="daily-delete-button" type="button" data-daily-delete="${escapeHtml(primary.job_id)}" data-daily-date="${escapeHtml(date)}" data-daily-current="${Boolean(current)}" ${state.dailyDeleting ? "disabled" : ""}>${escapeHtml(I18N.t("common.delete"))}</button>` : ""; ? `<button class="daily-delete-button" type="button" data-daily-delete="${escapeHtml(primary.job_id)}" data-daily-date="${escapeHtml(date)}" data-daily-current="${Boolean(current)}" ${state.dailyDeleting ? "disabled" : ""}>${escapeHtml(I18N.t("common.delete"))}</button>` : "";
const pendingMarkup = current && replacement const pendingMarkup = current && replacement
? `<div class="daily-replacement"><span>${escapeHtml(I18N.t("daily.replacement"))}</span>${dailyJobStatus(replacement)}</div>` : ""; ? `<div class="daily-replacement"><span>${escapeHtml(I18N.t("daily.replacement"))}</span>${dailyJobStatus(replacement)}</div>` : "";
const historyButton = Number(day.history_count) > 0
? `<button class="daily-history-button" type="button" data-daily-history="${escapeHtml(date)}">${escapeHtml(I18N.t("daily.history_versions", { count: formatInteger(day.history_count) }))}</button>` : "";
return `<tr> return `<tr>
<td><span class="filename" title="${escapeHtml(dailyJobSource(primary))}">${escapeHtml(dailyJobSource(primary))}</span>${current || historicalOnly ? `<small class="daily-version-label">${escapeHtml(I18N.t(current ? "daily.current" : "daily.history_only"))}</small>` : ""}</td> <td><span class="filename" title="${escapeHtml(dailyJobSource(primary))}">${escapeHtml(dailyJobSource(primary))}</span>${current || historicalOnly ? `<small class="daily-version-label">${escapeHtml(I18N.t(current ? "daily.current" : "daily.history_only"))}</small>` : ""}</td>
<td>${historicalOnly ? `<span class="cell-subtle">${escapeHtml(I18N.t("daily.no_current"))}</span>` : dailyJobStatus(primary)}${pendingMarkup}</td> <td>${historicalOnly ? `<span class="cell-subtle">${escapeHtml(I18N.t("daily.no_current"))}</span>` : dailyJobStatus(primary)}${pendingMarkup}</td>
@@ -1702,7 +1672,7 @@
<td>${current ? formatInteger(current.no_of_rooms) : "—"}</td> <td>${current ? formatInteger(current.no_of_rooms) : "—"}</td>
<td class="cell-subtle">${current ? duration(current) : "—"}</td> <td class="cell-subtle">${current ? duration(current) : "—"}</td>
<td class="cell-subtle">${primary?.created_at ? formatDate(primary.created_at, true) : "—"}</td> <td class="cell-subtle">${primary?.created_at ? formatDate(primary.created_at, true) : "—"}</td>
<td><div class="daily-row-actions">${dailyJobDownload(current)}${deleteButton}<button class="daily-history-button" type="button" data-daily-history="${escapeHtml(date)}">${escapeHtml(I18N.t("daily.history_versions", { count: formatInteger(day.history_count || 0) }))}</button></div></td> <td><div class="daily-row-actions">${dailyJobDownload(current)}${deleteButton}${historyButton}</div></td>
</tr>`; </tr>`;
}).join(""); }).join("");
renderDailyOverview(); renderDailyOverview();
@@ -1869,17 +1839,6 @@
: String(value); : String(value);
} }
function reviewCandidateMarkup(candidates) {
if (!Array.isArray(candidates) || !candidates.length) {
return `<span class="daily-price-review-none">${escapeHtml(I18N?.t("daily_review.no_candidates") || "暂无同公司 / Rate Code 固定价")}</span>`;
}
return candidates.map((candidate) => {
const opera = reviewPrice(candidate?.effective_rate_amount);
const price = reviewPrice(candidate?.real_price);
return `<span class="daily-price-review-candidate">Opera ${escapeHtml(opera)} → ${escapeHtml(price)}</span>`;
}).join("");
}
function strictReviewPrice(value) { function strictReviewPrice(value) {
return /^(?:0|[1-9]\d{0,15})$/.test(String(value ?? "").trim()); return /^(?:0|[1-9]\d{0,15})$/.test(String(value ?? "").trim());
} }
@@ -1950,18 +1909,9 @@
const editable = dailyPriceReviewEditable(review); const editable = dailyPriceReviewEditable(review);
const retryable = review.case_status === "generation_failed"; const retryable = review.case_status === "generation_failed";
const processing = review.case_status === "processing"; const processing = review.case_status === "processing";
const completed = Number(review.completed_items || 0);
const total = Number(review.total_items || 0);
panel.classList.toggle("is-busy", busy); panel.classList.toggle("is-busy", busy);
panel.classList.toggle("is-retryable", retryable); panel.classList.toggle("is-retryable", retryable);
panel.classList.toggle("is-processing", processing); panel.classList.toggle("is-processing", processing);
$("#daily-price-review-completed").textContent = formatInteger(completed);
$("#daily-price-review-pending").textContent = formatInteger(Math.max(0, total - completed));
const reviewCopy = $("#daily-price-review-copy");
reviewCopy.hidden = retryable;
reviewCopy.textContent = processing
? (I18N?.t("daily_review.processing_instruction") || "正在使用已冻结的价格清单重放原始 XML;此时无法修改。")
: (I18N?.t("daily_review.default_instruction") || "请填写每晚处理价,可填 0。每项填写后点击“暂存”,全部暂存后生成日报。");
const finalize = $("#daily-price-review-finalize"); const finalize = $("#daily-price-review-finalize");
const cancel = $("#daily-price-review-cancel"); const cancel = $("#daily-price-review-cancel");
finalize.disabled = busy || !dailyPriceReviewCanFinalize(review); finalize.disabled = busy || !dailyPriceReviewCanFinalize(review);
@@ -1979,9 +1929,6 @@
company: I18N?.t("common.company") || "公司", company: I18N?.t("common.company") || "公司",
rateCode: I18N?.t("daily_review.rate_code") || "Rate Code", rateCode: I18N?.t("daily_review.rate_code") || "Rate Code",
operaPrice: I18N?.t("daily_review.opera_price") || "Opera 价", operaPrice: I18N?.t("daily_review.opera_price") || "Opera 价",
candidates: I18N?.t("daily_review.fixed_candidates") || "固定价候选",
affectedRecords: I18N?.t("daily_review.affected_records") || "影响记录",
roomsNights: I18N?.t("daily_review.rooms_nights") || "房数 / 房晚",
manualPrice: I18N?.t("daily_review.manual_price") || "人工价格", manualPrice: I18N?.t("daily_review.manual_price") || "人工价格",
actions: I18N?.t("common.operation") || "操作", actions: I18N?.t("common.operation") || "操作",
}; };
@@ -1993,18 +1940,16 @@
const inputLabel = I18N?.t("daily_review.manual_price_label", { const inputLabel = I18N?.t("daily_review.manual_price_label", {
company: priceKey, company: priceKey,
rateCode: item.rate_code || "", rateCode: item.rate_code || "",
operaPrice: reviewPrice(item.effective_rate_amount),
}) || `${priceKey} ${item.rate_code || ""} 的人工价格`; }) || `${priceKey} ${item.rate_code || ""} 的人工价格`;
return ` return `
<tr class="daily-price-review-row${saved ? " is-complete" : ""}" data-daily-price-review-item-id="${Number(item.item_id)}"> <tr class="daily-price-review-row${saved ? " is-complete" : ""}" data-daily-price-review-item-id="${Number(item.item_id)}">
<td data-label="${escapeHtml(labels.company)}"><strong>${escapeHtml(item.company_key || "—")}</strong></td> <td data-label="${escapeHtml(labels.company)}"><strong>${escapeHtml(item.company_key || "—")}</strong></td>
<td data-label="${escapeHtml(labels.rateCode)}"><code>${escapeHtml(item.rate_code || "—")}</code></td> <td data-label="${escapeHtml(labels.rateCode)}"><code>${escapeHtml(item.rate_code || "—")}</code></td>
<td data-label="${escapeHtml(labels.operaPrice)}">${escapeHtml(reviewPrice(item.effective_rate_amount))}</td> <td data-label="${escapeHtml(labels.operaPrice)}">${escapeHtml(reviewPrice(item.effective_rate_amount))}</td>
<td class="daily-price-review-candidates" data-label="${escapeHtml(labels.candidates)}">${reviewCandidateMarkup(item.candidate_prices)}</td>
<td data-label="${escapeHtml(labels.affectedRecords)}">${formatInteger(item.affected_records)}</td>
<td data-label="${escapeHtml(labels.roomsNights)}">${formatInteger(item.affected_rooms)} / ${formatInteger(item.affected_room_nights)}</td>
<td data-label="${escapeHtml(labels.manualPrice)}"> <td data-label="${escapeHtml(labels.manualPrice)}">
<label class="sr-only" for="${inputId}">${escapeHtml(inputLabel)}</label> <label class="sr-only" for="${inputId}">${escapeHtml(inputLabel)}</label>
<input class="daily-price-review-input" id="${inputId}" data-daily-price-review-price value="${escapeHtml(reviewPriceInput(item.real_price))}" inputmode="numeric" autocomplete="off" pattern="(?:0|[1-9][0-9]{0,15})" placeholder="${escapeHtml(I18N?.t("daily_review.price_placeholder") || "请输入价格(可填 0)")}" aria-describedby="daily-price-review-copy" ${disabled ? "disabled" : ""} /> <input class="daily-price-review-input" id="${inputId}" data-daily-price-review-price value="${escapeHtml(reviewPriceInput(item.real_price))}" inputmode="numeric" autocomplete="off" pattern="(?:0|[1-9][0-9]{0,15})" placeholder="${escapeHtml(I18N?.t("daily_review.price_placeholder") || "请输入价格(可填 0)")}" aria-describedby="daily-price-review-message" ${disabled ? "disabled" : ""} />
</td> </td>
<td class="daily-price-review-row-actions" data-label="${escapeHtml(labels.actions)}"> <td class="daily-price-review-row-actions" data-label="${escapeHtml(labels.actions)}">
<span class="daily-price-review-status${saved ? " is-complete" : " is-pending"}">${escapeHtml(saved ? (I18N?.t("daily_review.saved") || "已暂存") : (I18N?.t("daily_review.awaiting_input") || "待填写"))}</span> <span class="daily-price-review-status${saved ? " is-complete" : " is-pending"}">${escapeHtml(saved ? (I18N?.t("daily_review.saved") || "已暂存") : (I18N?.t("daily_review.awaiting_input") || "待填写"))}</span>
@@ -4026,7 +3971,6 @@
if (!jobId) return; if (!jobId) return;
try { await openDailyPriceReview(jobId, true); } catch (error) { showToast(error.message, true); } try { await openDailyPriceReview(jobId, true); } catch (error) { showToast(error.message, true); }
}); });
$("#arr-download-data-review").addEventListener("click", () => loadARRDataReview(state.arrDownloadTask?.request_id, { scroll: true }));
$("#arr-download-pending-list").addEventListener("click", (event) => { $("#arr-download-pending-list").addEventListener("click", (event) => {
const button = event.target.closest("[data-arr-pending-review]"); const button = event.target.closest("[data-arr-pending-review]");
if (button) selectARRPendingReview(button.dataset.arrPendingReview); if (button) selectARRPendingReview(button.dataset.arrPendingReview);
+2 -3
View File
@@ -113,7 +113,7 @@
"arr_download.date_resume": ["可先选择日期;请先确认或继续 {date} 的原任务。", "You can choose a date. First check or resume the original task for {date}.", "เลือกวันที่ได้ แต่โปรดตรวจสอบหรือดำเนินงานเดิมของวันที่ {date} ต่อก่อน"], "arr_download.date_resume": ["可先选择日期;请先确认或继续 {date} 的原任务。", "You can choose a date. First check or resume the original task for {date}.", "เลือกวันที่ได้ แต่โปรดตรวจสอบหรือดำเนินงานเดิมของวันที่ {date} ต่อก่อน"],
"arr_download.help": ["选择一天,自动获取到店数据并生成报表。", "Choose one day to fetch arrivals and generate reports.", "เลือกหนึ่งวันเพื่อดาวน์โหลดและประมวลผลรายงานผู้เข้าพัก"], "arr_download.help": ["选择一天,自动获取到店数据并生成报表。", "Choose one day to fetch arrivals and generate reports.", "เลือกหนึ่งวันเพื่อดาวน์โหลดและประมวลผลรายงานผู้เข้าพัก"],
"arr_download.start": ["下载并处理", "Download & process", "ดาวน์โหลดและประมวลผล"], "arr_download.start": ["下载并处理", "Download & process", "ดาวน์โหลดและประมวลผล"],
"arr_acquisition.label": ["取数状态", "Data retrieval", "สถานะการดึงข้อมูล"], "arr_acquisition.label": ["状态", "Status", "สถานะ"],
"arr_acquisition.queued": ["等待取数", "Queued", "รอดึงข้อมูล"], "arr_acquisition.queued": ["等待取数", "Queued", "รอดึงข้อมูล"],
"arr_acquisition.active": ["取数中", "Retrieving", "กำลังดึงข้อมูล"], "arr_acquisition.active": ["取数中", "Retrieving", "กำลังดึงข้อมูล"],
"arr_acquisition.completed": ["已完成", "Completed", "เสร็จแล้ว"], "arr_acquisition.completed": ["已完成", "Completed", "เสร็จแล้ว"],
@@ -140,7 +140,6 @@
"arr_download.needs_review": ["等待人工价格复核", "Awaiting price review", "รอตรวจสอบราคาด้วยตนเอง"], "arr_download.needs_review": ["等待人工价格复核", "Awaiting price review", "รอตรวจสอบราคาด้วยตนเอง"],
"arr_download.needs_data_review": ["等待人工完善报表字段", "Awaiting source field review", "รอตรวจสอบข้อมูลรายงานด้วยตนเอง"], "arr_download.needs_data_review": ["等待人工完善报表字段", "Awaiting source field review", "รอตรวจสอบข้อมูลรายงานด้วยตนเอง"],
"arr_download.complete_data": ["完善报表字段", "Complete report fields", "กรอกข้อมูลรายงานให้ครบ"], "arr_download.complete_data": ["完善报表字段", "Complete report fields", "กรอกข้อมูลรายงานให้ครบ"],
"arr_download.date_independent": ["可选其他日期下载,当前待处理内容会保留。", "Choose another date to download; this review will be kept.", "เลือกวันอื่นเพื่อดาวน์โหลดได้ ข้อมูลที่รอตรวจสอบจะยังคงอยู่"],
"arr_download.pending_dates": ["需处理日期", "Dates requiring attention", "วันที่ต้องดำเนินการ"], "arr_download.pending_dates": ["需处理日期", "Dates requiring attention", "วันที่ต้องดำเนินการ"],
"arr_download.pending_date": ["待补字段", "Complete fields", "รอกรอกข้อมูล"], "arr_download.pending_date": ["待补字段", "Complete fields", "รอกรอกข้อมูล"],
"arr_download.pending_price": ["待复核价格", "Review prices", "รอตรวจสอบราคา"], "arr_download.pending_price": ["待复核价格", "Review prices", "รอตรวจสอบราคา"],
@@ -343,7 +342,7 @@
"daily_review.save_before_switch": ["当前价格尚未暂存,请先暂存,再切换需处理日期。", "Save the current price edits before switching to another review.", "โปรดบันทึกราคาที่แก้ไขก่อนเปลี่ยนไปตรวจสอบวันที่อื่น"], "daily_review.save_before_switch": ["当前价格尚未暂存,请先暂存,再切换需处理日期。", "Save the current price edits before switching to another review.", "โปรดบันทึกราคาที่แก้ไขก่อนเปลี่ยนไปตรวจสอบวันที่อื่น"],
"daily_review.no_candidates": ["暂无同公司 / Rate Code 固定价", "No fixed price for the same company / Rate Code", "ไม่มีราคาคงที่สำหรับบริษัท / Rate Code เดียวกัน"], "daily_review.no_candidates": ["暂无同公司 / Rate Code 固定价", "No fixed price for the same company / Rate Code", "ไม่มีราคาคงที่สำหรับบริษัท / Rate Code เดียวกัน"],
"daily_review.price_key": ["价格键", "price key", "คีย์ราคา"], "daily_review.price_key": ["价格键", "price key", "คีย์ราคา"],
"daily_review.manual_price_label": ["{company} {rateCode} 的人工价格", "Manual price for {company} {rateCode}", "ราคาที่กำหนดด้วยตนเองสำหรับ {company} {rateCode}"], "daily_review.manual_price_label": ["{company} {rateCode}(Opera 价 {operaPrice})的人工价格", "Manual price for {company} {rateCode} (Opera {operaPrice})", "ราคาที่กำหนดด้วยตนเองสำหรับ {company} {rateCode} (Opera {operaPrice})"],
"daily_review.saved": ["已暂存", "Saved", "บันทึกแล้ว"], "daily_review.saved": ["已暂存", "Saved", "บันทึกแล้ว"],
"daily_review.awaiting_input": ["待填写", "Not entered", "รอกรอก"], "daily_review.awaiting_input": ["待填写", "Not entered", "รอกรอก"],
"daily_review.save": ["暂存", "Save", "บันทึก"], "daily_review.save": ["暂存", "Save", "บันทึก"],
+6 -20
View File
@@ -76,7 +76,7 @@
</div> </div>
<div class="arr-acquisition-progress" id="arr-acquisition-progress" hidden> <div class="arr-acquisition-progress" id="arr-acquisition-progress" hidden>
<div class="arr-acquisition-meta"> <div class="arr-acquisition-meta">
<span><span id="arr-acquisition-label">取数状态</span><strong id="arr-acquisition-status" role="status" aria-live="polite"></strong></span> <span><span id="arr-acquisition-label">状态</span><strong id="arr-acquisition-status" role="status" aria-live="polite"></strong></span>
<strong id="arr-acquisition-percent" hidden></strong> <strong id="arr-acquisition-percent" hidden></strong>
</div> </div>
<div class="arr-acquisition-track" id="arr-acquisition-track" role="progressbar" aria-labelledby="arr-acquisition-label" aria-describedby="arr-acquisition-detail" aria-valuemin="0" aria-valuemax="100"> <div class="arr-acquisition-track" id="arr-acquisition-track" role="progressbar" aria-labelledby="arr-acquisition-label" aria-describedby="arr-acquisition-detail" aria-valuemin="0" aria-valuemax="100">
@@ -85,13 +85,13 @@
<p id="arr-acquisition-detail"></p> <p id="arr-acquisition-detail"></p>
</div> </div>
<div class="arr-download-actions"> <div class="arr-download-actions">
<button class="arr-download-review" id="arr-download-data-review" type="button" hidden>完善报表字段</button>
<button class="arr-download-review" id="arr-download-review" type="button" hidden>查看价格复核</button> <button class="arr-download-review" id="arr-download-review" type="button" hidden>查看价格复核</button>
<button class="primary-button" id="arr-download-button" type="submit" disabled>下载并处理</button> <button class="primary-button" id="arr-download-button" type="submit" disabled>下载并处理</button>
</div> </div>
</form> </form>
</section> </section>
<section class="panel upload-panel" aria-labelledby="upload-title"> <section class="panel upload-panel" aria-labelledby="upload-title">
<div class="upload-content">
<div class="panel-heading upload-heading"> <div class="panel-heading upload-heading">
<div> <div>
<h2 id="upload-title">上传 ARR.XML</h2> <h2 id="upload-title">上传 ARR.XML</h2>
@@ -115,6 +115,7 @@
<span id="upload-progress-bar"></span> <span id="upload-progress-bar"></span>
</div> </div>
</div> </div>
</div>
<div class="upload-footer"> <div class="upload-footer">
<p class="service-hint" id="upload-hint" role="status"></p> <p class="service-hint" id="upload-hint" role="status"></p>
<button class="primary-button" id="upload-button" type="button" disabled>开始处理</button> <button class="primary-button" id="upload-button" type="button" disabled>开始处理</button>
@@ -176,13 +177,7 @@
<p id="manual-review-context" role="status" aria-live="polite"></p> <p id="manual-review-context" role="status" aria-live="polite"></p>
</header> </header>
<section class="arr-data-review-panel" id="arr-data-review-panel" aria-labelledby="arr-data-review-title" tabindex="-1" hidden> <section class="arr-data-review-panel" id="arr-data-review-panel" aria-labelledby="arr-data-review-title" tabindex="-1" hidden>
<header class="arr-data-review-header"> <h3 class="sr-only" id="arr-data-review-title">完善报表字段</h3>
<div>
<h3 class="sr-only" id="arr-data-review-title">完善报表字段</h3>
<p id="arr-data-review-copy">请核实并逐项保存缺失或有冲突的字段。允许为空的字段也须明确确认,全部完成后才能生成日报。</p>
</div>
<p id="arr-data-review-progress" role="status" aria-live="polite"></p>
</header>
<div class="arr-data-review-toolbar"> <div class="arr-data-review-toolbar">
<p id="arr-data-review-message" role="status" aria-live="polite"></p> <p id="arr-data-review-message" role="status" aria-live="polite"></p>
<div class="arr-data-review-actions"> <div class="arr-data-review-actions">
@@ -200,16 +195,7 @@
</section> </section>
<section class="daily-price-review-panel" id="daily-price-review-panel" aria-labelledby="daily-price-review-title" tabindex="-1" hidden> <section class="daily-price-review-panel" id="daily-price-review-panel" aria-labelledby="daily-price-review-title" tabindex="-1" hidden>
<header class="daily-price-review-header"> <h3 class="sr-only" id="daily-price-review-title">价格复核</h3>
<div class="daily-price-review-title-block">
<h3 class="sr-only" id="daily-price-review-title">价格复核</h3>
<p id="daily-price-review-copy" role="status">请为所有缺少固定价的价格键填写每晚价格。相同键只需填写一次。</p>
</div>
<dl class="daily-price-review-metrics" aria-label="人工价格复核进度">
<div><dt>已填写</dt><dd id="daily-price-review-completed">0</dd></div>
<div class="is-review"><dt>待处理</dt><dd id="daily-price-review-pending">0</dd></div>
</dl>
</header>
<div class="daily-price-review-toolbar"> <div class="daily-price-review-toolbar">
<p class="daily-price-review-message" id="daily-price-review-message" role="status"></p> <p class="daily-price-review-message" id="daily-price-review-message" role="status"></p>
<div class="daily-price-review-actions"> <div class="daily-price-review-actions">
@@ -220,7 +206,7 @@
<div class="daily-price-review-scroll" role="region" aria-label="待人工定价项" tabindex="0"> <div class="daily-price-review-scroll" role="region" aria-label="待人工定价项" tabindex="0">
<table class="daily-price-review-table"> <table class="daily-price-review-table">
<thead> <thead>
<tr><th scope="col">公司</th><th scope="col">Rate Code</th><th scope="col">Opera 价</th><th scope="col">固定价候选</th><th scope="col">影响记录</th><th scope="col">房数 / 房晚</th><th scope="col">人工价格</th><th scope="col">操作</th></tr> <tr><th scope="col">公司</th><th scope="col">Rate Code</th><th scope="col">Opera 价</th><th scope="col">人工价格</th><th scope="col">操作</th></tr>
</thead> </thead>
<tbody id="daily-price-review-body"></tbody> <tbody id="daily-price-review-body"></tbody>
</table> </table>
+26 -13
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 { 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; } .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(220px, 1.15fr) minmax(230px, 1.2fr) minmax(155px, .8fr) minmax(125px, .65fr) minmax(220px, 1.15fr); gap: 16px; align-items: stretch; margin-bottom: 20px; } .daily-overview { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.25fr) minmax(0, .85fr) minmax(0, .7fr) minmax(0, 1.2fr); gap: 12px; align-items: stretch; margin-bottom: 20px; }
.daily-overview > * { min-width: 0; } .daily-overview > * { min-width: 0; }
.daily-overview > .upload-panel { display: flex; flex-direction: column; } .daily-overview > .upload-panel { display: flex; flex-direction: column; }
.daily-overview .upload-footer { margin-top: auto; } .daily-overview .upload-footer { margin-top: auto; }
@@ -273,7 +273,7 @@ button { color: inherit; }
.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 { 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 > .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 { padding-left: 16px; padding-right: 16px; }
.daily-overview > .arrival-metric strong { font-size: clamp(20px, 1.5vw, 24px); } .daily-overview > .arrival-metric strong { font-size: clamp(18px, 1.3vw, 22px); white-space: nowrap; }
.report-calendar { position: relative; display: flex; flex-direction: column; justify-content: center; padding: 22px; overflow: visible; } .report-calendar { position: relative; display: flex; flex-direction: column; justify-content: center; padding: 22px; overflow: visible; }
.report-calendar h2 { margin: 0 0 16px; font-size: 17px; line-height: 1.4; letter-spacing: -.02em; } .report-calendar h2 { margin: 0 0 16px; font-size: 17px; line-height: 1.4; 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 button { min-height: 44px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink); background: #fff; cursor: pointer; }
@@ -514,7 +514,7 @@ tbody tr:hover td { background: #fafcff; }
.daily-price-review-finalize:hover:not(:disabled) { border-color: #854507; background: #854507; } .daily-price-review-finalize:hover:not(:disabled) { border-color: #854507; background: #854507; }
.daily-price-review-cancel:disabled, .daily-price-review-finalize:disabled { border-color: #d7dde7; color: #667085; background: #e9edf3; box-shadow: none; cursor: not-allowed; } .daily-price-review-cancel:disabled, .daily-price-review-finalize:disabled { border-color: #d7dde7; color: #667085; background: #e9edf3; box-shadow: none; cursor: not-allowed; }
.daily-price-review-scroll { max-width: 100%; overflow: auto; } .daily-price-review-scroll { max-width: 100%; overflow: auto; }
.daily-price-review-table { width: 100%; min-width: 1080px; border-collapse: collapse; } .daily-price-review-table { width: 100%; min-width: 660px; border-collapse: collapse; }
.daily-price-review-table th { padding: 10px 12px; color: #735d39; background: #fffaf0; font-size: 9px; font-weight: 800; letter-spacing: .04em; text-align: left; text-transform: uppercase; white-space: nowrap; } .daily-price-review-table th { padding: 10px 12px; color: #735d39; background: #fffaf0; font-size: 9px; font-weight: 800; letter-spacing: .04em; text-align: left; text-transform: uppercase; white-space: nowrap; }
.daily-price-review-table td { padding: 10px 12px; border-top: 1px solid #f0f1f4; color: var(--ink); font-size: 10px; vertical-align: middle; } .daily-price-review-table td { padding: 10px 12px; border-top: 1px solid #f0f1f4; color: var(--ink); font-size: 10px; vertical-align: middle; }
.daily-price-review-row:hover td { background: #fffdf8; } .daily-price-review-row:hover td { background: #fffdf8; }
@@ -822,16 +822,18 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
} }
@media (min-width: 701px) { @media (min-width: 701px) {
.daily-overview .upload-heading { padding: 12px 18px 0; } .daily-overview > .upload-panel { position: relative; justify-content: center; min-height: 260px; }
.daily-overview .upload-heading h2 { font-size: 17px; } .daily-overview .upload-content { padding-bottom: 0; }
.daily-overview .upload-heading { padding: 0 18px; }
.daily-overview .upload-heading h2 { margin: 0; font-size: 17px; line-height: 1.4; }
.daily-overview .dropzone { .daily-overview .dropzone {
min-height: 72px; min-height: 72px;
margin: 8px 18px 0; margin: 16px 18px 0;
padding: 10px 12px; padding: 8px;
display: grid; display: grid;
grid-template-columns: 28px minmax(0, 1fr); grid-template-columns: 20px minmax(0, 1fr);
grid-template-rows: repeat(3, min-content); grid-template-rows: repeat(3, min-content);
column-gap: 10px; column-gap: 8px;
row-gap: 0; row-gap: 0;
align-content: center; align-content: center;
align-items: center; align-items: center;
@@ -839,11 +841,11 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
} }
.daily-overview .dropzone .upload-glyph { .daily-overview .dropzone .upload-glyph {
grid-row: 1 / span 3; grid-row: 1 / span 3;
width: 28px; width: 20px;
height: 28px; height: 20px;
border-radius: 9px; border-radius: 6px;
} }
.daily-overview .dropzone .upload-glyph svg { width: 16px; height: 16px; } .daily-overview .dropzone .upload-glyph svg { width: 14px; height: 14px; }
.daily-overview .dropzone strong { .daily-overview .dropzone strong {
margin-top: 0; margin-top: 0;
font-size: 12px; font-size: 12px;
@@ -868,6 +870,10 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
.daily-overview .upload-progress-meta strong { font-size: 10px; } .daily-overview .upload-progress-meta strong { font-size: 10px; }
.daily-overview .upload-progress-track { height: 5px; margin-top: 6px; } .daily-overview .upload-progress-track { height: 5px; margin-top: 6px; }
.daily-overview .upload-footer { .daily-overview .upload-footer {
position: absolute;
right: 0;
bottom: 0;
left: 0;
min-height: 44px; min-height: 44px;
margin: 0 18px; margin: 0 18px;
margin-top: auto; margin-top: auto;
@@ -884,6 +890,13 @@ input:focus, select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgb
} }
} }
@media (min-width: 1440px) {
.daily-overview { width: calc(100% * 2 / 3); }
.daily-overview > .metric-card { padding-left: 14px; padding-right: 14px; }
.daily-overview > .arrival-metric strong { font-size: 18px; }
.report-calendar { padding-left: 18px; padding-right: 18px; }
}
@media (max-width: 700px) { @media (max-width: 700px) {
.shell { width: min(100% - 28px, 1400px); } .shell { width: min(100% - 28px, 1400px); }
.masthead { height: 64px; } .masthead { height: 64px; }
+26 -21
View File
@@ -318,21 +318,23 @@ test('pending date labels have Chinese, English and Thai translations',()=>{
assert.equal(catalog['arr_download.pending_price'][0],'待复核价格'); assert.equal(catalog['arr_download.pending_price'][0],'待复核价格');
}); });
test('cancelled source rows need no fields and the exclusion stays visible before continuation',async()=>{ test('cancelled source exclusions need no fields or summary and still permit continuation',async()=>{
const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6})); const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6}));
await h.acceptARRDownloadTask(task,{sync:false}); await h.acceptARRDownloadTask(task,{sync:false});
assert.equal(h.element('#arr-data-review-body').innerHTML,''); assert.equal(h.element('#arr-data-review-body').innerHTML,'');
assert.match(h.element('#arr-data-review-progress').textContent,/data_review\.excluded_cancelled/); assert.equal(h.element('#arr-data-review-progress').textContent,'');
assert.equal(h.state.arrDataReview.excluded_cancelled_count,6);
assert.equal(h.element('#arr-data-review-finalize').disabled,false); assert.equal(h.element('#arr-data-review-finalize').disabled,false);
assert.equal(h.calls.every(call=>call.method==='GET'),true); assert.equal(h.calls.every(call=>call.method==='GET'),true);
}); });
test('PM exclusions stay visible without requiring field completion',async()=>{ test('PM exclusions retain their original state without displaying a summary or requiring fields',async()=>{
const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6,excluded_pm_count:19})); const h=harness(()=>response(200,{...review([]),excluded_cancelled_count:6,excluded_pm_count:19}));
await h.acceptARRDownloadTask(task,{sync:false}); await h.acceptARRDownloadTask(task,{sync:false});
const progress=h.element('#arr-data-review-progress').textContent; const progress=h.element('#arr-data-review-progress').textContent;
assert.match(progress,/data_review\.excluded_cancelled/); assert.equal(progress,'');
assert.match(progress,/data_review\.excluded_pm/); assert.equal(h.state.arrDataReview.excluded_cancelled_count,6);
assert.equal(h.state.arrDataReview.excluded_pm_count,19);
assert.equal(h.element('#arr-data-review-body').innerHTML,''); assert.equal(h.element('#arr-data-review-body').innerHTML,'');
assert.equal(h.element('#arr-data-review-finalize').disabled,false); assert.equal(h.element('#arr-data-review-finalize').disabled,false);
assert.equal(h.calls.every(call=>call.method==='GET'),true); assert.equal(h.calls.every(call=>call.method==='GET'),true);
@@ -383,39 +385,41 @@ for (const fixture of [
{name:'confirmed decisions show the saved confirmation instead of the original problem', {name:'confirmed decisions show the saved confirmation instead of the original problem',
source_state:'failed', reason_code:'http_failure', confirmed:true, value:'VERIFIED', expected:'confirmed_value'}, source_state:'failed', reason_code:'http_failure', confirmed:true, value:'VERIFIED', expected:'confirmed_value'},
]) { ]) {
test(`field review explanation: ${fixture.name}`,async()=>{ test(`compact field review preserves decisions without explanatory metadata: ${fixture.name}`,async()=>{
const source=item(fixture); const source=item(fixture);
const original=plain(source); const original=plain(source);
const h=harness(()=>response(200,review([source]))); const h=harness(()=>response(200,review([source])));
await h.acceptARRDownloadTask(task,{sync:false}); await h.acceptARRDownloadTask(task,{sync:false});
const markup=h.element('#arr-data-review-body').innerHTML; const markup=h.element('#arr-data-review-body').innerHTML;
assert.match(markup,new RegExp(`data_review\\.${fixture.expected}\\b`)); assert.doesNotMatch(markup,/<small>|data_review\.(?:optional_field_rule|required_field_rule|source_row|room_missing_in_scope|source_unconfirmed|fetch_failed|conflict_value|invalid_value|confirmed_value)/);
assert.match(markup,new RegExp(`data_review\\.${source.can_be_empty?'optional':'required'}_field_rule\\b`)); assert.match(markup,new RegExp(`<label[^>]+>${source.field_label}</label>`));
assert.match(markup,/<strong>SYNTHETIC-001<\/strong>/);
assert.doesNotMatch(markup,/Synthetic Company|SYNTHETIC-ROOM|<small>/);
assert.equal((markup.match(/<td /g)||[]).length,4);
assert.match(markup,/data-arr-data-review-value/);
assert.match(markup,/data-arr-data-review-save/);
assert.match(markup,new RegExp(source.confirmed?'data_review.confirmed':'data_review.pending'));
assert.equal(markup.includes('data_review.missing_value'),false); assert.equal(markup.includes('data_review.missing_value'),false);
assert.equal(markup.includes('data-arr-data-review-empty'),source.can_be_empty); assert.equal(markup.includes('data-arr-data-review-empty'),source.can_be_empty);
assert.deepEqual(plain(source),original,'rendering explanations never changes a source or confirmation'); assert.deepEqual(plain(source),original,'a compact display never changes source metadata or confirmation');
assert.equal(h.calls.every(call=>call.method==='GET'),true); assert.equal(h.calls.every(call=>call.method==='GET'),true);
}); });
} }
test('review explanations have distinct Chinese, English and Thai copy for required, optional and failed reads',()=>{ test('compact review controls and validation messages remain available in Chinese, English and Thai',()=>{
const context=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}}); const context=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}});
vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),context); vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),context);
const catalog=context.window.ARRI18n.CATALOG; const catalog=context.window.ARRI18n.CATALOG;
const keys=['required_field_rule','optional_field_rule','required_value_missing','room_missing_in_scope','source_unconfirmed', const keys=['save','confirm_empty','confirmed','pending','field_block_code','field_disp_room_no','use_empty',
'fetch_failed','conflict_value','invalid_value']; 'complete_first','conflict','disconnected','invalid','required','integer_required'];
for(const key of keys) { for(const key of keys) {
const translations=catalog[`data_review.${key}`]; const translations=catalog[`data_review.${key}`];
assert.equal(translations.length,3); assert.equal(translations.length,3);
translations.forEach(text=>assert.equal(typeof text==='string' && text.length>0,true)); translations.forEach(text=>assert.equal(typeof text==='string' && text.length>0,true));
} }
assert.match(catalog['data_review.required_field_rule'][0],/原处理规则必填/); assert.equal(catalog['arr_acquisition.label'][0],'状态');
assert.match(catalog['data_review.optional_field_rule'][0],/允许为空/); assert.equal(catalog['arr_acquisition.label'][1],'Status');
assert.match(catalog['data_review.room_missing_in_scope'][0],/费率在处理范围内/); assert.equal(catalog['arr_download.date_independent'],undefined);
assert.match(catalog['data_review.source_unconfirmed'][0],/确认无此信息/);
assert.match(catalog['data_review.fetch_failed'][0],/不能直接认定没有此信息/);
assert.equal(catalog['data_review.fetch_failed'][0].includes('刷新'),false,
'refreshing the review must not imply a retry of Oracle acquisition');
}); });
test('source review opens before a job exists and another date can start its own download',async()=>{ test('source review opens before a job exists and another date can start its own download',async()=>{
@@ -423,7 +427,8 @@ test('source review opens before a job exists and another date can start its own
? response(202,{...JSON.parse(options.body),status:'queued',job_id:null,can_retry:false}) ? response(202,{...JSON.parse(options.body),status:'queued',job_id:null,can_retry:false})
: response(200,review([item()]))); : response(200,review([item()])));
await h.acceptARRDownloadTask(task,{sync:false}); await h.acceptARRDownloadTask(task,{sync:false});
assert.equal(h.element('#arr-download-data-review').hidden,false); assert.equal(h.element('#arr-download-button').textContent,'arr_download.complete_data');
assert.equal(h.element('#arr-download-help').hidden,true);
assert.equal(h.element('#arr-data-review-panel').hidden,false); assert.equal(h.element('#arr-data-review-panel').hidden,false);
assert.equal(h.element('#arr-download-review').hidden,true); assert.equal(h.element('#arr-download-review').hidden,true);
assert.equal(h.element('#arr-data-review-finalize').disabled,true); assert.equal(h.element('#arr-data-review-finalize').disabled,true);
@@ -586,7 +591,7 @@ test('revision conflict reloads the current revision before any next save',async
test('unconfirmed source values are never prefilled and all labels and stored values are escaped',async()=>{ test('unconfirmed source values are never prefilled and all labels and stored values are escaped',async()=>{
const h=harness(()=>response(200,review([ const h=harness(()=>response(200,review([
item({company_name:'<img src=x onerror=alert(1)>',value:'GUESSED-BLOCK'}), item({company_name:'<img src=x onerror=alert(1)>',confirmation_no:'<img src=x onerror=alert(1)>',value:'GUESSED-BLOCK'}),
item({item_id:'2:RES_COMMENT',field:'RES_COMMENT',confirmed:true,value:'</textarea><script>bad()</script>'}), item({item_id:'2:RES_COMMENT',field:'RES_COMMENT',confirmed:true,value:'</textarea><script>bad()</script>'}),
]))); ])));
await h.acceptARRDownloadTask(task,{sync:false}); await h.acceptARRDownloadTask(task,{sync:false});
+1 -1
View File
@@ -74,7 +74,7 @@ test('completed retrieval remains distinct from pending field and price review',
assert.equal(h.element('#arr-acquisition-status').textContent,'已完成'); assert.equal(h.element('#arr-acquisition-status').textContent,'已完成');
assert.equal(h.element('#arr-acquisition-percent').textContent,'100%'); assert.equal(h.element('#arr-acquisition-percent').textContent,'100%');
assert.equal(h.element('#arr-acquisition-detail').textContent,'已获取 85 / 85 笔'); assert.equal(h.element('#arr-acquisition-detail').textContent,'已获取 85 / 85 笔');
if(status==='needs_data_review') assert.equal(h.element('#arr-download-data-review').hidden,false); if(status==='needs_data_review') assert.equal(h.element('#arr-download-button').textContent,'完善报表字段');
if(status==='needs_review') assert.equal(h.element('#arr-download-review').hidden,false); if(status==='needs_review') assert.equal(h.element('#arr-download-review').hidden,false);
assert.doesNotMatch(h.element('#arr-acquisition-detail').textContent,/生成|报表已完成/); assert.doesNotMatch(h.element('#arr-acquisition-detail').textContent,/生成|报表已完成/);
} }
+9 -1
View File
@@ -19,8 +19,16 @@ test('zero is a valid explicit price while blank and negative prices remain inva
test('zero is rendered as a saved value and permits continuation when all items are saved',()=>{ test('zero is rendered as a saved value and permits continuation when all items are saved',()=>{
const h=harness(()=>{throw new Error('unexpected request');}); const h=harness(()=>{throw new Error('unexpected request');});
h.renderDailyPriceReview(fixture(0)); const review=fixture(0);
review.items[0].candidate_prices=[{effective_rate_amount:888888,real_price:999999}];
review.items[0].affected_records=333333;
h.renderDailyPriceReview(review);
const html=h.element('#daily-price-review-body').innerHTML; const html=h.element('#daily-price-review-body').innerHTML;
assert.equal((html.match(/<td /g)||[]).length,5);
assert.match(html,/Fixture Company|FIXTURE/);
assert.match(html,/>100<\/td>/);
assert.doesNotMatch(html,/888888|999999|333333|daily-price-review-candidates/);
assert.match(html,/aria-describedby="daily-price-review-message"/);
assert.match(html,/value="0"/); assert.match(html,/value="0"/);
assert.match(html,/daily-price-review-row is-complete/); assert.match(html,/daily-price-review-row is-complete/);
assert.match(html,/daily_review.saved/); assert.match(html,/daily_review.saved/);
@@ -62,6 +62,19 @@ test('a successful replacement displays only the accepted new report for the sam
assert.doesNotMatch(h.element('#jobs-body').innerHTML,/job_id=old/); assert.doesNotMatch(h.element('#jobs-body').innerHTML,/job_id=old/);
}); });
test('history access appears only when the date has historical versions while current download and delete remain available',()=>{
const h=setup();
for(const count of [0,undefined,1,2]) {
h.state.dailyReports=[day(job('current'),undefined,{history_count:count})];
h.renderJobs();
const html=h.element('#jobs-body').innerHTML;
assert.equal(html.includes(`data-daily-history="${date}"`),Number(count)>0);
assert.match(html,/job_id=current/);
assert.match(html,/data-daily-delete="current"/);
}
assert.equal(h.calls.length,0);
});
test('a failed replacement never removes the valid old report',()=>{ test('a failed replacement never removes the valid old report',()=>{
const h=setup(); const h=setup();
h.state.dailyReports=[day(job('old'),job('new',{status:'failed',is_current:false,daily_report_sha256:null}))]; h.state.dailyReports=[day(job('old'),job('new',{status:'failed',is_current:false,daily_report_sha256:null}))];
+14 -2
View File
@@ -38,7 +38,7 @@ class DailyPriceReviewUiContractTests(unittest.TestCase):
self.assertIn('id="daily-price-review-panel" aria-labelledby="daily-price-review-title" tabindex="-1"', self.html) self.assertIn('id="daily-price-review-panel" aria-labelledby="daily-price-review-title" tabindex="-1"', self.html)
self.assertIn("panel.focus({ preventScroll: true });", self.script) self.assertIn("panel.focus({ preventScroll: true });", self.script)
def test_retry_copy_is_removed_and_hidden(self) -> None: def test_explanatory_copy_and_metrics_are_removed_while_price_keys_and_controls_remain(self) -> None:
removed = ( removed = (
"冻结清单已保留,但上次技术生成未完成。" "冻结清单已保留,但上次技术生成未完成。"
"可再次确认重试,不能再修改价格。" "可再次确认重试,不能再修改价格。"
@@ -46,7 +46,19 @@ class DailyPriceReviewUiContractTests(unittest.TestCase):
self.assertNotIn(removed, self.html) self.assertNotIn(removed, self.html)
self.assertNotIn(removed, self.script) self.assertNotIn(removed, self.script)
self.assertNotIn(removed, self.i18n) self.assertNotIn(removed, self.i18n)
self.assertIn("reviewCopy.hidden = retryable;", self.script) for element_id in ("daily-price-review-copy", "daily-price-review-completed", "daily-price-review-pending"):
self.assertNotIn(f'id="{element_id}"', self.html)
self.assertNotIn(f'$("#{element_id}")', self.script)
start = self.html.index('class="daily-price-review-table"')
end = self.html.index("</thead>", start)
heading = self.html[start:end]
self.assertEqual(heading.count('<th scope="col">'), 5)
for key in ("公司", "Rate Code", "Opera 价", "人工价格", "操作"):
self.assertIn(key, heading)
for key in ("固定价候选", "影响记录", "房数 / 房晚"):
self.assertNotIn(key, heading)
self.assertIn('data-daily-price-review-save', self.script)
self.assertIn('aria-describedby="daily-price-review-message"', self.script)
def test_finalize_sets_upload_card_to_generating_before_request(self) -> None: def test_finalize_sets_upload_card_to_generating_before_request(self) -> None:
start = self.script.index("async function finalizeDailyPriceReview()") start = self.script.index("async function finalizeDailyPriceReview()")
+7 -2
View File
@@ -47,7 +47,7 @@ class DailyVisualStaticContractTests(unittest.TestCase):
overview_markup, overview_markup,
) )
self.assertIn( self.assertIn(
".daily-overview { display: grid; grid-template-columns: minmax(220px, 1.15fr) minmax(230px, 1.2fr) minmax(155px, .8fr) minmax(125px, .65fr) minmax(220px, 1.15fr);", ".daily-overview { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.25fr) minmax(0, .85fr) minmax(0, .7fr) minmax(0, 1.2fr);",
self.styles, self.styles,
) )
self.assertIn(".daily-overview > .metric-card", self.styles) self.assertIn(".daily-overview > .metric-card", self.styles)
@@ -61,7 +61,7 @@ class DailyVisualStaticContractTests(unittest.TestCase):
self.assertIn(".daily-overview { grid-template-columns: 1fr;", self.styles) self.assertIn(".daily-overview { grid-template-columns: 1fr;", self.styles)
self.assertIn("@media (min-width: 701px)", self.styles) self.assertIn("@media (min-width: 701px)", self.styles)
self.assertIn(".daily-overview .dropzone {", self.styles) self.assertIn(".daily-overview .dropzone {", self.styles)
self.assertIn("grid-template-columns: 28px minmax(0, 1fr)", self.styles) self.assertIn("grid-template-columns: 20px minmax(0, 1fr)", self.styles)
self.assertIn("min-height: 72px", self.styles) self.assertIn("min-height: 72px", self.styles)
self.assertIn(".daily-overview .upload-footer", self.styles) self.assertIn(".daily-overview .upload-footer", self.styles)
self.assertIn("min-height: 44px", self.styles) self.assertIn("min-height: 44px", self.styles)
@@ -69,6 +69,7 @@ class DailyVisualStaticContractTests(unittest.TestCase):
def test_upload_panel_has_compact_action_footer(self) -> None: def test_upload_panel_has_compact_action_footer(self) -> None:
self.assertIn('class="panel-heading upload-heading"', self.html) self.assertIn('class="panel-heading upload-heading"', self.html)
self.assertIn('class="upload-content"', self.html)
self.assertIn('class="upload-footer"', self.html) self.assertIn('class="upload-footer"', self.html)
self.assertIn(".upload-panel .primary-button", self.styles) self.assertIn(".upload-panel .primary-button", self.styles)
self.assertIn("min-width: 118px", self.styles) self.assertIn("min-width: 118px", self.styles)
@@ -152,6 +153,10 @@ class DailyVisualStaticContractTests(unittest.TestCase):
self.assertIn('<h2 id="arr-download-pending-title">需处理日期</h2>', self.html) self.assertIn('<h2 id="arr-download-pending-title">需处理日期</h2>', self.html)
self.assertNotIn('<h2 id="daily-price-review-title">', self.html) self.assertNotIn('<h2 id="daily-price-review-title">', self.html)
self.assertNotIn('<h2 id="arr-data-review-title">', self.html) self.assertNotIn('<h2 id="arr-data-review-title">', self.html)
self.assertNotIn('id="arr-data-review-copy"', self.html)
self.assertNotIn('id="arr-data-review-progress"', self.html)
self.assertNotIn('id="arr-download-data-review"', self.html)
self.assertIn('id="arr-download-button" type="submit"', self.html)
def test_removed_upload_labels_do_not_return(self) -> None: def test_removed_upload_labels_do_not_return(self) -> None:
upload_start = self.html.index('class="panel upload-panel"') upload_start = self.html.index('class="panel upload-panel"')