feat: add read-first Week Ahead overview (Closes #1216)
This commit is contained in:
parent
45e2798d37
commit
42e97b4a79
|
|
@ -167,6 +167,9 @@ storage before closing on a phone. The Queues summary marks it **sync pending**
|
||||||
foreground, and midnight lifecycle checks share one delivery flight. A successful account receipt removes
|
foreground, and midnight lifecycle checks share one delivery flight. A successful account receipt removes
|
||||||
the pending copy, while a revision conflict preserves both the phone plan and fresh server snapshot for
|
the pending copy, while a revision conflict preserves both the phone plan and fresh server snapshot for
|
||||||
review. Unsynced Tomorrow work is never promoted into Today.
|
review. Unsynced Tomorrow work is never promoted into Today.
|
||||||
|
**Week Ahead** opens as a read-first seven-day mobile overview, so operators can inspect the next planned day,
|
||||||
|
work titles and references, load versus capacity, overloads, and pending sync without staging a change. **Edit day**
|
||||||
|
enters one date and returns to the refreshed overview; **Edit week** starts the continuous planning pass.
|
||||||
**Plan Week Ahead** continues through seven local dates and now finishes on a mobile review step instead of
|
**Plan Week Ahead** continues through seven local dates and now finishes on a mobile review step instead of
|
||||||
closing after the seventh save. The review shows planned minutes against each day’s capacity, marks overloads,
|
closing after the seventh save. The review shows planned minutes against each day’s capacity, marks overloads,
|
||||||
and flags work assigned to more than one date. Operators can move an item to another date without copying it;
|
and flags work assigned to more than one date. Operators can move an item to another date without copying it;
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.week-review-days { display:grid; gap:12px; }
|
.week-review-days { display:grid; gap:12px; }
|
||||||
.week-review-day { padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; }
|
.week-review-day { padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; }
|
||||||
.week-review-day.is-overloaded { border-color:#f59e0b; background:#2a1c12; }
|
.week-review-day.is-overloaded { border-color:#f59e0b; background:#2a1c12; }
|
||||||
|
.week-review-day.is-next-up { border-color:#60a5fa; box-shadow:0 0 0 1px #60a5fa inset; }
|
||||||
|
.week-next-up { display:inline-block; margin-left:6px; padding:2px 7px; border-radius:999px; background:#1d4f7a; color:#dbeafe; font-size:11px; vertical-align:middle; }
|
||||||
.week-review-day header { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
.week-review-day header { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
||||||
.week-review-day header button { flex:0 0 auto; min-height:44px; }
|
.week-review-day header button { flex:0 0 auto; min-height:44px; }
|
||||||
.week-review-day h3, .week-review-day p { margin:0 0 8px; }
|
.week-review-day h3, .week-review-day p { margin:0 0 8px; }
|
||||||
|
|
@ -337,6 +339,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.week-review-duplicates { margin:12px 0; padding:12px; border:1px solid #f59e0b; border-radius:10px; background:#2a1c12; }
|
.week-review-duplicates { margin:12px 0; padding:12px; border:1px solid #f59e0b; border-radius:10px; background:#2a1c12; }
|
||||||
.week-review-status { min-height:1.4em; margin:10px 0; }
|
.week-review-status { min-height:1.4em; margin:10px 0; }
|
||||||
#confirm-week-plan { width:100%; min-height:48px; position:sticky; bottom:0; }
|
#confirm-week-plan { width:100%; min-height:48px; position:sticky; bottom:0; }
|
||||||
|
#edit-week-plan { width:100%; min-height:48px; position:sticky; bottom:0; }
|
||||||
#open-week-capacity-import { width:100%; min-height:44px; margin:8px 0 12px; }
|
#open-week-capacity-import { width:100%; min-height:44px; margin:8px 0 12px; }
|
||||||
.week-capacity-import { position:fixed; z-index:121; inset:0; box-sizing:border-box; width:100%; max-width:560px; margin-inline:auto; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; overflow:auto; overflow-x:hidden; }
|
.week-capacity-import { position:fixed; z-index:121; inset:0; box-sizing:border-box; width:100%; max-width:560px; margin-inline:auto; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; overflow:auto; overflow-x:hidden; }
|
||||||
.week-capacity-import[hidden] { display:none; }
|
.week-capacity-import[hidden] { display:none; }
|
||||||
|
|
|
||||||
|
|
@ -506,13 +506,14 @@
|
||||||
<div class="small week-conflict-status" id="week-conflict-status" role="status" aria-live="assertive"></div>
|
<div class="small week-conflict-status" id="week-conflict-status" role="status" aria-live="assertive"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="week-review" id="week-review" aria-labelledby="week-review-title" hidden>
|
<section class="week-review" id="week-review" aria-labelledby="week-review-title" hidden>
|
||||||
<div class="small">Final planning step</div>
|
<div class="small">Seven-day overview</div>
|
||||||
<h2 id="week-review-title">Review & rebalance Week Ahead</h2>
|
<h2 id="week-review-title">Week Ahead</h2>
|
||||||
<p class="small muted">Check each day’s load. Move work instead of copying it, then confirm one executable week.</p>
|
<p class="small muted">See what is next and check each day’s load. Edit only when you are ready.</p>
|
||||||
<button id="open-week-capacity-import" type="button">Set capacity from calendar</button>
|
<button id="open-week-capacity-import" type="button">Set capacity from calendar</button>
|
||||||
<div class="week-review-duplicates" id="week-review-duplicates" role="alert" hidden></div>
|
<div class="week-review-duplicates" id="week-review-duplicates" role="alert" hidden></div>
|
||||||
<div class="week-review-days" id="week-review-days"></div>
|
<div class="week-review-days" id="week-review-days"></div>
|
||||||
<div class="small week-review-status" id="week-review-status" role="status" aria-live="polite"></div>
|
<div class="small week-review-status" id="week-review-status" role="status" aria-live="polite"></div>
|
||||||
|
<button id="edit-week-plan" type="button" hidden>Edit week</button>
|
||||||
<button id="confirm-week-plan" type="button">Confirm Week Ahead</button>
|
<button id="confirm-week-plan" type="button">Confirm Week Ahead</button>
|
||||||
</section>
|
</section>
|
||||||
<section class="week-capacity-import" id="week-capacity-import" aria-labelledby="week-capacity-title" hidden>
|
<section class="week-capacity-import" id="week-capacity-import" aria-labelledby="week-capacity-title" hidden>
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,7 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
todayWork,refresh,warm}={}) {
|
todayWork,refresh,warm}={}) {
|
||||||
let selectedDate=null;
|
let selectedDate=null;
|
||||||
let reviewing=false;
|
let reviewing=false;
|
||||||
|
let overviewing=false;
|
||||||
let editingFromReview=false;
|
let editingFromReview=false;
|
||||||
let blockedReviewOpen=false;
|
let blockedReviewOpen=false;
|
||||||
let reconciliation=null;
|
let reconciliation=null;
|
||||||
|
|
@ -419,6 +420,7 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
}
|
}
|
||||||
function renderReview() {
|
function renderReview() {
|
||||||
const value=controller.review(),root=qs('#week-review-days'),duplicates=qs('#week-review-duplicates');
|
const value=controller.review(),root=qs('#week-review-days'),duplicates=qs('#week-review-duplicates');
|
||||||
|
const nextUp=overviewing?value.days.find(day=>day.ids.length):null;
|
||||||
const destinations=value.days.map(day=>'<option value="'+escapeAttribute(day.plan_date)+'">'+escapeHtml(day.label)+'</option>').join('');
|
const destinations=value.days.map(day=>'<option value="'+escapeAttribute(day.plan_date)+'">'+escapeHtml(day.label)+'</option>').join('');
|
||||||
const itemMarkup=(id,day)=>{
|
const itemMarkup=(id,day)=>{
|
||||||
const item=getItem(id),estimate=Number(day.estimates?.[id])||0;
|
const item=getItem(id),estimate=Number(day.estimates?.[id])||0;
|
||||||
|
|
@ -430,10 +432,11 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
root.innerHTML=value.days.map(day=>{
|
root.innerHTML=value.days.map(day=>{
|
||||||
const capacity=Number(day.capacity_minutes)||0;
|
const capacity=Number(day.capacity_minutes)||0;
|
||||||
const load=day.planned_minutes+(capacity?' of '+capacity:'')+' min'+(day.overloaded?' · over capacity':'');
|
const load=day.planned_minutes+(capacity?' of '+capacity:'')+' min'+(day.overloaded?' · over capacity':'');
|
||||||
const items=day.ids.length?'<ul>'+day.ids.map(id=>'<li>'+itemMarkup(id,day)+'<div class="week-review-move"><label>Move to <select data-week-move-destination="'+
|
const move=id=>overviewing?'':'<div class="week-review-move"><label>Move to <select data-week-move-destination="'+escapeAttribute(id)+'">'+destinations+
|
||||||
escapeAttribute(id)+'">'+destinations+'</select></label><button type="button" data-week-move="'+escapeAttribute(id)+'">Move</button></div></li>').join('')+'</ul>':
|
'</select></label><button type="button" data-week-move="'+escapeAttribute(id)+'">Move</button></div>';
|
||||||
|
const items=day.ids.length?'<ul>'+day.ids.map(id=>'<li>'+itemMarkup(id,day)+move(id)+'</li>').join('')+'</ul>':
|
||||||
'<p class="small muted">Nothing planned.</p>';
|
'<p class="small muted">Nothing planned.</p>';
|
||||||
return '<article class="week-review-day'+(day.overloaded?' is-overloaded':'')+'"><header><h3>'+escapeHtml(day.label)+'</h3><button type="button" data-week-edit-day="'+
|
return '<article class="week-review-day'+(day.overloaded?' is-overloaded':'')+(day===nextUp?' is-next-up':'')+'"><header><h3>'+escapeHtml(day.label)+(day===nextUp?' <span class="week-next-up">Next up</span>':'')+'</h3><button type="button" data-week-edit-day="'+
|
||||||
escapeAttribute(day.plan_date)+'" aria-label="Edit '+escapeAttribute(day.label)+'">Edit day</button></header><p class="week-review-load">'+
|
escapeAttribute(day.plan_date)+'" aria-label="Edit '+escapeAttribute(day.label)+'">Edit day</button></header><p class="week-review-load">'+
|
||||||
escapeHtml(load)+'</p>'+items+'</article>';
|
escapeHtml(load)+'</p>'+items+'</article>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -444,12 +447,14 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
const pending=Boolean(controller.pending?.());
|
const pending=Boolean(controller.pending?.());
|
||||||
confirm.disabled=!value.can_confirm||pending;
|
confirm.disabled=!value.can_confirm||pending;
|
||||||
const blocker=value.blockers?.[0];
|
const blocker=value.blockers?.[0];
|
||||||
qs('#week-review-status').textContent=value.duplicates.length?'Duplicate work must be moved to one date.':
|
qs('#week-review-status').textContent=overviewing?'Week Ahead overview · '+(pending?'sync pending.':'no changes made.'):(value.duplicates.length?'Duplicate work must be moved to one date.':
|
||||||
(pending?'Saving the latest week to your account…':blocker?(
|
(pending?'Saving the latest week to your account…':blocker?(
|
||||||
blocker.type==='over-capacity'?blocker.plan_date+' is '+blocker.minutes+' min over capacity. Edit that day before confirming.':
|
blocker.type==='over-capacity'?blocker.plan_date+' is '+blocker.minutes+' min over capacity. Edit that day before confirming.':
|
||||||
blocker.type==='missing-estimate'?'Add an estimate for work on '+blocker.plan_date+' before confirming.':
|
blocker.type==='missing-estimate'?'Add an estimate for work on '+blocker.plan_date+' before confirming.':
|
||||||
'Add capacity for '+blocker.plan_date+' before confirming.'
|
'Add capacity for '+blocker.plan_date+' before confirming.'
|
||||||
):'Week Ahead is balanced and ready.');
|
):'Week Ahead is balanced and ready.'));
|
||||||
|
confirm.hidden=overviewing;
|
||||||
|
const editWeek=qs('#edit-week-plan');if(editWeek){editWeek.hidden=!overviewing;editWeek.textContent=nextUp?'Edit week':'Plan Week Ahead';}
|
||||||
root.querySelectorAll('[data-week-move]').forEach(button=>button.addEventListener('click',()=>{
|
root.querySelectorAll('[data-week-move]').forEach(button=>button.addEventListener('click',()=>{
|
||||||
const selector=root.querySelector(`[data-week-move-destination="${button.dataset.weekMove}"]`);
|
const selector=root.querySelector(`[data-week-move-destination="${button.dataset.weekMove}"]`);
|
||||||
if(!controller.move(button.dataset.weekMove,selector?.value))return;
|
if(!controller.move(button.dataset.weekMove,selector?.value))return;
|
||||||
|
|
@ -469,6 +474,12 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
qs('#week-plan-progress')?.focus?.();
|
qs('#week-plan-progress')?.focus?.();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
function editWeek() {
|
||||||
|
overviewing=false;reviewing=false;selectedDate=controller.dates()[0].date;setReviewMode(false);
|
||||||
|
qs('#week-review').hidden=true;qs('#confirm-week-plan').hidden=false;
|
||||||
|
const edit=qs('#edit-week-plan');if(edit)edit.hidden=true;
|
||||||
|
renderDates();openPlanner(null,false);qs('#week-plan-progress')?.focus?.();return true;
|
||||||
|
}
|
||||||
function returnToReview() {
|
function returnToReview() {
|
||||||
if(!editingFromReview)return false;
|
if(!editingFromReview)return false;
|
||||||
const editedDate=selectedDate;
|
const editedDate=selectedDate;
|
||||||
|
|
@ -480,11 +491,12 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async function open(trigger) {
|
async function open(trigger) {
|
||||||
trigger.disabled=true;reviewing=false;editingFromReview=false;setReviewMode(false);const reviewRoot=qs('#week-review');if(reviewRoot)reviewRoot.hidden=true;
|
trigger.disabled=true;reviewing=false;overviewing=false;editingFromReview=false;setReviewMode(false);const reviewRoot=qs('#week-review');if(reviewRoot)reviewRoot.hidden=true;
|
||||||
const back=qs('#back-to-week-review');if(back)back.hidden=true;
|
const back=qs('#back-to-week-review');if(back)back.hidden=true;
|
||||||
selectedDate=controller.dates()[0].date;renderDates();openPlanner(trigger);
|
selectedDate=null;renderDates();openPlanner(trigger);
|
||||||
qs('#mobile-week-summary').textContent='Loading Week Ahead…';
|
qs('#mobile-week-summary').textContent='Loading Week Ahead…';
|
||||||
try{await controller.load();qs('#mobile-week-summary').textContent=controller.summary();renderDates();openPlanner(null,false);return true;}
|
try{await controller.load();qs('#mobile-week-summary').textContent=controller.summary();overviewing=true;reviewing=true;
|
||||||
|
setReviewMode(true);reviewRoot.hidden=false;renderDates();renderReview();openPlanner(null,false);return true;}
|
||||||
catch(error){qs('#mobile-week-summary').textContent='Unavailable · tap to retry';qs('#my-work-action-status').textContent=(error.message||'Week Ahead is unavailable.')+' Retry when connected.';return false;}
|
catch(error){qs('#mobile-week-summary').textContent='Unavailable · tap to retry';qs('#my-work-action-status').textContent=(error.message||'Week Ahead is unavailable.')+' Retry when connected.';return false;}
|
||||||
finally{trigger.disabled=false;}
|
finally{trigger.disabled=false;}
|
||||||
}
|
}
|
||||||
|
|
@ -534,8 +546,9 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
if(!reviewing||!value.can_confirm||controller.pending()) return false;
|
if(!reviewing||!value.can_confirm||controller.pending()) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function finish(){reviewing=false;setReviewMode(false);qs('#week-review').hidden=true;return true;}
|
function finish(){reviewing=false;overviewing=false;setReviewMode(false);qs('#week-review').hidden=true;return true;}
|
||||||
qs('#back-to-week-review')?.addEventListener('click',returnToReview);
|
qs('#back-to-week-review')?.addEventListener('click',returnToReview);
|
||||||
|
qs('#edit-week-plan')?.addEventListener('click',editWeek);
|
||||||
async function promote(plan){
|
async function promote(plan){
|
||||||
try{await controller.load();const promoted=await controller.promote(plan.revision);if(!promoted)return false;blockedReviewOpen=false;todayWork.replace(promoted.ids);todayWork.replacePlanning({capacity_minutes:promoted.capacity_minutes??null,estimates:promoted.estimates||{}});refresh();warm();qs('#mobile-week-summary').textContent=controller.summary();qs('#my-work-action-status').textContent='Your saved Week Ahead plan is now Today.';return true;}
|
try{await controller.load();const promoted=await controller.promote(plan.revision);if(!promoted)return false;blockedReviewOpen=false;todayWork.replace(promoted.ids);todayWork.replacePlanning({capacity_minutes:promoted.capacity_minutes??null,estimates:promoted.estimates||{}});refresh();warm();qs('#mobile-week-summary').textContent=controller.summary();qs('#my-work-action-status').textContent='Your saved Week Ahead plan is now Today.';return true;}
|
||||||
catch(error){
|
catch(error){
|
||||||
|
|
@ -549,12 +562,13 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
|
||||||
qs('#my-work-action-status').textContent=(error.message||'Week Ahead needs review before promotion.')+' Open Week Ahead to review.';return false;
|
qs('#my-work-action-status').textContent=(error.message||'Week Ahead needs review before promotion.')+' Open Week Ahead to review.';return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const workflow={open,save,advance,confirm,finish,promote,renderDates,renderReview,renderConflict,active:()=>Boolean(selectedDate)||reviewing||Boolean(reconciliation),
|
const workflow={open,save,advance,confirm,finish,promote,editWeek,renderDates,renderReview,renderConflict,active:()=>Boolean(selectedDate)||reviewing||Boolean(reconciliation),
|
||||||
reviewing:()=>reviewing,selectedDate:()=>selectedDate,
|
reviewing:()=>reviewing,overviewing:()=>overviewing,selectedDate:()=>selectedDate,
|
||||||
day:()=>reconciliationDay()||(selectedDate?controller.day(selectedDate):null),
|
day:()=>reconciliationDay()||(selectedDate?controller.day(selectedDate):null),
|
||||||
copy:()=>reconciliation?{title:"Start today's plan",heading:'Unfinished Today + due Week Ahead',available:'Available today',build:'Build combined Today'}:
|
copy:()=>reconciliation?{title:"Start today's plan",heading:'Unfinished Today + due Week Ahead',available:'Available today',build:'Build combined Today'}:
|
||||||
(selectedDate?{title:'Plan Week Ahead',heading:selectedDate+', in order',available:'Available this day',build:'Build this day'}:null),
|
(overviewing?{title:'Week Ahead',heading:'Seven-day overview',available:'Planned work',build:'Edit week'}:
|
||||||
clear(){selectedDate=null;reviewing=false;editingFromReview=false;reconciliation=null;setReviewMode(false);qs('#week-review').hidden=true;
|
(selectedDate?{title:'Plan Week Ahead',heading:selectedDate+', in order',available:'Available this day',build:'Build this day'}:null)),
|
||||||
|
clear(){selectedDate=null;reviewing=false;overviewing=false;editingFromReview=false;reconciliation=null;setReviewMode(false);qs('#week-review').hidden=true;
|
||||||
const back=qs('#back-to-week-review');if(back)back.hidden=true;renderDates();}};
|
const back=qs('#back-to-week-review');if(back)back.hidden=true;renderDates();}};
|
||||||
if(typeof weekCalendarImport!=='undefined')weekCalendarImport.mount(controller,workflow,qs);
|
if(typeof weekCalendarImport!=='undefined')weekCalendarImport.mount(controller,workflow,qs);
|
||||||
return workflow;
|
return workflow;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,26 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||||
page.wait_for_timeout(100)
|
page.wait_for_timeout(100)
|
||||||
|
|
||||||
assert not page_errors, f"Week Ahead launch raised: {page_errors}"
|
assert not page_errors, f"Week Ahead launch raised: {page_errors}"
|
||||||
|
expect(page.locator("#plan-today-title")).to_have_text("Week Ahead")
|
||||||
|
overview = page.locator("#week-review")
|
||||||
|
expect(overview).to_be_visible()
|
||||||
|
cards = page.locator("#week-review-days .week-review-day")
|
||||||
|
expect(cards).to_have_count(7)
|
||||||
|
expect(cards.first).to_contain_text("Next up")
|
||||||
|
expect(cards.first).to_contain_text("Ship mobile capture")
|
||||||
|
expect(page.locator("#week-review-status")).to_have_text(
|
||||||
|
"Week Ahead overview · no changes made."
|
||||||
|
)
|
||||||
|
expect(page.locator("#confirm-week-plan")).to_be_hidden()
|
||||||
|
edit_week = page.locator("#edit-week-plan")
|
||||||
|
expect(edit_week).to_be_visible()
|
||||||
|
assert saved == [], "opening and inspecting Week Ahead must not write"
|
||||||
|
for control in (cards.first.locator("[data-week-edit-day]"), edit_week):
|
||||||
|
bounds = control.bounding_box()
|
||||||
|
assert bounds and bounds["height"] >= 44
|
||||||
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
|
edit_week.click()
|
||||||
|
expect(overview).to_be_hidden()
|
||||||
expect(page.locator("#plan-today-title")).to_have_text("Plan Week Ahead")
|
expect(page.locator("#plan-today-title")).to_have_text("Plan Week Ahead")
|
||||||
dates = page.locator("#week-plan-dates button")
|
dates = page.locator("#week-plan-dates button")
|
||||||
expect(dates).to_have_count(7)
|
expect(dates).to_have_count(7)
|
||||||
|
|
@ -243,6 +263,7 @@ def test_release_artifact_reconciles_only_the_week_day_changed_on_both_devices(
|
||||||
page.wait_for_url(origin + "/", wait_until="networkidle")
|
page.wait_for_url(origin + "/", wait_until="networkidle")
|
||||||
page.locator('[data-mobile-task="queues"]').click()
|
page.locator('[data-mobile-task="queues"]').click()
|
||||||
page.locator('[data-mobile-queue="week"]').click()
|
page.locator('[data-mobile-queue="week"]').click()
|
||||||
|
page.locator("#edit-week-plan").click()
|
||||||
page.locator("#plan-today-available").fill("60")
|
page.locator("#plan-today-available").fill("60")
|
||||||
page.locator("#save-today-plan").click()
|
page.locator("#save-today-plan").click()
|
||||||
page.wait_for_function("() => document.querySelector('#mobile-week-summary').textContent.includes('Conflict')")
|
page.wait_for_function("() => document.querySelector('#mobile-week-summary').textContent.includes('Conflict')")
|
||||||
|
|
|
||||||
|
|
@ -179,15 +179,18 @@ Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons
|
||||||
root.querySelectorAll=()=>root.buttons||[];
|
root.querySelectorAll=()=>root.buttons||[];
|
||||||
root.querySelector=selector=>(root.buttons||[]).find(button=>selector.includes(button.dataset.weekPlanDate))||null;
|
root.querySelector=selector=>(root.buttons||[]).find(button=>selector.includes(button.dataset.weekPlanDate))||null;
|
||||||
elements.set('#week-plan-dates',root);
|
elements.set('#week-plan-dates',root);
|
||||||
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan']) elements.set(selector,makeElement());
|
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
|
||||||
|
'#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status','#edit-week-plan']) elements.set(selector,makeElement());
|
||||||
let opened=0,staged=[];
|
let opened=0,staged=[];
|
||||||
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>({plan_date:date,ids:[]}),
|
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>({plan_date:date,ids:[]}),
|
||||||
pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:staged.length,next_date:dates[index+1]||null,last:index===6};},
|
pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:staged.length,next_date:dates[index+1]||null,last:index===6};},
|
||||||
load:async()=>({}),summary:()=> 'Nothing planned',stageDay:(date,plan)=>{staged.push({date,plan});return true;},flush:async()=>({})};
|
load:async()=>({}),summary:()=> 'Nothing planned',review:()=>({days:dates.map(date=>({plan_date:date,label:date,ids:[],capacity_minutes:null,estimates:{},planned_minutes:0,overloaded:false})),duplicates:[],blockers:[],can_confirm:true}),pending:()=>false,
|
||||||
|
stageDay:(date,plan)=>{staged.push({date,plan});return true;},flush:async()=>({})};
|
||||||
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
|
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
|
||||||
openPlanner:()=>{opened+=1;},escapeHtml:value=>value,escapeAttribute:value=>value,
|
openPlanner:()=>{opened+=1;},escapeHtml:value=>value,escapeAttribute:value=>value,
|
||||||
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
||||||
await workflow.open({disabled:false});
|
await workflow.open({disabled:false});
|
||||||
|
workflow.editWeek();
|
||||||
workflow.save({ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}});
|
workflow.save({ids:['issue:r:1:'],capacity_minutes:60,estimates:{'issue:r:1:':30}});
|
||||||
const continued=workflow.advance();
|
const continued=workflow.advance();
|
||||||
console.log(JSON.stringify({continued,selected:workflow.selectedDate(),opened,staged,
|
console.log(JSON.stringify({continued,selected:workflow.selectedDate(),opened,staged,
|
||||||
|
|
@ -196,12 +199,67 @@ console.log(JSON.stringify({continued,selected:workflow.selectedDate(),opened,st
|
||||||
|
|
||||||
assert result["continued"] is True
|
assert result["continued"] is True
|
||||||
assert result["selected"] == "2026-08-22"
|
assert result["selected"] == "2026-08-22"
|
||||||
assert result["opened"] == 3
|
assert result["opened"] == 4
|
||||||
assert result["staged"][0]["date"] == "2026-08-21"
|
assert result["staged"][0]["date"] == "2026-08-21"
|
||||||
assert result["progress"] == "Day 2 of 7 · 1 planned"
|
assert result["progress"] == "Day 2 of 7 · 1 planned"
|
||||||
assert result["label"] == "Save & next"
|
assert result["label"] == "Save & next"
|
||||||
|
|
||||||
|
|
||||||
|
def test_week_workflow_opens_a_read_only_overview_with_next_up_work():
|
||||||
|
result = run_controller("""
|
||||||
|
const createWorkflow=createWeekPlan.Workflow;
|
||||||
|
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||||
|
const elements=new Map();
|
||||||
|
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
|
||||||
|
querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
|
||||||
|
const dateRoot=makeElement();
|
||||||
|
Object.defineProperty(dateRoot,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
|
||||||
|
elements.set('#week-plan-dates',dateRoot);
|
||||||
|
const reviewDays=makeElement();
|
||||||
|
Object.defineProperty(reviewDays,'innerHTML',{set(value){this.value=value;},get(){return this.value||'';}});
|
||||||
|
elements.set('#week-review-days',reviewDays);
|
||||||
|
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
|
||||||
|
'#week-review','#week-review-duplicates','#confirm-week-plan','#week-review-status','#back-to-week-review',
|
||||||
|
'#edit-week-plan']) if(!elements.has(selector))elements.set(selector,makeElement());
|
||||||
|
let writes=0,opened=0,reviewMode=false;
|
||||||
|
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),
|
||||||
|
ids:index===2?['issue:stackchain/dashboard:42:']:[],capacity_minutes:60,
|
||||||
|
estimates:index===2?{'issue:stackchain/dashboard:42:':30}:{},planned_minutes:index===2?30:0,overloaded:false})),
|
||||||
|
duplicates:[],blockers:[],can_confirm:true};
|
||||||
|
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
|
||||||
|
pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:1,next_date:dates[index+1]||null,last:index===6};},
|
||||||
|
load:async()=>({}),summary:()=> '1 item across 1 day',review:()=>review,pending:()=>false,
|
||||||
|
move:()=>true,flush:async()=>{writes+=1;}};
|
||||||
|
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
|
||||||
|
getItem:id=>id.includes(':42:')?{kind:'issue',title:'Ship mobile overview',repository:'stackchain/dashboard',number:42}:null,
|
||||||
|
openPlanner:()=>{opened+=1;},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
|
||||||
|
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
||||||
|
await workflow.open({disabled:false});
|
||||||
|
console.log(JSON.stringify({reviewing:workflow.reviewing(),reviewMode,writes,opened,copy:workflow.copy(),
|
||||||
|
reviewHidden:elements.get('#week-review').hidden,datesHidden:elements.get('#week-plan-dates').hidden,
|
||||||
|
markup:reviewDays.innerHTML,status:elements.get('#week-review-status').textContent,
|
||||||
|
confirmHidden:elements.get('#confirm-week-plan').hidden,editWeekHidden:elements.get('#edit-week-plan').hidden,
|
||||||
|
editWeekLabel:elements.get('#edit-week-plan').textContent}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result["reviewing"] is True
|
||||||
|
assert result["reviewMode"] is True
|
||||||
|
assert result["writes"] == 0
|
||||||
|
assert result["copy"] == {
|
||||||
|
"title": "Week Ahead", "heading": "Seven-day overview",
|
||||||
|
"available": "Planned work", "build": "Edit week",
|
||||||
|
}
|
||||||
|
assert result["reviewHidden"] is False
|
||||||
|
assert result["datesHidden"] is True
|
||||||
|
assert "Ship mobile overview" in result["markup"]
|
||||||
|
assert "Next up" in result["markup"]
|
||||||
|
assert "data-week-move" not in result["markup"]
|
||||||
|
assert result["status"] == "Week Ahead overview · no changes made."
|
||||||
|
assert result["confirmHidden"] is True
|
||||||
|
assert result["editWeekHidden"] is False
|
||||||
|
assert result["editWeekLabel"] == "Edit week"
|
||||||
|
|
||||||
|
|
||||||
def test_week_workflow_opens_review_after_day_seven_and_blocks_duplicate_confirmation():
|
def test_week_workflow_opens_review_after_day_seven_and_blocks_duplicate_confirmation():
|
||||||
result = run_controller("""
|
result = run_controller("""
|
||||||
const createWorkflow=createWeekPlan.Workflow;
|
const createWorkflow=createWeekPlan.Workflow;
|
||||||
|
|
@ -213,7 +271,7 @@ const root=makeElement();
|
||||||
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
|
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
|
||||||
elements.set('#week-plan-dates',root);
|
elements.set('#week-plan-dates',root);
|
||||||
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
|
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
|
||||||
'#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status']) elements.set(selector,makeElement());
|
'#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status','#edit-week-plan']) elements.set(selector,makeElement());
|
||||||
let reviewMode=false;
|
let reviewMode=false;
|
||||||
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index<2?['shared']:[],
|
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index<2?['shared']:[],
|
||||||
capacity_minutes:index===0?30:60,planned_minutes:index===0?45:15,overloaded:index===0})),
|
capacity_minutes:index===0?30:60,planned_minutes:index===0?45:15,overloaded:index===0})),
|
||||||
|
|
@ -225,6 +283,7 @@ const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),ge
|
||||||
openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
|
openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
|
||||||
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
|
||||||
await workflow.open({disabled:false});
|
await workflow.open({disabled:false});
|
||||||
|
workflow.editWeek();
|
||||||
for(let index=0;index<6;index++) workflow.advance();
|
for(let index=0;index<6;index++) workflow.advance();
|
||||||
const advanced=workflow.advance();
|
const advanced=workflow.advance();
|
||||||
console.log(JSON.stringify({advanced,reviewing:workflow.reviewing(),reviewMode,
|
console.log(JSON.stringify({advanced,reviewing:workflow.reviewing(),reviewMode,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user