'+
@@ -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…';
diff --git a/frontend/week-plan.js b/frontend/week-plan.js
index 0704143..9075a51 100644
--- a/frontend/week-plan.js
+++ b/frontend/week-plan.js
@@ -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'}:
diff --git a/tests/e2e/test_mobile_week_ahead_release.py b/tests/e2e/test_mobile_week_ahead_release.py
index 3379bb5..0ef3587 100644
--- a/tests/e2e/test_mobile_week_ahead_release.py
+++ b/tests/e2e/test_mobile_week_ahead_release.py
@@ -105,10 +105,11 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
expect(cards).to_have_count(7)
expect(cards.first).to_contain_text("Next up")
expect(cards.first).to_contain_text("Ship mobile capture")
- expect(page.locator("#week-review-status")).to_have_text(
- "Week Ahead overview · no changes made."
- )
+ expect(page.locator("#week-review-status")).to_contain_text("30 min over capacity")
expect(page.locator("#confirm-week-plan")).to_be_hidden()
+ blocked_export = page.locator("#export-saved-week-calendar")
+ expect(blocked_export).to_be_visible()
+ expect(blocked_export).to_be_disabled()
availability = page.locator("#open-week-availability")
expect(availability).to_be_visible()
availability_bounds = availability.bounding_box()
@@ -192,6 +193,22 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
expect(overview).to_be_visible()
expect(planned_item).to_be_focused()
assert len(saved) == writes_before_reflow + 1, "opening and inspecting Week Ahead must not add a write"
+ export_saved = page.locator("#export-saved-week-calendar")
+ expect(export_saved).to_be_visible()
+ expect(export_saved).to_be_enabled()
+ export_bounds = export_saved.bounding_box()
+ assert export_bounds and export_bounds["height"] >= 44
+ writes_before_export = len(saved)
+ export_saved.click()
+ expect(page.locator("#week-calendar-handoff")).to_be_visible()
+ expect(overview).to_be_hidden()
+ expect(page.locator('[data-week-calendar-preview="issue:acme/mobile:41:"]')).to_have_text(
+ "09:00–09:45 · 45 min"
+ )
+ assert len(saved) == writes_before_export, "exporting a saved week must not add a write"
+ page.locator("#back-to-week-review-from-calendar").click()
+ expect(overview).to_be_visible()
+ expect(export_saved).to_be_focused()
for control in (cards.first.locator("[data-week-edit-day]"), edit_week):
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
diff --git a/tests/test_week_calendar.py b/tests/test_week_calendar.py
index 5932888..f928c55 100644
--- a/tests/test_week_calendar.py
+++ b/tests/test_week_calendar.py
@@ -151,11 +151,17 @@ def test_week_calendar_handoff_is_wired_into_confirmation_with_mobile_privacy_co
assert 'id="week-calendar-days"' in index
assert 'id="share-week-calendar"' in index
assert 'id="back-to-week-review-from-calendar"' in index
+ assert 'id="export-saved-week-calendar"' in index
+ assert "Add saved week to calendar" in index
assert "Titles, references, dates, and times are included" in index
assert "Imported busy time is used only until this page reloads" in index
assert "mountWeekCalendarHandoff" in WEEK_CALENDAR.read_text()
- assert "weekCalendar.open(weekPlan.state())" in dashboard
- assert "weekCalendar.close()" in dashboard
+ assert "wc.open(weekPlan.state())" in dashboard
+ assert "getItem:weekFlow.i" in dashboard
+ assert "i:id=>getItem(id)||controller.item?.(id)" in (WEEK_CALENDAR.parent / "week-plan.js").read_text()
+ assert "#export-saved-week-calendar" in WEEK_CALENDAR.read_text()
+ assert "focusReturn=event.currentTarget" in WEEK_CALENDAR.read_text()
+ assert "wc.close()" in dashboard
assert "StackchainWeekCalendar.setAvailabilityProvider(workflow.availability)" in (WEEK_CALENDAR.parent / "week-calendar-import.js").read_text()
assert "Planning around imported busy time" in WEEK_CALENDAR.read_text()
assert '"static/week-calendar.js", "static/week-calendar-import.js", "static/week-plan.js"' in bundle