Combine report field and price checks into one review workspace

This commit is contained in:
Wyndham ARR committed 2026-10-08 23:26:59 +08:00
1 parent 2888d6edbc
commit 698d91367c
8 files changed
+273 -87

No files matched your search

+93 -3
View File
@@ -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],'待复核价格');
});
+2 -2
View File
@@ -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'});