Combine report field and price checks into one review workspace
This commit is contained in:
1 parent
2888d6edbc
commit
698d91367c
8 files changed
+273
-87
No files matched your search
@@ -289,3 +289,13 @@ Read: memory-index, project-positioning, current-state latest September sections
|
||||
- Confirmed actual acquisition, not a demonstrated frozen page: authenticated local API and read-only queue show new9/15 request6430210d593fc4754dcb8bc292d6c6e3 downloading, started2026-10-08T14:55:58Z. Saved search has85 reservations and hasMore=false. Capture responses advanced from135 at15:01:21Z to157 at15:02:20Z to175 at15:03:04Z; at15:03:06Z completed58 reservation details,58 rate queries and58 profile reads plus initial search, all175 upstream200, no transport failures. Median response2.17s and latest20 mean2.43s; collection is sequential with final source recheck, explaining minutes of sustained download. No source result/final handoff yet. Queue updated_at records the phase start rather than a progress heartbeat, so its unchanged value is not evidence of a stall.
|
||||
- Read-only frontend audit: successful task GET returning downloading correctly preserves that message. A separate unresolved-fetch weakness can hold loading forever (no API timeout); agent reproduced it with an isolated memory-only harness, but it is not the demonstrated cause here because saved acquisition is actively advancing. Record as a future resilience candidate, not a fix claim. Chrome computer-control surface is unavailable, so no claim of directly inspecting the user's Chrome page or its network requests. Backend acquisition evidence is independent of browser.
|
||||
- Outcome/boundaries: explain85 reservations and sequential supplemental reads, with observed progress rather than claiming success or exact completion time. Existing active task left running without restart, retry, new Oracle request initiated by this investigation, cancellation, price/report mutation or code changes. No automated follow-up scheduled. No regression suite needed for a read-only investigation; project-document boundary/structure and whitespace checks apply. Promotion candidate for integration-owned product follow-up: expose completed/total acquisition progress and bound local status-query waits; no business-rule change or new authorization is implied.
|
||||
|
||||
## Same-task Follow-up: Unified Manual Check Layout
|
||||
|
||||
- User requests moving pending dates below the former field-completion heading, renaming that date area “需处理日期”, and combining field completion/price review under “人工核对”. Concurrent Task Gate Passed for same task/feature/codex/owned checkout/branch/base, no peers and no unknown edits. Project Context Loaded: retained required project memory, ADR004/006/007, architecture/domain/evidence/reflection/commitments/stale context; refreshed active task and planning entry/gate. Goal: one date selector area and one manual-check workspace while preserving independent date requests and original field/price rules. Modules: static index/styles/app/i18n and existing JS/UI checks. Constraints: no Oracle acquisition or business value changes, no resets/finalization of real tasks, preserve source drafts and unsaved-price safeguards. Design skill loaded; preserve the existing design, adapting only grouping/labels, responsive spacing and focus. Planning Gate Passed.
|
||||
- Plan: remove pending-date controls from the download card and create a dedicated “需处理日期” section preceding a shared “人工核对” workspace. Retain field and price content as distinct conditional bodies inside that workspace with explicit subtype guidance, one visible parent heading, existing controls/API/IDs and audited state. Validate source↔price switching, existing guards and empty/terminal states, then verify actual local page and screenshot without entering real prices or confirming generation. Static assets can activate without restarting a service or disturbing active acquisition.
|
||||
- Outcome: independent “需处理日期” card now occupies the former field-review position below the overview. One “人工核对” parent contains mutually exclusive field-completion and price-review bodies, with current date/type guidance and one visible primary heading. Existing field/price IDs, save/confirm/cancel/pagination and audited endpoints remain. Opening either kind scrolls to the unified heading; cached field→history price→same field correctly restores the field body. Context switches clear the shared mode and cached price display, and late price reads cannot repopulate the old context. No source/pricing/Finance business rule or input value changed.
|
||||
- Verification:74 related JavaScript cases passed, including4 new shared-workspace cases and strengthened existing cross-month assertions; independent read-only audit reran47 field/review JS cases and found no new blocker.12 existing static UI cases passed, including a new hierarchy check proving the dates sit outside the download form/conditional bodies and both review bodies share the parent. Final root rerun of those12 passed after the CSS correction; node syntax, whitespace and documentation gates passed. Meaningful state checks cover history entry, cached return, terminal hiding while other dates remain, source-context recovery and unsaved0 protection. Existing same-page price redraw/language-change draft limitations are not claimed resolved by this layout change.
|
||||
- Actual local UI: reloaded only the independent verification tab, then selected9/17 fields→history9/16 prices→same cached9/17 fields→9/15 fields→9/16 prices. At each step the date/type matched and only one review body was visible.9/15 retains its1 missing-room item,9/16 retains its one blank price key0/1,9/17 retains0 active field items; no save, cancellation or generation was triggered. All3 complete review objects match the pre-layout authenticated baseline exactly. Private before/after JSON evidence and screenshots are in `production-validation-20261007/unified-manual-check-{before,after}-20261008.json` and `unified-manual-check-layout-20261008.png`; raw material stays outside Git.
|
||||
- Responsive result: observed CSS widths375/767/1024/1440 verified. Initial767 check found7px page overflow from the absolute screen-reader label inside the price table; setting the new parent to position:relative bounds that label, and final375/767 checks now have scrollWidth equal clientWidth. Wide tables retain their existing contained scrolling. Temporary viewport overrides were reset. The preserved visual palette/typography was preferred over the design search's unrelated liquid-glass/marketing recommendations. Existing user tabs were not reloaded; result tab retained. Static changes are served by the running8875 instance, without a service restart, new acquisition, hotel write, real review mutation, report generation or remote push.
|
||||
- Promotion candidate: integration-owned product/data-flow docs should describe independent “需处理日期” navigation followed by a single “人工核对” workspace, keeping source completeness and price decisions as distinct business stages. No conflict with ADR006/007 or additional business-policy confirmation. Remaining action for users is refreshing an existing page to load the new layout, then continuing original pending tasks.
|
||||
+45
-10
@@ -26,6 +26,7 @@
|
||||
arrDownloadUsername: "",
|
||||
arrDownloadContextId: "",
|
||||
arrDownloadPendingReviews: [],
|
||||
manualReviewMode: "",
|
||||
arrDataReview: null,
|
||||
arrDataReviewRequestId: "",
|
||||
arrDataReviewLoading: false,
|
||||
@@ -391,10 +392,12 @@
|
||||
$("#arr-download-date").value = task.report_date;
|
||||
await acceptARRDownloadTask(task, { sync: false });
|
||||
if (arrDownloadNeedsDataReview()) {
|
||||
state.manualReviewMode = "fields";
|
||||
state.dailyPriceReview = null;
|
||||
state.dailyPriceReviewJobId = "";
|
||||
renderARRDataReview();
|
||||
renderDailyPriceReview(null);
|
||||
$("#arr-data-review-panel").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
$("#manual-review-workspace").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
$("#arr-data-review-panel").focus({ preventScroll: true });
|
||||
} else if (task.job_id && ["needs_review", "succeeded", "failed"].includes(task.status)) {
|
||||
state.jobsMonth = task.report_date.slice(0, 7);
|
||||
@@ -577,7 +580,13 @@
|
||||
state.arrDownloadDisconnected = false;
|
||||
state.arrDownloadSynced = "";
|
||||
state.arrDownloadPendingReviews = [];
|
||||
state.manualReviewMode = "";
|
||||
state.dailyPriceReview = null;
|
||||
state.dailyPriceReviewJobId = "";
|
||||
state.dailyPriceReviewOffset = 0;
|
||||
state.dailyPriceReviewLoading = false;
|
||||
resetARRDataReview({ clearDrafts: true });
|
||||
renderDailyPriceReview(null);
|
||||
}
|
||||
const authoritativePendingReviews = Array.isArray(config.pending_reviews);
|
||||
const pendingReviews = authoritativePendingReviews ? config.pending_reviews : config.pending_data_reviews;
|
||||
@@ -731,11 +740,29 @@
|
||||
node.classList.toggle("is-error", isError);
|
||||
}
|
||||
|
||||
function renderManualReviewWorkspace(priceReview = state.dailyPriceReview) {
|
||||
const fieldsAvailable = Boolean(state.arrDataReviewRequestId && arrDownloadNeedsDataReview()
|
||||
&& state.arrDownloadTask.request_id === state.arrDataReviewRequestId);
|
||||
const pricesAvailable = Boolean(priceReview && ["open", "generation_failed", "processing"].includes(priceReview.case_status));
|
||||
const mode = state.manualReviewMode || (fieldsAvailable ? "fields" : "prices");
|
||||
const showFields = mode === "fields" && fieldsAvailable;
|
||||
const showPrices = mode === "prices" && pricesAvailable;
|
||||
$("#arr-data-review-panel").hidden = !showFields;
|
||||
$("#daily-price-review-panel").hidden = !showPrices;
|
||||
$("#manual-review-workspace").hidden = !showFields && !showPrices;
|
||||
$("#manual-review-title").textContent = I18N.t("manual_review.title");
|
||||
const type = I18N.t(showFields ? "manual_review.fields" : "manual_review.prices");
|
||||
const priceTask = state.arrDownloadTask?.job_id === state.dailyPriceReviewJobId ? state.arrDownloadTask : null;
|
||||
const date = showFields ? state.arrDataReview?.report_date || state.arrDownloadTask?.report_date
|
||||
: priceReview?.business_date || priceReview?.report_date || priceTask?.report_date
|
||||
|| state.jobs.find((job) => job.job_id === state.dailyPriceReviewJobId)?.arrival_date;
|
||||
$("#manual-review-context").textContent = date ? I18N.t("manual_review.context", { date, type }) : type;
|
||||
}
|
||||
|
||||
function renderARRDataReview() {
|
||||
const panel = $("#arr-data-review-panel");
|
||||
const review = state.arrDataReview;
|
||||
panel.hidden = !state.arrDataReviewRequestId || !arrDownloadNeedsDataReview()
|
||||
|| state.arrDownloadTask.request_id !== state.arrDataReviewRequestId;
|
||||
renderManualReviewWorkspace();
|
||||
if (panel.hidden) return;
|
||||
const busy = state.arrDataReviewLoading || state.arrDataReviewMutating;
|
||||
const editable = review?.status === "editing" && !busy && !state.arrDataReviewDisconnected && !state.arrDataReviewFinalizing;
|
||||
@@ -786,12 +813,19 @@
|
||||
|
||||
async function loadARRDataReview(requestId = state.arrDownloadTask?.request_id, { scroll = false, showErrors = true } = {}) {
|
||||
if (!requestId || state.arrDataReviewLoading || !arrDownloadNeedsDataReview()) return null;
|
||||
if (state.manualReviewMode === "prices" && dailyPriceReviewHasUnsavedChanges()) {
|
||||
const message = I18N.t("daily_review.save_before_switch");
|
||||
setDailyPriceReviewMessage(message, true);
|
||||
showToast(message, true);
|
||||
return null;
|
||||
}
|
||||
if (state.arrDataReviewRequestId !== requestId) {
|
||||
resetARRDataReview();
|
||||
state.arrDataReviewRequestId = requestId;
|
||||
state.arrDataReviewDrafts = { ...state.arrDataReviewDraftsByRequest[requestId] };
|
||||
}
|
||||
const contextId = state.arrDownloadContextId;
|
||||
state.manualReviewMode = "fields";
|
||||
state.arrDataReviewLoading = true;
|
||||
renderARRDataReview();
|
||||
try {
|
||||
@@ -808,7 +842,7 @@
|
||||
if (item.confirmed && state.arrDataReviewDrafts[item.item_id] === arrDataReviewInputValue(item)) delete state.arrDataReviewDrafts[item.item_id];
|
||||
});
|
||||
if (scroll) {
|
||||
$("#arr-data-review-panel").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
$("#manual-review-workspace").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
$("#arr-data-review-panel").focus({ preventScroll: true });
|
||||
}
|
||||
return review;
|
||||
@@ -1315,13 +1349,12 @@
|
||||
const panel = $("#daily-price-review-panel");
|
||||
const body = $("#daily-price-review-body");
|
||||
const empty = $("#daily-price-review-empty");
|
||||
renderManualReviewWorkspace(review);
|
||||
if (!review) {
|
||||
panel.hidden = true;
|
||||
body.innerHTML = "";
|
||||
empty.hidden = true;
|
||||
return;
|
||||
}
|
||||
panel.hidden = false;
|
||||
const busy = state.dailyPriceReviewLoading || state.dailyPriceReviewMutating;
|
||||
const editable = dailyPriceReviewEditable(review);
|
||||
const retryable = review.case_status === "generation_failed";
|
||||
@@ -1393,31 +1426,33 @@
|
||||
|
||||
async function loadDailyPriceReview(jobId = state.dailyPriceReviewJobId, { reset = false, scroll = false, showErrors = true } = {}) {
|
||||
if (!jobId || state.dailyPriceReviewLoading) return;
|
||||
state.manualReviewMode = "prices";
|
||||
if (reset || jobId !== state.dailyPriceReviewJobId) {
|
||||
state.dailyPriceReviewJobId = jobId;
|
||||
state.dailyPriceReviewOffset = 0;
|
||||
state.dailyPriceReview = null;
|
||||
}
|
||||
const expectedJobId = state.dailyPriceReviewJobId;
|
||||
const contextId = state.arrDownloadContextId;
|
||||
state.dailyPriceReviewLoading = true;
|
||||
renderDailyPriceReview(state.dailyPriceReview);
|
||||
try {
|
||||
const review = await api(`/api/jobs/${encodeURIComponent(expectedJobId)}/review?limit=${DAILY_PRICE_REVIEW_PAGE_SIZE}&offset=${state.dailyPriceReviewOffset}`);
|
||||
if (state.dailyPriceReviewJobId !== expectedJobId) return;
|
||||
if (state.dailyPriceReviewJobId !== expectedJobId || state.arrDownloadContextId !== contextId) return;
|
||||
state.dailyPriceReview = review;
|
||||
renderDailyPriceReview(review);
|
||||
if (scroll) {
|
||||
const panel = $("#daily-price-review-panel");
|
||||
panel.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
$("#manual-review-workspace").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
panel.focus({ preventScroll: true });
|
||||
}
|
||||
} catch (error) {
|
||||
if (state.dailyPriceReviewJobId === expectedJobId && showErrors) {
|
||||
if (state.dailyPriceReviewJobId === expectedJobId && state.arrDownloadContextId === contextId && showErrors) {
|
||||
setDailyPriceReviewMessage(error.message, true);
|
||||
showToast(error.message, true);
|
||||
}
|
||||
} finally {
|
||||
if (state.dailyPriceReviewJobId === expectedJobId) {
|
||||
if (state.dailyPriceReviewJobId === expectedJobId && state.arrDownloadContextId === contextId) {
|
||||
state.dailyPriceReviewLoading = false;
|
||||
renderDailyPriceReview(state.dailyPriceReview);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"arr_download.needs_data_review": ["等待人工完善报表字段", "Awaiting source field review", "รอตรวจสอบข้อมูลรายงานด้วยตนเอง"],
|
||||
"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 awaiting review", "วันที่รอดำเนินการ"],
|
||||
"arr_download.pending_dates": ["需处理日期", "Dates requiring attention", "วันที่ต้องดำเนินการ"],
|
||||
"arr_download.pending_date": ["待补字段", "Complete fields", "รอกรอกข้อมูล"],
|
||||
"arr_download.pending_price": ["待复核价格", "Review prices", "รอตรวจสอบราคา"],
|
||||
"arr_download.source_failed": ["到店数据获取未完成,请继续原任务重试。", "Arrival data could not be fetched. Resume the original task to retry.", "ยังดึงข้อมูลผู้เข้าพักไม่สำเร็จ โปรดดำเนินงานเดิมต่อเพื่อลองอีกครั้ง"],
|
||||
@@ -147,7 +147,11 @@
|
||||
"arr_download.date_conflict": ["原任务日期不能修改", "The original task date cannot be changed", "ไม่สามารถเปลี่ยนวันที่ของงานเดิม"],
|
||||
"arr_download.retry_unavailable": ["该任务不能继续重试,请查看任务日志", "This task cannot be retried. Check the task log.", "ไม่สามารถลองงานนี้อีกครั้ง โปรดดูบันทึกงาน"],
|
||||
"arr_download.other_active": ["该日期已有进行中的下载任务", "A download for this date is already in progress", "มีงานดาวน์โหลดสำหรับวันที่นี้กำลังดำเนินการอยู่"],
|
||||
"data_review.title": ["人工完善报表字段", "Complete report fields", "ตรวจสอบและกรอกข้อมูลรายงาน"],
|
||||
"manual_review.title": ["人工核对", "Manual review", "ตรวจสอบด้วยตนเอง"],
|
||||
"manual_review.fields": ["完善报表字段", "Complete report fields", "กรอกข้อมูลรายงานให้ครบ"],
|
||||
"manual_review.prices": ["价格复核", "Review prices", "ตรวจสอบราคา"],
|
||||
"manual_review.context": ["{date} · {type}", "{date} · {type}", "{date} · {type}"],
|
||||
"data_review.title": ["完善报表字段", "Complete report fields", "ตรวจสอบและกรอกข้อมูลรายงาน"],
|
||||
"data_review.copy": ["请按各项提示核实来源与原处理规则。可空字段不必补成有值;核实无此信息后,可明确确认留空。全部确认后可生成日报。", "Check each field's source and original processing requirement. Optional fields need no value when none exists; explicitly confirm this after verification. Generate the daily report after all items are confirmed.", "ตรวจสอบแหล่งข้อมูลและข้อกำหนดเดิมของแต่ละช่อง ช่องที่เว้นว่างได้ไม่จำเป็นต้องกรอกค่า หากตรวจสอบแล้วว่าไม่มีข้อมูล ให้ยืนยันการเว้นว่าง สร้างรายงานรายวันได้เมื่อยืนยันครบทุกช่อง"],
|
||||
"data_review.progress": ["{date} · 已确认 {completed} / {total} 项", "{date} · {completed} of {total} confirmed", "{date} · ยืนยันแล้ว {completed} / {total} รายการ"],
|
||||
"data_review.reservation": ["预订信息", "Reservation", "ข้อมูลการจอง"],
|
||||
@@ -250,7 +254,7 @@
|
||||
"daily_review.open": ["打开人工价格复核,已填写 {completed} / {total}", "Open manual price review, {completed} of {total} completed", "เปิดการตรวจสอบราคาด้วยตนเอง กรอกแล้ว {completed} / {total}"],
|
||||
"daily_review.progress_short": ["复核 {completed} / {total}", "Review {completed} / {total}", "ตรวจสอบ {completed} / {total}"],
|
||||
"daily_review.step": ["价格复核 / 待处理", "PRICE REVIEW / REQUIRED", "ตรวจสอบราคา / ต้องดำเนินการ"],
|
||||
"daily_review.title": ["人工价格复核", "Manual price review", "ตรวจสอบราคาด้วยตนเอง"],
|
||||
"daily_review.title": ["价格复核", "Review prices", "ตรวจสอบราคา"],
|
||||
"daily_review.default_instruction": ["请填写每晚处理价,可填 0。每项填写后点击“暂存”,全部暂存后生成日报。相同价格项只需填写一次。", "Enter the nightly price; 0 is allowed. Save each item, then generate the daily report when all items are saved. Enter each identical price key only once.", "กรอกราคาต่อคืน สามารถกรอก 0 ได้ กดบันทึกแต่ละรายการ แล้วสร้างรายงานรายวันเมื่อบันทึกครบทุกรายการ คีย์ราคาเดียวกันกรอกเพียงครั้งเดียว"],
|
||||
"daily_review.processing_instruction": ["正在使用已冻结的价格清单重放原始 XML;此时无法修改。", "The original XML is being replayed with the frozen price list. Prices cannot be changed now.", "กำลังประมวลผล XML ต้นทางซ้ำด้วยรายการราคาที่ตรึงไว้ ขณะนี้ไม่สามารถแก้ไขราคาได้"],
|
||||
"daily_review.progress_aria": ["人工价格复核进度", "Manual price review progress", "ความคืบหน้าการตรวจสอบราคาด้วยตนเอง"],
|
||||
@@ -268,7 +272,7 @@
|
||||
"daily_review.rooms_nights": ["房数 / 房晚", "Rooms / room nights", "ห้อง / คืนห้องพัก"],
|
||||
"daily_review.manual_price": ["人工价格", "Manual price", "ราคาที่กำหนดด้วยตนเอง"],
|
||||
"daily_review.price_placeholder": ["请输入价格(可填 0)", "Enter price (0 allowed)", "กรอกราคา (กรอก 0 ได้)"],
|
||||
"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.price_key": ["价格键", "price key", "คีย์ราคา"],
|
||||
"daily_review.manual_price_label": ["{company} {rateCode} 的人工价格", "Manual price for {company} {rateCode}", "ราคาที่กำหนดด้วยตนเองสำหรับ {company} {rateCode}"],
|
||||
|
||||
+65
-59
@@ -74,10 +74,6 @@
|
||||
<span class="arr-download-indicator" aria-hidden="true"></span>
|
||||
<span id="arr-download-status">正在连接下载服务…</span>
|
||||
</div>
|
||||
<div class="arr-download-pending" id="arr-download-pending" hidden>
|
||||
<span id="arr-download-pending-title">待完善日期</span>
|
||||
<div id="arr-download-pending-list"></div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -132,66 +128,76 @@
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel arr-data-review-panel" id="arr-data-review-panel" aria-labelledby="arr-data-review-title" tabindex="-1" hidden>
|
||||
<header class="arr-data-review-header">
|
||||
<div>
|
||||
<h2 id="arr-data-review-title">人工完善报表字段</h2>
|
||||
<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">
|
||||
<p id="arr-data-review-message" role="status" aria-live="polite"></p>
|
||||
<div class="arr-data-review-actions">
|
||||
<button id="arr-data-review-refresh" type="button">刷新字段</button>
|
||||
<button class="primary-button" id="arr-data-review-finalize" type="button" disabled>确认并生成日报</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arr-data-review-scroll" role="region" aria-label="待完善报表字段" tabindex="0">
|
||||
<table class="arr-data-review-table">
|
||||
<thead><tr><th scope="col">预订信息</th><th scope="col">待完善字段</th><th scope="col">核实后的值</th><th scope="col">操作</th></tr></thead>
|
||||
<tbody id="arr-data-review-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="arr-data-review-empty" id="arr-data-review-empty" hidden>没有待完善字段。</p>
|
||||
<section class="panel arr-download-pending" id="arr-download-pending" aria-labelledby="arr-download-pending-title" hidden>
|
||||
<h2 id="arr-download-pending-title">需处理日期</h2>
|
||||
<div id="arr-download-pending-list"></div>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<div class="daily-price-review-title-block">
|
||||
<span class="step-label">价格复核 / 待处理</span>
|
||||
<h2 id="daily-price-review-title">人工价格复核</h2>
|
||||
<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>
|
||||
<section class="panel manual-review-workspace" id="manual-review-workspace" aria-labelledby="manual-review-title" hidden>
|
||||
<header class="manual-review-heading">
|
||||
<h2 id="manual-review-title">人工核对</h2>
|
||||
<p id="manual-review-context" role="status" aria-live="polite"></p>
|
||||
</header>
|
||||
<div class="daily-price-review-toolbar">
|
||||
<p class="daily-price-review-message" id="daily-price-review-message" role="status"></p>
|
||||
<div class="daily-price-review-actions">
|
||||
<button class="daily-price-review-cancel" id="daily-price-review-cancel" type="button">取消本次复核</button>
|
||||
<button class="daily-price-review-finalize" id="daily-price-review-finalize" type="button" disabled>确认并生成日报</button>
|
||||
<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">
|
||||
<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">
|
||||
<p id="arr-data-review-message" role="status" aria-live="polite"></p>
|
||||
<div class="arr-data-review-actions">
|
||||
<button id="arr-data-review-refresh" type="button">刷新字段</button>
|
||||
<button class="primary-button" id="arr-data-review-finalize" type="button" disabled>确认并生成日报</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="daily-price-review-scroll" role="region" aria-label="待人工定价项" tabindex="0">
|
||||
<table class="daily-price-review-table">
|
||||
<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>
|
||||
</thead>
|
||||
<tbody id="daily-price-review-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="daily-price-review-empty" id="daily-price-review-empty" hidden>没有可显示的待人工定价项。</p>
|
||||
<nav class="daily-price-review-pagination" aria-label="人工价格复核分页">
|
||||
<span id="daily-price-review-page-summary">共 0 项</span>
|
||||
<div>
|
||||
<button id="daily-price-review-prev" type="button" disabled>上一页</button>
|
||||
<strong id="daily-price-review-page-label">第 1 / 1 页</strong>
|
||||
<button id="daily-price-review-next" type="button" disabled>下一页</button>
|
||||
<div class="arr-data-review-scroll" role="region" aria-label="待完善报表字段" tabindex="0">
|
||||
<table class="arr-data-review-table">
|
||||
<thead><tr><th scope="col">预订信息</th><th scope="col">待完善字段</th><th scope="col">核实后的值</th><th scope="col">操作</th></tr></thead>
|
||||
<tbody id="arr-data-review-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</nav>
|
||||
<p class="arr-data-review-empty" id="arr-data-review-empty" hidden>没有待完善字段。</p>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<p class="daily-price-review-message" id="daily-price-review-message" role="status"></p>
|
||||
<div class="daily-price-review-actions">
|
||||
<button class="daily-price-review-cancel" id="daily-price-review-cancel" type="button">取消本次复核</button>
|
||||
<button class="daily-price-review-finalize" id="daily-price-review-finalize" type="button" disabled>确认并生成日报</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="daily-price-review-scroll" role="region" aria-label="待人工定价项" tabindex="0">
|
||||
<table class="daily-price-review-table">
|
||||
<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>
|
||||
</thead>
|
||||
<tbody id="daily-price-review-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="daily-price-review-empty" id="daily-price-review-empty" hidden>没有可显示的待人工定价项。</p>
|
||||
<nav class="daily-price-review-pagination" aria-label="人工价格复核分页">
|
||||
<span id="daily-price-review-page-summary">共 0 项</span>
|
||||
<div>
|
||||
<button id="daily-price-review-prev" type="button" disabled>上一页</button>
|
||||
<strong id="daily-price-review-page-label">第 1 / 1 页</strong>
|
||||
<button id="daily-price-review-next" type="button" disabled>下一页</button>
|
||||
</div>
|
||||
</nav>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="panel table-panel" aria-labelledby="daily-history-title">
|
||||
|
||||
@@ -201,16 +201,21 @@ button { color: inherit; }
|
||||
.arr-download-actions .primary-button { min-height: 34px; margin: 0; padding: 0 12px; border-radius: 8px; font-size: 11px; }
|
||||
.arr-download-review { padding: 4px 0; border: 0; color: var(--blue-dark); background: transparent; font-size: 11px; font-weight: 650; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.arr-download-review:hover { color: var(--blue); }
|
||||
.arr-download-pending { margin: 10px 0 12px; }
|
||||
.arr-download-pending { margin: 0 0 20px; padding: 20px; }
|
||||
.arr-download-pending[hidden] { display: none; }
|
||||
.arr-download-pending > span { display: block; margin-bottom: 5px; color: var(--muted); font-size: 11px; }
|
||||
#arr-download-pending-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 110px; overflow-y: auto; }
|
||||
#arr-download-pending-list button { padding: 5px 8px; border: 1px solid var(--line); border-radius: 6px; color: var(--blue-dark); background: #fff; font: inherit; font-size: 11px; cursor: pointer; }
|
||||
.arr-download-pending h2 { margin: 0 0 12px; font-size: 18px; line-height: 1.3; letter-spacing: -.025em; }
|
||||
#arr-download-pending-list { display: flex; flex-wrap: wrap; gap: 8px; max-height: 160px; overflow-y: auto; }
|
||||
#arr-download-pending-list button { min-height: 38px; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; color: var(--blue-dark); background: #fff; font: inherit; font-size: 12px; cursor: pointer; }
|
||||
#arr-download-pending-list button[aria-pressed="true"] { border-color: var(--blue); background: #f0f4ff; }
|
||||
#arr-download-pending-list button:disabled { opacity: .55; cursor: default; }
|
||||
.arr-data-review-panel { margin: 20px 0; border-color: #e6d4b5; overflow: hidden; }
|
||||
.manual-review-workspace { position: relative; margin: 0 0 20px; overflow: hidden; }
|
||||
.manual-review-workspace[hidden] { display: none; }
|
||||
.manual-review-heading { padding: 20px 20px 16px; border-bottom: 1px solid var(--line); }
|
||||
.manual-review-heading h2 { margin: 0; font-size: 18px; line-height: 1.3; letter-spacing: -.025em; }
|
||||
.manual-review-heading p { margin: 7px 0 0; color: var(--blue-dark); font-size: 12px; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
||||
.arr-data-review-panel { margin: 0; overflow: hidden; }
|
||||
.arr-data-review-panel[hidden] { display: none; }
|
||||
.arr-data-review-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 20px; background: #fffcf5; }
|
||||
.arr-data-review-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 16px 20px; background: var(--surface-soft); }
|
||||
.arr-data-review-header h2 { margin: 0 0 6px; font-size: 17px; }
|
||||
.arr-data-review-header p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
|
||||
#arr-data-review-progress { flex-shrink: 0; color: var(--amber); font-variant-numeric: tabular-nums; }
|
||||
@@ -397,13 +402,14 @@ tbody tr:hover td { background: #fafcff; }
|
||||
.job-review-link:hover .job-review-progress { text-decoration-color: currentColor; }
|
||||
.job-review-link:hover .status-chip { box-shadow: inset 0 0 0 1px #e5ba63; }
|
||||
|
||||
.daily-price-review-panel { margin: 0 0 20px; overflow: hidden; scroll-margin-top: 18px; border: 1px solid #f1d49c; border-radius: 16px; background: #fff; box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 14px 32px rgba(146, 93, 13, .08); }
|
||||
.daily-price-review-panel { margin: 0; overflow: hidden; scroll-margin-top: 18px; }
|
||||
.daily-price-review-panel[hidden] { display: none; }
|
||||
.daily-price-review-panel:focus-visible { outline: 3px solid rgba(168, 90, 10, .2); outline-offset: 3px; }
|
||||
.daily-price-review-header { padding: 18px 20px 16px; display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 1px solid #f5e4bf; background: linear-gradient(100deg, #fffaf0, #fff); }
|
||||
.daily-price-review-header { padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 1px solid var(--line); background: var(--surface-soft); }
|
||||
.daily-price-review-title-block { min-width: 0; flex: 1 1 auto; display: grid; gap: 5px; }
|
||||
.daily-price-review-title-block .step-label { color: #a85a0a; }
|
||||
.daily-price-review-title-block h2 { margin: 0; color: #3c280e; font-size: 18px; letter-spacing: -.025em; }
|
||||
.daily-price-review-title-block p { margin: 0; color: #7a5a2c; font-size: 11px; line-height: 1.5; }
|
||||
.daily-price-review-title-block p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
|
||||
.daily-price-review-metrics { margin: 0; display: grid; grid-template-columns: repeat(2, minmax(78px, 1fr)); gap: 8px; }
|
||||
.daily-price-review-metrics > div { min-width: 78px; padding: 8px 10px; border: 1px solid #ead9b4; border-radius: 9px; background: rgba(255, 255, 255, .82); }
|
||||
.daily-price-review-metrics > div.is-review { border-color: #f0c970; background: #fff7e5; }
|
||||
|
||||
@@ -11,7 +11,7 @@ const review=(items,revision=1)=>({request_id:requestId,report_date:task.report_
|
||||
const plain=value=>JSON.parse(JSON.stringify(value));
|
||||
const priceTask={request_id:'c'.repeat(32),report_date:'2026-09-16',status:'needs_review',
|
||||
job_id:'fixture-price-september',can_retry:false};
|
||||
const priceReview=(price=null)=>({case_id:'fixture-price-case',case_status:'open',revision:1,
|
||||
const priceReview=(price=null)=>({case_id:'fixture-price-case',case_status:'open',business_date:priceTask.report_date,revision:1,
|
||||
completed_items:price===null?0:1,total_items:1,
|
||||
items:[{item_id:1,company_key:'Fixture Company',rate_code:'FIXTURE',effective_rate_amount:100,
|
||||
real_price:price,affected_records:1,affected_rooms:1,affected_room_nights:2,candidate_prices:[]}],
|
||||
@@ -48,6 +48,9 @@ test('cross-month field and price dates restore, stay clickable after polling, a
|
||||
assert.equal(h.state.dailyPriceReviewJobId,priceTask.job_id);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,false);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,false);
|
||||
assert.equal(h.element('#manual-review-context').textContent,'2026-09-16 · manual_review.prices');
|
||||
assert.equal(h.element('#arr-download-pending').hidden,false);
|
||||
assert.equal(h.element('#arr-download-date').value,'2026-09-16');
|
||||
assert.doesNotMatch(h.element('#arr-download-pending-list').innerHTML,/ disabled/);
|
||||
await h.loadARRDownloadTask();
|
||||
@@ -56,10 +59,94 @@ test('cross-month field and price dates restore, stay clickable after polling, a
|
||||
await h.selectARRPendingReview(requestId);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,true);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,false);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,false);
|
||||
assert.equal(h.element('#manual-review-context').textContent,'2026-10-07 · manual_review.fields');
|
||||
assert.equal(h.element('#arr-download-pending').hidden,false);
|
||||
assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'OCTOBER-DRAFT');
|
||||
assert.equal(h.calls.every(call=>call.method==='GET'),true,'restoring reviews never downloads or changes prices');
|
||||
});
|
||||
|
||||
test('history price entry uses the same workspace and keeps field drafts while switching bodies',async()=>{
|
||||
const h=harness(url=>pendingAPI(url));
|
||||
h.state.arrDownloadPendingReviews=[task,priceTask];
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
h.trackARRDataReviewDraft(h.row('1:BLOCK_CODE','FIELD-DRAFT').input);
|
||||
await h.openDailyPriceReview(priceTask.job_id,false);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,false);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,false);
|
||||
assert.equal(h.element('#manual-review-context').textContent,'2026-09-16 · manual_review.prices');
|
||||
assert.equal(h.element('#arr-download-pending').hidden,false);
|
||||
h.renderARRDataReview();
|
||||
h.renderDailyPriceReview();
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true,'routine renders cannot switch the selected body');
|
||||
await h.selectARRPendingReview(requestId);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,false);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,true);
|
||||
assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'FIELD-DRAFT');
|
||||
assert.equal(h.calls.every(call=>call.method==='GET'),true);
|
||||
});
|
||||
|
||||
test('terminal reviews hide the workspace without hiding other dates requiring attention',async()=>{
|
||||
const h=harness(url=>pendingAPI(url));
|
||||
h.state.arrDownloadPendingReviews=[task,priceTask];
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
await h.openDailyPriceReview(priceTask.job_id,false);
|
||||
for(const case_status of ['completed','cancelled','failed']) {
|
||||
h.state.dailyPriceReview={...priceReview(),case_status};
|
||||
h.renderDailyPriceReview();
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,true);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,true);
|
||||
assert.equal(h.element('#arr-download-pending').hidden,false);
|
||||
}
|
||||
h.state.arrDownloadTask={...task,status:'succeeded'};
|
||||
h.state.manualReviewMode='fields';
|
||||
h.renderARRDataReview();
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,true);
|
||||
});
|
||||
|
||||
test('opening fields directly cannot hide an unsaved price edit',async()=>{
|
||||
const h=harness(url=>pendingAPI(url));
|
||||
await h.acceptARRDownloadTask(task,{sync:false});
|
||||
await h.openDailyPriceReview(priceTask.job_id,false);
|
||||
const input={value:'0',closest:()=>({dataset:{dailyPriceReviewItemId:'1'}})};
|
||||
h.element('#daily-price-review-body').querySelectorAll=()=>[input];
|
||||
const count=h.calls.length;
|
||||
await h.loadARRDataReview();
|
||||
assert.equal(h.calls.length,count);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,false);
|
||||
assert.equal(h.element('#arr-data-review-panel').hidden,true);
|
||||
assert.equal(input.value,'0');
|
||||
});
|
||||
|
||||
test('a source context change clears the workspace and ignores an older price response',async()=>{
|
||||
let finishPrice;
|
||||
const h=harness(url=>{
|
||||
if(url.includes('/review?')) return new Promise(resolve=>{finishPrice=()=>resolve(response(200,priceReview()));});
|
||||
if(url==='/api/arr-downloads') return response(200,{context_id:'new-context',ready:true,
|
||||
default_date:'2026-09-17',pending_reviews:[],latest_task:null});
|
||||
throw new Error('unexpected request '+url);
|
||||
});
|
||||
h.state.arrDownloadContextId='old-context';
|
||||
h.state.arrDownloadStorageKey=scopedKey('old-context');
|
||||
h.state.dailyPriceReview=priceReview();
|
||||
h.state.dailyPriceReviewJobId=priceTask.job_id;
|
||||
h.state.dailyPriceReviewOffset=50;
|
||||
const loading=h.loadDailyPriceReview();
|
||||
await h.initARRDownload();
|
||||
assert.equal(h.state.manualReviewMode,'');
|
||||
assert.equal(h.state.dailyPriceReview,null);
|
||||
assert.equal(h.state.dailyPriceReviewJobId,'');
|
||||
assert.equal(h.state.dailyPriceReviewOffset,0);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,true);
|
||||
finishPrice();
|
||||
await loading;
|
||||
assert.equal(h.state.dailyPriceReview,null);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,true);
|
||||
assert.equal(h.calls.every(call=>call.method==='GET'),true);
|
||||
});
|
||||
|
||||
test('an authoritative empty pending list overrides legacy and stale current review state',async()=>{
|
||||
let cleared=false;
|
||||
const h=harness(url=>url==='/api/arr-downloads'
|
||||
@@ -96,6 +183,7 @@ for(const status of ['succeeded','failed']) {
|
||||
assert.equal(h.state.arrDownloadTask.status,status);
|
||||
assert.equal(h.state.arrDownloadPendingReviews.length,0);
|
||||
assert.equal(h.element('#arr-download-pending').hidden,true);
|
||||
assert.equal(h.element('#manual-review-workspace').hidden,true);
|
||||
assert.equal(h.element('#daily-price-review-panel').hidden,true);
|
||||
assert.equal(h.calls.some(call=>call.url.includes('/review?')),false);
|
||||
assert.equal(h.calls.every(call=>call.method==='GET'),true);
|
||||
@@ -148,11 +236,13 @@ test('pending date labels have Chinese, English and Thai translations',()=>{
|
||||
const context=vm.createContext({window:{},document:{readyState:'loading',addEventListener(){}}});
|
||||
vm.runInContext(fs.readFileSync(path.resolve(__dirname,'../../arr_web/static/i18n.js'),'utf8'),context);
|
||||
const catalog=context.window.ARRI18n.CATALOG;
|
||||
for(const key of ['arr_download.pending_dates','arr_download.pending_date','arr_download.pending_price','daily_review.save_before_switch']) {
|
||||
for(const key of ['arr_download.pending_dates','arr_download.pending_date','arr_download.pending_price','daily_review.save_before_switch',
|
||||
'manual_review.title','manual_review.fields','manual_review.prices','manual_review.context']) {
|
||||
assert.equal(catalog[key].length,3);
|
||||
catalog[key].forEach(text=>assert.equal(typeof text==='string' && text.length>0,true));
|
||||
}
|
||||
assert.equal(catalog['arr_download.pending_dates'][0],'待处理日期');
|
||||
assert.equal(catalog['arr_download.pending_dates'][0],'需处理日期');
|
||||
assert.equal(catalog['manual_review.title'][0],'人工核对');
|
||||
assert.equal(catalog['arr_download.pending_price'][0],'待复核价格');
|
||||
});
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ function harness(respond) {
|
||||
};
|
||||
const context=vm.createContext({Headers, console, Date, Intl, Uint8Array,
|
||||
document:{hidden:false,querySelector:element},
|
||||
window:{ARRI18n:{t:key=>key,text:value=>value,errorMessage:code=>code,formatInteger:value=>String(value),formatMonth:value=>value},crypto:webcrypto,
|
||||
window:{ARRI18n:{t:(key,values)=>key==='manual_review.context'?`${values.date} · ${values.type}`:key,text:value=>value,errorMessage:code=>code,formatInteger:value=>String(value),formatMonth:value=>value},crypto:webcrypto,
|
||||
setTimeout(){return 1;},clearTimeout(){},location:{replace(){}}},
|
||||
localStorage:{setItem:(k,v)=>storage.set(k,v),getItem:k=>{storageReads.push(k);return storage.get(k);},removeItem:k=>storage.delete(k)},
|
||||
fetch:async (url, options) => {calls.push({url,method:options.method||'GET',body:options.body,csrf:options.headers.get('X-ARR-CSRF')});return respond(url,options,calls);},
|
||||
});
|
||||
const exports='state, api, submitARRDownload, rememberARRIntent, acceptARRDownloadTask, handleARRDownloadDateChange, initARRDownload, loadARRDownloadTask, loadARRDataReview, renderARRDataReview, arrDataReviewCanFinalize, saveARRDataReviewItem, finalizeARRDataReview, trackARRDataReviewDraft, parseARRDataReviewValue, selectARRPendingReview, strictReviewPrice, reviewPriceInput, renderDailyPriceReview, saveDailyPriceReviewItem, loadDailyPriceReview, dailyPriceReviewCanFinalize, dailyPriceReviewHasUnsavedChanges, finalizeDailyPriceReview, confirmDailyPriceReviewCancel';
|
||||
const exports='state, api, submitARRDownload, rememberARRIntent, acceptARRDownloadTask, handleARRDownloadDateChange, initARRDownload, loadARRDownloadTask, loadARRDataReview, renderARRDataReview, arrDataReviewCanFinalize, saveARRDataReviewItem, finalizeARRDataReview, trackARRDataReviewDraft, parseARRDataReviewValue, selectARRPendingReview, strictReviewPrice, reviewPriceInput, renderDailyPriceReview, saveDailyPriceReviewItem, loadDailyPriceReview, openDailyPriceReview, dailyPriceReviewCanFinalize, dailyPriceReviewHasUnsavedChanges, finalizeDailyPriceReview, confirmDailyPriceReviewCancel';
|
||||
vm.runInContext(source.replace(boot,` globalThis.subject = {${exports}};\n})();`),context);
|
||||
const subject=context.subject;
|
||||
Object.assign(subject.state,{arrDownloadReady:true,arrDownloadLoaded:true,arrDownloadStorageKey:'test',arrDownloadUsername:'operator',csrf:'fixture'});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -72,6 +73,40 @@ class DailyVisualStaticContractTests(unittest.TestCase):
|
||||
self.assertIn("min-width: 118px", self.styles)
|
||||
self.assertNotIn("width: calc(100% - 48px)", self.styles)
|
||||
|
||||
def test_review_dates_are_independent_and_both_review_bodies_share_one_workspace(self) -> None:
|
||||
class Regions(HTMLParser):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.stack = []
|
||||
self.parents = {}
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
element_id = dict(attrs).get("id")
|
||||
if element_id:
|
||||
self.parents[element_id] = [item[1] for item in self.stack if item[1]]
|
||||
if tag not in {"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"}:
|
||||
self.stack.append((tag, element_id))
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
for index in range(len(self.stack) - 1, -1, -1):
|
||||
if self.stack[index][0] == tag:
|
||||
del self.stack[index:]
|
||||
break
|
||||
|
||||
regions = Regions()
|
||||
regions.feed(self.html)
|
||||
dates = regions.parents["arr-download-pending"]
|
||||
self.assertNotIn("arr-download-form", dates)
|
||||
self.assertNotIn("arr-data-review-panel", dates)
|
||||
self.assertNotIn("manual-review-workspace", dates)
|
||||
for body_id in ("arr-data-review-panel", "daily-price-review-panel"):
|
||||
self.assertIn("manual-review-workspace", regions.parents[body_id])
|
||||
self.assertLess(self.html.index('id="arr-download-pending"'), self.html.index('id="manual-review-workspace"'))
|
||||
self.assertEqual(self.html.count('<h2 id="manual-review-title">人工核对</h2>'), 1)
|
||||
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="arr-data-review-title">', self.html)
|
||||
|
||||
def test_removed_upload_labels_do_not_return(self) -> None:
|
||||
upload_start = self.html.index('class="panel upload-panel"')
|
||||
upload_end = self.html.index("</section>", upload_start)
|
||||
|
||||
Reference in new issue
Block a user