feat: unplan Week Ahead work with undo (Closes #1240)
This commit is contained in:
1 parent
95887d7493
commit
449ab78991
18 files changed
+216
-32
No files matched your search
@@ -346,6 +346,9 @@ 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-review-unplan { width:100%; min-height:44px; border-color:#6b87a6; background:transparent; color:#d7e5f5; }
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
@@ -526,6 +526,8 @@
|
||||
</section>
|
||||
<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-unplan-receipt" id="week-unplan-receipt" role="status" aria-live="polite" hidden></div>
|
||||
<button id="undo-week-unplan" type="button" hidden>Undo</button>
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/private-data-registry.js');
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v129';
|
||||
const CACHE = 'stackchain-dashboard-shell-v130';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
||||
+68
-3
@@ -248,6 +248,24 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
const staged=stageDays(days);
|
||||
return staged?{...staged,removed}:false;
|
||||
}
|
||||
function unplan(id) {
|
||||
if(!id||offlineSnapshot)return false;
|
||||
const source=week.days.find(item=>(item.ids||[]).includes(id));
|
||||
if(!source)return false;
|
||||
const index=source.ids.indexOf(id),estimate=Number(source.estimates?.[id]);
|
||||
const receipt={id,plan_date:source.plan_date,index,estimate:Number.isFinite(estimate)?estimate:null};
|
||||
const staged=retire(id);
|
||||
return staged?receipt:false;
|
||||
}
|
||||
function restore(receipt) {
|
||||
if(!receipt?.id||!receipt.plan_date||!Number.isInteger(receipt.index)||offlineSnapshot||placement(receipt.id))return false;
|
||||
const days=week.days.map(cloneDay);
|
||||
let destination=days.find(item=>item.plan_date===receipt.plan_date);
|
||||
if(!destination){destination={plan_date:receipt.plan_date,ids:[],capacity_minutes:null,estimates:{}};days.push(destination);}
|
||||
destination.ids.splice(Math.min(receipt.index,destination.ids.length),0,receipt.id);
|
||||
if(Number.isFinite(Number(receipt.estimate)))destination.estimates[receipt.id]=Number(receipt.estimate);
|
||||
return stageDays(days);
|
||||
}
|
||||
function placement(id) {
|
||||
const found=week.days.find(item=>(item.ids||[]).includes(id));
|
||||
if(!found)return null;
|
||||
@@ -475,14 +493,14 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
const label=planned.length?`${items} item${items===1?'':'s'} across ${planned.length} day${planned.length===1?'':'s'}`:'Nothing planned';
|
||||
return label+(pending()?' · sync pending':'');
|
||||
}
|
||||
return {adopt,state,dates,day,pass,load,saveDay,stageDay,stageCapacities,review,previewReflow,applyReflow,move,retire,placement,place,pending,flush,conflict,chooseDay,saveMerged,
|
||||
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,
|
||||
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,x=null}={}) {
|
||||
confirmEarly=message=>globalThis.confirm?.(message)??false,setTimer=globalThis.setTimeout,clearTimer=globalThis.clearTimeout,x=null}={}) {
|
||||
let selectedDate=null;
|
||||
let reviewing=false;
|
||||
let overviewing=false;
|
||||
@@ -490,6 +508,8 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
||||
let blockedReviewOpen=false;
|
||||
let reconciliation=null;
|
||||
let loadState=null;
|
||||
let unplanReceipt=null;
|
||||
let unplanTimer=null;
|
||||
function renderOfflineState(value=loadState) {
|
||||
const notice=qs('#week-offline-snapshot'),retry=qs('#retry-week-live'),capacity=qs('#open-week-capacity-import');
|
||||
const offline=Boolean(value?.offline_snapshot);
|
||||
@@ -616,6 +636,36 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function dismissUnplanReceipt({focus=false}={}) {
|
||||
if(unplanTimer){clearTimer?.(unplanTimer);unplanTimer=null;}
|
||||
unplanReceipt=null;
|
||||
const receipt=qs('#week-unplan-receipt'),undo=qs('#undo-week-unplan');
|
||||
if(receipt)receipt.hidden=true;
|
||||
if(undo)undo.hidden=true;
|
||||
if(focus)qs('#edit-week-plan')?.focus?.();
|
||||
}
|
||||
function showUnplanReceipt(title) {
|
||||
const receipt=qs('#week-unplan-receipt'),undo=qs('#undo-week-unplan');
|
||||
if(receipt){receipt.textContent=title+' removed from Week Ahead.';receipt.hidden=false;}
|
||||
if(undo){undo.hidden=false;undo.focus?.();}
|
||||
if(unplanTimer)clearTimer?.(unplanTimer);
|
||||
unplanTimer=setTimer?.(()=>dismissUnplanReceipt({focus:true}),10000)||null;
|
||||
}
|
||||
async function undoUnplan() {
|
||||
const receipt=unplanReceipt;
|
||||
if(!receipt||!controller.restore?.(receipt))return false;
|
||||
dismissUnplanReceipt();renderReview();renderPass();
|
||||
try{
|
||||
await controller.flush();
|
||||
renderReview();
|
||||
qs('#week-review-status').textContent='Restored to Week Ahead.';
|
||||
}catch(error){
|
||||
qs('#week-review-status').textContent=(error.message||'Week Ahead sync is unavailable.')+' Restore remains saved on this phone.';
|
||||
}
|
||||
qs('#week-review-days').querySelector?.(`[data-week-unplan="${receipt.id}"]`)?.focus?.();
|
||||
return true;
|
||||
}
|
||||
qs('#undo-week-unplan')?.addEventListener('click',undoUnplan);
|
||||
function renderReview() {
|
||||
const value=controller.review(),root=qs('#week-review-days'),duplicates=qs('#week-review-duplicates');
|
||||
const readOnly=Boolean(controller.offline?.());
|
||||
@@ -638,7 +688,8 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
||||
const load=day.planned_minutes+(capacity?' of '+capacity:'')+' min'+(day.overloaded?' · over capacity':'');
|
||||
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>':
|
||||
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>':
|
||||
'<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>':'';
|
||||
@@ -667,6 +718,20 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
|
||||
controller.flush().then(()=>{renderReview();qs('#mobile-week-summary').textContent=controller.summary();})
|
||||
.catch(error=>{qs('#week-review-status').textContent=(error.message||'Week Ahead sync is unavailable.')+' Changes remain on this phone.';});
|
||||
}));
|
||||
root.querySelectorAll('[data-week-unplan]').forEach(button=>button.addEventListener('click',async event=>{
|
||||
const id=event.currentTarget.dataset.weekUnplan,item=getItem(id)||controller.item?.(id);
|
||||
const receipt=controller.unplan?.(id);
|
||||
if(!receipt)return;
|
||||
unplanReceipt=receipt;
|
||||
renderReview();renderPass();showUnplanReceipt(String(item?.title||'Work'));
|
||||
try{
|
||||
await controller.flush();
|
||||
renderReview();
|
||||
qs('#mobile-week-summary').textContent=controller.summary();
|
||||
}catch(error){
|
||||
qs('#week-review-status').textContent=(error.message||'Week Ahead sync is unavailable.')+' Removal remains saved on this phone.';
|
||||
}
|
||||
}));
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user