Simplify daily review UI and compact header cards
This commit is contained in:
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.
|
||||||
@@ -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时不显示该按钮。删除前页面会说明影响:
|
||||||
|
|
||||||
- 删除当前有效日报:该日从月报中移除,系统自动生成剩余日期的月报;整月已无有效日报时,撤下该月报。旧日报不会自动恢复。
|
- 删除当前有效日报:该日从月报中移除,系统自动生成剩余日期的月报;整月已无有效日报时,撤下该月报。旧日报不会自动恢复。
|
||||||
- 删除历史版本或待价格核对版本:保留当前有效日报,月报不变。运行中的版本须等任务停止后再删除。
|
- 删除历史版本或待价格核对版本:保留当前有效日报,月报不变。运行中的版本须等任务停止后再删除。
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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", "บันทึก"],
|
||||||
|
|||||||
@@ -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
@@ -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; }
|
||||||
|
|||||||
@@ -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});
|
||||||
|
|||||||
@@ -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,/生成|报表已完成/);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}))];
|
||||||
|
|||||||
@@ -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()")
|
||||||
|
|||||||
@@ -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"')
|
||||||
|
|||||||
Reference in new issue
Block a user