feat: add read-first Week Ahead overview (Closes #1216)
CI / lint (pull_request) Successful in 3m37s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 5m19s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-21 09:07:27 +00:00
1 parent 45e2798d37
commit 42e97b4a79
6 files changed
+121 -20

No files matched your search

+3
View File
@@ -321,6 +321,8 @@ textarea { resize: vertical; min-height: 120px; }
.week-review-days { display:grid; gap:12px; }
.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-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 button { flex:0 0 auto; min-height:44px; }
.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-status { min-height:1.4em; margin:10px 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; }
.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; }
+4 -3
View File
@@ -506,13 +506,14 @@
<div class="small week-conflict-status" id="week-conflict-status" role="status" aria-live="assertive"></div>
</section>
<section class="week-review" id="week-review" aria-labelledby="week-review-title" hidden>
<div class="small">Final planning step</div>
<h2 id="week-review-title">Review &amp; rebalance Week Ahead</h2>
<p class="small muted">Check each day’s load. Move work instead of copying it, then confirm one executable week.</p>
<div class="small">Seven-day overview</div>
<h2 id="week-review-title">Week Ahead</h2>
<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>
<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="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>
</section>
<section class="week-capacity-import" id="week-capacity-import" aria-labelledby="week-capacity-title" hidden>
+27 -13
View File
@@ -348,6 +348,7 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
todayWork,refresh,warm}={}) {
let selectedDate=null;
let reviewing=false;
let overviewing=false;
let editingFromReview=false;
let blockedReviewOpen=false;
let reconciliation=null;
@@ -419,6 +420,7 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
}
function renderReview() {
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 itemMarkup=(id,day)=>{
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=>{
const capacity=Number(day.capacity_minutes)||0;
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="'+
escapeAttribute(id)+'">'+destinations+'</select></label><button type="button" data-week-move="'+escapeAttribute(id)+'">Move</button></div></li>').join('')+'</ul>':
const move=id=>overviewing?'':'<div class="week-review-move"><label>Move to <select data-week-move-destination="'+escapeAttribute(id)+'">'+destinations+
'</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>';
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">'+
escapeHtml(load)+'</p>'+items+'</article>';
}).join('');
@@ -444,12 +447,14 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
const pending=Boolean(controller.pending?.());
confirm.disabled=!value.can_confirm||pending;
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?(
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.':
'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',()=>{
const selector=root.querySelector(`[data-week-move-destination="${button.dataset.weekMove}"]`);
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?.();
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() {
if(!editingFromReview)return false;
const editedDate=selectedDate;
@@ -480,11 +491,12 @@ function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPla
return true;
}
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;
selectedDate=controller.dates()[0].date;renderDates();openPlanner(trigger);
selectedDate=null;renderDates();openPlanner(trigger);
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;}
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;
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('#edit-week-plan')?.addEventListener('click',editWeek);
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;}
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;
}
}
const workflow={open,save,advance,confirm,finish,promote,renderDates,renderReview,renderConflict,active:()=>Boolean(selectedDate)||reviewing||Boolean(reconciliation),
reviewing:()=>reviewing,selectedDate:()=>selectedDate,
const workflow={open,save,advance,confirm,finish,promote,editWeek,renderDates,renderReview,renderConflict,active:()=>Boolean(selectedDate)||reviewing||Boolean(reconciliation),
reviewing:()=>reviewing,overviewing:()=>overviewing,selectedDate:()=>selectedDate,
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'}:
(selectedDate?{title:'Plan Week Ahead',heading:selectedDate+', in order',available:'Available this day',build:'Build this day'}:null),
clear(){selectedDate=null;reviewing=false;editingFromReview=false;reconciliation=null;setReviewMode(false);qs('#week-review').hidden=true;
(overviewing?{title:'Week Ahead',heading:'Seven-day overview',available:'Planned work',build:'Edit week'}:
(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();}};
if(typeof weekCalendarImport!=='undefined')weekCalendarImport.mount(controller,workflow,qs);
return workflow;