feat: export saved Week Ahead from overview (Closes #1260)
CI / lint (pull_request) Successful in 4m18s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 6m0s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-22 11:15:10 +00:00
1 parent 9eac3bb958
commit f0a966d3a3
7 files changed
+71 -48

No files matched your search

+1
View File
@@ -371,6 +371,7 @@ textarea { resize: vertical; min-height: 120px; }
#retry-week-live { min-height:44px; width:100%; margin-bottom:8px; border-color:#f59e0b; }
#confirm-week-plan { width:100%; min-height:48px; position:sticky; bottom:0; }
#edit-week-plan { width:100%; min-height:48px; position:sticky; bottom:0; }
#export-saved-week-calendar { width:100%; min-height:48px; margin:0 0 8px; }
#open-week-capacity-import { width:100%; min-height:44px; margin:8px 0 12px; }
#open-week-reflow { width:100%; min-height:48px; margin:0 0 12px; border-color:#60a5fa; }
.week-reflow-review { box-sizing:border-box; width:100%; margin:0 0 14px; padding:14px; border:1px solid #60a5fa; border-radius:12px; background:#10233d; overflow-x:hidden; }
+22 -35
View File
@@ -460,46 +460,33 @@
escapeHtml,escapeAttribute:escAttr,todayWork,
t:v=>v?latestTodayPlan=v:latestTodayPlan,r:refreshMyWorkView,w:warmTodayOffline,
x:currentTodayProgressTarget});
const weekCalendar=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekItem,escapeHtml,escapeAttribute:escAttr,
onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();},
});
const todaySync = createTodaySync({
storage: localStorage,
getLogin: () => planningOwnerLogin,
fetchJson: fetchReviewJson,
coordinator: outboxCoordinator,
onRemoteIds: ids => {
if (!planningOwnerLogin || !todayWork.replace(ids)) return;
refreshMyWorkView();
warmTodayOffline();
},
onRemotePlan: plan => {
if (!planningOwnerLogin) return;
latestTodayPlan = plan;
const wc=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekFlow.i,escapeHtml,escapeAttribute:escAttr,
onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();}});
const todaySync=createTodaySync({storage:localStorage,getLogin: () => planningOwnerLogin,fetchJson:fetchReviewJson,
coordinator:outboxCoordinator,
onRemoteIds:ids=>{if(!planningOwnerLogin||!todayWork.replace(ids))return;refreshMyWorkView();warmTodayOffline();},
onRemotePlan:plan=>{
if(!planningOwnerLogin)return;
latestTodayPlan=plan;
weekFlow.promote(plan).finally(() => {
if (window.location.hash !== '#/my-work/start-day') return;
window.history.replaceState({}, '', '#/my-work/today');
openMobileStartDay();
});
todayWork.replacePlanning({
capacity_minutes: plan.capacity_minutes ?? null,
estimates: plan.estimates || {},
});
const s = todayRollover.reviewState(plan);
if (!['stale', 'legacy'].includes(s)) {
rolloverReviewPlan = null;
qs('#plan-today').textContent = 'Plan Today';
todayWork.replacePlanning({capacity_minutes:plan.capacity_minutes??null,estimates:plan.estimates||{}});
const s=todayRollover.reviewState(plan);
if(!['stale','legacy'].includes(s)){
rolloverReviewPlan=null;
qs('#plan-today').textContent='Plan Today';
return;
}
rolloverReviewPlan = plan;
qs('#plan-today').textContent = 'Review new day';
qs('#my-work-action-status').textContent = 'Review yesterday’s unfinished work before starting today.';
const marker = `stackchain.today-rollover-reviewed.v1.${encodeURIComponent(planningOwnerLogin)}.${todayRollover.localDate()}`;
if (!localStorage.getItem(marker)) {
localStorage.setItem(marker, '1');
setTimeout(() => {
if (!workSession.checkpointed()) openPlanToday(qs('#plan-today'));
}, 0);
rolloverReviewPlan=plan;
qs('#plan-today').textContent='Review new day';
qs('#my-work-action-status').textContent='Review yesterday’s unfinished work before starting today.';
const marker=`stackchain.today-rollover-reviewed.v1.${encodeURIComponent(planningOwnerLogin)}.${todayRollover.localDate()}`;
if(!localStorage.getItem(marker)){
localStorage.setItem(marker,'1');
setTimeout(()=>{if(!workSession.checkpointed())openPlanToday(qs('#plan-today'));},0);
}
},
onStatus: (state, detail = {}) => {
@@ -2706,7 +2693,7 @@
}
planToday.cancel();
planningTomorrow = false;
weekCalendar.close();
wc.close();
weekFlow.clear();
qs('#tomorrow-conflict-review').hidden = true;
qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode');
@@ -7926,7 +7913,7 @@
qs('#week-review-status').textContent=weekPlan.pending()?'Wait for Week Ahead to finish syncing before confirming.':'Move duplicated work to one date before confirming.';
return;
}
weekCalendar.open(weekPlan.state());
wc.open(weekPlan.state());
});
qs('#plan-today-sheet').addEventListener('click', event => {
if (event.target === qs('#plan-today-sheet')) closePlanToday();
+1
View File
@@ -560,6 +560,7 @@
<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="export-saved-week-calendar" type="button" hidden>Add saved week to calendar</button>
<button id="edit-week-plan" type="button" hidden>Edit week</button>
<button id="confirm-week-plan" type="button">Confirm Week Ahead</button>
</section>
+5 -4
View File
@@ -73,7 +73,7 @@
function mountWeekCalendarHandoff({qs,getItem,getAvailability=availabilityProvider,escapeHtml,escapeAttribute,onDone,windowObject=root,navigatorObject=root.navigator,
documentObject=root.document,urlApi=root.URL,FileCtor=root.File}={}){
const handoff=qs('#week-calendar-handoff'),review=qs('#week-review'),daysRoot=qs('#week-calendar-days');
let plan=null;
let plan=null,focusReturn=null;
const selected=()=>new Set(Array.from(daysRoot.querySelectorAll('[data-week-calendar-item]')).filter(input=>input.checked).map(input=>input.value));
const starts=()=>Object.fromEntries(Array.from(daysRoot.querySelectorAll('[data-week-calendar-start]')).map(input=>[input.dataset.weekCalendarStart,input.value]));
const schedule=()=>buildWeekSchedule(plan,starts(),getItem,selected(),getAvailability?.());
@@ -94,10 +94,10 @@
}
function close({back=false}={}){
handoff.hidden=true;
if(back){review.hidden=false;qs('#confirm-week-plan').focus?.();}
if(back){review.hidden=false;const target=focusReturn||qs('#confirm-week-plan');focusReturn=null;target.focus?.();}
}
function open(value){
plan=value;review.hidden=true;handoff.hidden=false;
function open(value,{returnFocus=null}={}){
plan=value;if(returnFocus)focusReturn=returnFocus;review.hidden=true;handoff.hidden=false;
const labels=new Map((value.days||[]).map(day=>[day.plan_date,new Intl.DateTimeFormat('en',{weekday:'short',month:'short',day:'numeric',timeZone:'UTC'}).format(new Date(day.plan_date+'T12:00:00Z'))]));
daysRoot.innerHTML=(value.days||[]).filter(day=>day.ids?.length).map(day=>'<article class="week-calendar-day"><header><h3>'+escapeHtml(labels.get(day.plan_date)||day.plan_date)+
'</h3><label>Start <input type="time" value="09:00" data-week-calendar-start="'+escapeAttribute(day.plan_date)+'"></label></header>'+
@@ -108,6 +108,7 @@
update();qs('#back-to-week-review-from-calendar').focus?.();return true;
}
qs('#back-to-week-review-from-calendar').addEventListener('click',()=>close({back:true}));
qs('#export-saved-week-calendar')?.addEventListener('click',event=>{focusReturn=event.currentTarget;qs('#confirm-week-plan').click();});
qs('#share-week-calendar').addEventListener('click',async()=>{
const button=qs('#share-week-calendar'),current=blocks();if(!current.length)return;
button.disabled=true;qs('#week-calendar-status').textContent='Preparing calendar blocks…';
+14 -4
View File
@@ -936,13 +936,22 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
const confirm=qs('#confirm-week-plan');
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.':
const blockerMessage=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.'
):'';
qs('#week-review-status').textContent=readOnly?'Offline snapshot · viewing only. Retry for live editing.':(overviewing?(pending?'Week Ahead overview · sync pending.':
(blockerMessage||'Week Ahead overview · 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.'
blockerMessage
):'Week Ahead is balanced and ready.')));
confirm.hidden=overviewing||readOnly;
const exportSaved=qs('#export-saved-week-calendar');
if(exportSaved){
exportSaved.hidden=!overviewing||!value.days.some(day=>day.ids.length);
exportSaved.disabled=pending||!value.can_confirm;
}
const reflow=qs('#open-week-reflow');if(reflow)reflow.hidden=!overviewing||readOnly||pending||!value.days.some(day=>day.overloaded);
const editWeek=qs('#edit-week-plan');if(editWeek){editWeek.hidden=!overviewing||readOnly;editWeek.textContent=nextUp?'Edit week':'Plan Week Ahead';}
root.querySelectorAll('[data-week-move]').forEach(button=>button.addEventListener('click',()=>{
@@ -1215,6 +1224,7 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
}
}
const workflow={open,save,advance,confirm,finish,promote,retire,resumePull,flushPull,editWeek,renderDates,renderReview,renderConflict,active:()=>Boolean(selectedDate)||reviewing||Boolean(reconciliation),
i:id=>getItem(id)||controller.item?.(id),
reviewing:()=>reviewing,overviewing:()=>overviewing,selectedDate:()=>selectedDate,
day:()=>reconciliationDay()||(selectedDate?controller.day(selectedDate):(overviewing?{ids:[]}:null)),
copy:()=>reconciliation?{title:"Start today's plan",heading:'Unfinished Today + due Week Ahead',available:'Available today',build:'Build combined Today'}: