feat: start Week Ahead days early (Closes #1224)
CI / lint (pull_request) Successful in 3m53s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m38s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-21 14:37:50 +00:00
1 parent 25597d1f05
commit f64ce3acc4
8 files changed
+212 -11

No files matched your search

+2
View File
@@ -333,6 +333,8 @@ textarea { resize: vertical; min-height: 120px; }
.week-review-item-open { display:block; width:100%; min-width:0; min-height:44px; padding:8px; border:0; border-radius:8px; background:transparent; color:inherit; text-align:left; }
.week-review-item-open:hover { background:#173453; }
.week-review-item-open:focus-visible { outline:3px solid #93c5fd; outline-offset:2px; }
.week-start-early { display:block; width:100%; min-height:44px; margin-top:12px; border-color:#60a5fa; background:#1d4f7a; color:#eff6ff; font-weight:800; }
.week-start-early:focus-visible { outline:3px solid #bfdbfe; outline-offset:2px; }
.week-review-item-copy strong, .week-review-item-copy small { overflow-wrap:anywhere; }
.week-review-item-copy small { color:#a9bdd3; }
.week-review-move { display:flex; align-items:end; gap:8px; }
+2 -2
View File
@@ -457,7 +457,7 @@
getItem:weekItem,openItem:openRoutedWork,
openPlanner:openPlanToday,setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value),
escapeHtml,escapeAttribute:escAttr,todayWork,
refresh:refreshMyWorkView,warm:warmTodayOffline});
t:()=>latestTodayPlan,r:refreshMyWorkView,w:warmTodayOffline});
const weekCalendar=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekItem,escapeHtml,escapeAttribute:escAttr,
onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();},
});
@@ -7881,7 +7881,7 @@
try {
await weekPlan.keepLocal();
qs('#mobile-week-summary').textContent=weekPlan.summary();
qs('#my-work-action-status').textContent='This phone’s Week Ahead plan is saved to your account. Today was not changed.';
qs('#my-work-action-status').textContent='This phone’s Week Ahead plan is saved. Today was not changed.';
closePlanToday();
} catch(error) {
const conflict=weekPlan.conflict();
+39 -4
View File
@@ -406,6 +406,17 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
plan_date:due.plan_date,today_revision:todayRevision,
})});
}
async function startEarly(planDate,todayRevision) {
if(offlineSnapshot||pending()||conflict()||!Number.isInteger(todayRevision))return false;
const day=week.days.find(item=>item.plan_date===planDate&&item.ids.length);
if(!day)return false;
const promoted=await fetchJson('api/v1/week/start-early',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
promotion_id:`early-${day.plan_date}-r${week.revision}`,week_revision:week.revision,
plan_date:day.plan_date,today_revision:todayRevision,
})});
await load();
return promoted;
}
async function reconcilePromotion(preserved,selection) {
if(offlineSnapshot||pending()||conflict()) return false;
const due=(preserved?.week?.days||week.days).find(item=>item.plan_date<=localDate()&&item.ids.length);
@@ -425,12 +436,13 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
return label+(pending()?' · sync pending':'');
}
return {adopt,state,dates,day,pass,load,saveDay,stageDay,stageCapacities,review,move,placement,place,pending,flush,conflict,chooseDay,saveMerged,
keepLocal,useRemote,promote,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
keepLocal,useRemote,promote,startEarly,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
item:id=>pendingItems[id]||confirmedItems[id]||null,
offline:()=>offlineSnapshot};
}
function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
todayWork,refresh,warm}={}) {
todayWork,refresh:r=()=>{},warm:w=()=>{},today:t=()=>null,r:refresh=r,w:warm=w,t:getTodayPlan=t,
confirmEarly=message=>globalThis.confirm?.(message)??false}={}) {
let selectedDate=null;
let reviewing=false;
let overviewing=false;
@@ -521,7 +533,10 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
function renderReview() {
const value=controller.review(),root=qs('#week-review-days'),duplicates=qs('#week-review-duplicates');
const readOnly=Boolean(controller.offline?.());
const pending=Boolean(controller.pending?.());
const nextUp=overviewing?value.days.find(day=>day.ids.length):null;
const today=getTodayPlan?.();
const canStartEarly=Boolean(nextUp&&!readOnly&&!pending&&Number.isInteger(today?.revision)&&!today.ids?.length);
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)||controller.item?.(id),estimate=Number(day.estimates?.[id])||0;
@@ -540,14 +555,14 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
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>';
const edit=readOnly?'':'<button type="button" data-week-edit-day="'+escapeAttribute(day.plan_date)+'" aria-label="Edit '+escapeAttribute(day.label)+'">Edit day</button>';
const start=day===nextUp&&canStartEarly?'<button type="button" class="week-start-early" data-week-start-early="'+escapeAttribute(day.plan_date)+'">Start this day early</button>':'';
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>'+edit+'</header><p class="week-review-load">'+
escapeHtml(load)+'</p>'+items+'</article>';
escapeHtml(load)+'</p>'+items+start+'</article>';
}).join('');
duplicates.hidden=!value.duplicates.length;
duplicates.innerHTML=value.duplicates.length?'<strong>Choose one date for duplicated work before confirming.</strong><ul>'+value.duplicates.map(item=>
'<li><code>'+escapeHtml(item.id)+'</code> · '+escapeHtml(item.dates.join(', '))+'</li>').join('')+'</ul>':'';
const confirm=qs('#confirm-week-plan');
const pending=Boolean(controller.pending?.());
confirm.disabled=!value.can_confirm||pending;
const blocker=value.blockers?.[0];
qs('#week-review-status').textContent=readOnly?'Offline snapshot · viewing only. Retry for live editing.':(overviewing?'Week Ahead overview · '+(pending?'sync pending.':'no changes made.'):(value.duplicates.length?'Duplicate work must be moved to one date.':
@@ -569,6 +584,26 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
const id=button.dataset.weekOpenItem,item=getItem(id)||controller.item?.(id);
if(item)openItem?.(item,event.currentTarget);
}));
root.querySelectorAll('[data-week-start-early]').forEach(button=>button.addEventListener('click',async event=>{
const day=value.days.find(item=>item.plan_date===event.currentTarget.dataset.weekStartEarly);
const current=getTodayPlan?.();
if(!day||!Number.isInteger(current?.revision)||current.ids?.length)return;
const capacity=Number(day.capacity_minutes)||0;
const detail=day.ids.length+' item'+(day.ids.length===1?'':'s')+' · '+day.planned_minutes+(capacity?' of '+capacity:'')+' min';
if(!confirmEarly('Start '+day.label+' early?\n\n'+detail+' will move into Today.'))return;
event.currentTarget.disabled=true;
try{
const promoted=await controller.startEarly(day.plan_date,current.revision);
if(!promoted)return;
todayWork.replace(promoted.ids);todayWork.replacePlanning(promoted);refresh();warm();
qs('#mobile-week-summary').textContent=controller.summary();
qs('#my-work-action-status').textContent=day.label+' is now Today.';
finish();
}catch(error){
qs('#my-work-action-status').textContent=(error.message||'Week Ahead changed before it could start.')+' Reopen Week Ahead and review.';
event.currentTarget.disabled=false;
}
}));
root.querySelectorAll('[data-week-edit-day]').forEach(button=>button.addEventListener('click',()=>editDay(button.dataset.weekEditDay)));
renderPass();
}