feat: pull Week Ahead work into Today (Closes #1242)
CI / lint (pull_request) Successful in 3m22s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 5m11s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-22 00:10:39 +00:00
1 parent 6afdad6fe4
commit 3fd5983df6
7 files changed
+314 -13

No files matched your search

+2
View File
@@ -347,6 +347,8 @@ textarea { resize: vertical; min-height: 120px; }
.week-review-item-open:hover { background:#173453; }
.week-review-item-open:focus-visible { outline:3px solid #93c5fd; outline-offset:2px; }
.week-review-unplan { width:100%; min-height:44px; border-color:#6b87a6; background:transparent; color:#d7e5f5; }
.week-review-pull { grid-column:1/-1; width:100%; min-height:44px; border-color:#60a5fa; background:#1d4f7a; color:#eff6ff; font-weight:800; }
.week-review-unplan { grid-column:1/-1; }
.week-unplan-receipt { margin:12px 0 6px; padding:10px 12px; border:1px solid #60a5fa; border-radius:10px; background:#112d4d; color:#dbeafe; }
#undo-week-unplan { width:100%; min-height:44px; margin-bottom:8px; border-color:#60a5fa; background:#1d4f7a; color:#eff6ff; font-weight:800; }
.week-start-early { display:block; width:100%; min-height:44px; margin-top:12px; border-color:#60a5fa; background:#1d4f7a; color:#eff6ff; font-weight:800; }
+38 -3
View File
@@ -464,6 +464,15 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
plan_date:due.plan_date,today_revision:todayRevision,
})});
}
async function pullItem(identity,today,operationId) {
if(offlineSnapshot||pending()||conflict()||!identity||!operationId||
!Number.isInteger(today?.revision)||!(today.ids||[]).length)return false;
const result=await fetchJson('api/v1/week/pull-item',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
operation_id:operationId,identity,today_revision:today.revision,week_revision:week.revision,
})});
adoptConfirmed(result.week,{...confirmedItems,...pendingItems});
return result;
}
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);
@@ -494,13 +503,14 @@ 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,previewReflow,applyReflow,move,retire,unplan,restore,placement,place,pending,flush,conflict,chooseDay,saveMerged,
keepLocal,useRemote,promote,startEarly,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
keepLocal,useRemote,promote,pullItem,startEarly,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
item:id=>pendingItems[id]||confirmedItems[id]||null,
offline:()=>offlineSnapshot,request:fetchJson,reschedule:()=>({storage,getLogin})};
}
function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
todayWork,refresh:r=()=>{},warm:w=()=>{},today:t=()=>null,r:refresh=r,w:warm=w,t:getTodayPlan=t,
confirmEarly=message=>globalThis.confirm?.(message)??false,setTimer=globalThis.setTimeout,clearTimer=globalThis.clearTimeout,x=null}={}) {
confirmEarly=message=>globalThis.confirm?.(message)??false,setTimer=globalThis.setTimeout,clearTimer=globalThis.clearTimeout,
operationId=()=>globalThis.crypto?.randomUUID?.()||('pull-'+Date.now()+'-'+Math.random().toString(16).slice(2)),x=null}={}) {
let selectedDate=null;
let reviewing=false;
let overviewing=false;
@@ -689,7 +699,13 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
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 unplan=id=>overviewing&&!readOnly?'<button type="button" class="week-review-unplan" data-week-unplan="'+escapeAttribute(id)+'">Remove from week</button>':'';
const items=day.ids.length?'<ul>'+day.ids.map(id=>'<li>'+itemMarkup(id,day)+move(id)+unplan(id)+'</li>').join('')+'</ul>':
const pull=id=>{
const inToday=(today?.ids||[]).includes(id),full=(today?.ids||[]).length>=5;
if(!overviewing||readOnly||pending||!Number.isInteger(today?.revision)||!(today?.ids||[]).length||inToday)return '';
return '<button type="button" class="week-review-pull" data-week-pull-item="'+escapeAttribute(id)+'"'+(full?' disabled':'')+'>'+
(full?'Today is full':'Add to Today')+'</button>';
};
const items=day.ids.length?'<ul>'+day.ids.map(id=>'<li>'+itemMarkup(id,day)+move(id)+pull(id)+unplan(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>':'';
@@ -732,6 +748,25 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
qs('#week-review-status').textContent=(error.message||'Week Ahead sync is unavailable.')+' Removal remains saved on this phone.';
}
}));
root.querySelectorAll('[data-week-pull-item]').forEach(button=>button.addEventListener('click',async event=>{
const id=event.currentTarget.dataset.weekPullItem,current=getTodayPlan?.();
if(!id||!Number.isInteger(current?.revision)||!(current.ids||[]).length||current.ids.length>=5)return;
event.currentTarget.disabled=true;
try{
const result=await controller.pullItem?.(id,current,operationId());
if(!result)return;
getTodayPlan?.(result.today);todayWork.replace(result.today.ids);
todayWork.replacePlanning({capacity_minutes:result.today.capacity_minutes??null,estimates:result.today.estimates||{}});
refresh();warm();renderReview();
qs('#mobile-week-summary').textContent=controller.summary();
const item=getItem(id)||controller.item?.(id);
qs('#week-review-status').textContent=String(item?.title||'Work')+' added to Today.';
}catch(error){
renderReview();
qs('#week-review-status').textContent=error?.code==='today_full'?'Today is full. Finish or move work before adding more.':
((error.message||'Today or Week Ahead changed.')+' Reopen Week Ahead and retry.');
}
}));
root.querySelectorAll('[data-week-open-item]').forEach(button=>button.addEventListener('click',event=>{
const id=button.dataset.weekOpenItem,item=getItem(id)||controller.item?.(id);
if(item)openItem?.(item,event.currentTarget);