diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7189de5..ff73238 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: pip install -r requirements-e2e.txt python3 -m playwright install --with-deps chromium - name: Exercise packaged mobile work journeys - run: python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py tests/e2e/test_mobile_search_preview_navigation.py tests/e2e/test_mobile_find_work_release.py tests/e2e/test_mobile_home_bootstrap_release.py tests/e2e/test_mobile_sign_out_release.py tests/e2e/test_mobile_today_handoff_release.py tests/e2e/test_mobile_today_wrap_up_release.py tests/e2e/test_mobile_today_summary_release.py tests/e2e/test_mobile_tomorrow_conflict_release.py tests/e2e/test_mobile_wrap_up_handoff_release.py -q + run: python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py tests/e2e/test_mobile_search_preview_navigation.py tests/e2e/test_mobile_find_work_release.py tests/e2e/test_mobile_home_bootstrap_release.py tests/e2e/test_mobile_sign_out_release.py tests/e2e/test_mobile_today_handoff_release.py tests/e2e/test_mobile_today_wrap_up_release.py tests/e2e/test_mobile_today_summary_release.py tests/e2e/test_mobile_tomorrow_conflict_release.py tests/e2e/test_mobile_week_ahead_release.py tests/e2e/test_mobile_wrap_up_handoff_release.py -q release-candidate: runs-on: ubuntu-latest diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 9e1800d..01c78c9 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -241,6 +241,10 @@ textarea { resize: vertical; min-height: 120px; } .plan-today-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } .plan-today-header h2, .plan-today-header p { margin-top:0; } .plan-today-header button { min-width:44px; min-height:44px; } +.week-plan-dates { display:flex; gap:8px; margin:8px 0 12px; padding:2px 0 8px; overflow-x:auto; overscroll-behavior-inline:contain; } +.week-plan-dates[hidden] { display:none; } +.week-plan-dates button { min-height:44px; min-width:86px; flex:0 0 auto; padding:6px 10px; } +.week-plan-dates button[aria-current="date"] { color:#bfdbfe; background:#17365a; border-color:#60a5fa; } .tomorrow-conflict-review { margin-top:14px; } .tomorrow-conflict-plans { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; } .tomorrow-conflict-plans > section { min-width:0; padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 60f793f..2189cfb 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -229,6 +229,7 @@ deadlineBadge: qs('#mobile-deadline-count'), onSelectQueue:(name, row) => name === 'recaps' ? qs('#open-today-recaps').click() : name === 'find' ? qs('#find-work').click() : + name === 'week' ? openWeekPlanner(row) : name === 'tomorrow' ? openTomorrowPlanner(row) : mobileQueueLauncher.open(name), detour:() => timerView, overlays: mobileTaskOverlays, @@ -380,6 +381,7 @@ const todayRollover = createTodayRollover(); let planningTomorrow = false; let latestTodayPlan = null; + const tomorrowPlan = createTomorrowPlan({ fetchJson:fetchReviewJson, localDate:todayRollover.localDate, @@ -387,6 +389,10 @@ storage:localStorage, getLogin:() => planningOwnerLogin, }); + const weekPlan = createWeekPlan({ + fetchJson:fetchReviewJson,localDate:todayRollover.localDate,timeZone:todayRollover.timeZone, + }); + function openWeekPlanner(trigger) { planningTomorrow=false; return weekWorkflow.open(trigger); } function renderTomorrowQueueSummary(value) { qs('#mobile-tomorrow-summary').textContent = value ? tomorrowPlan.summary(value) : tomorrowPlan.summary(); } @@ -445,27 +451,9 @@ storage: localStorage, getLogin: () => planningOwnerLogin, }); - async function promoteTomorrowIfDue(plan) { - try { - await tomorrowPlan.load(); - const promoted = await tomorrowPlan.promote(plan.revision); - if (!promoted) return false; - todayWork.replace(promoted.ids); - todayWork.replacePlanning({ - capacity_minutes:promoted.capacity_minutes ?? null, - estimates:promoted.estimates || {}, - }); - refreshMyWorkView(); - warmTodayOffline(); - qs('#my-work-action-status').textContent = - 'Your saved Tomorrow plan is now Today.'; - return true; - } catch (error) { - qs('#my-work-action-status').textContent = - `${error.message || 'Tomorrow needs review before promotion.'} Open Plan Tomorrow to review.`; - return false; - } - } + const weekWorkflow=createWeekPlanWorkflow({controller:weekPlan,qs,getLogin:()=>planningOwnerLogin, + openPlanner:openPlanToday,escapeHtml,escapeAttribute:escAttr,todayWork, + refresh:refreshMyWorkView,warm:warmTodayOffline}); const todaySync = createTodaySync({ storage: localStorage, getLogin: () => planningOwnerLogin, @@ -480,7 +468,7 @@ if (!planningOwnerLogin) return; latestTodayPlan = plan; const startDayLaunch = window.location.hash === '#/my-work/start-day'; - promoteTomorrowIfDue(plan).finally(() => { + weekWorkflow.promote(plan).finally(() => { if (!startDayLaunch) return; window.history.replaceState({}, '', '#/my-work/today'); openMobileStartDay(); @@ -523,7 +511,7 @@ documentObject:document, check:() => Promise.all([ syncPendingTomorrow(), - latestTodayPlan ? promoteTomorrowIfDue(latestTodayPlan) : false, + latestTodayPlan ? weekWorkflow.promote(latestTodayPlan) : false, ]), }); const todayHandoff = createTodayHandoff({ @@ -2709,6 +2697,7 @@ } planToday.cancel(); planningTomorrow = false; + weekWorkflow.clear(); qs('#tomorrow-conflict-review').hidden = true; qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode'); qs('#plan-today-sheet').hidden = true; @@ -2765,7 +2754,8 @@ const planToday = createPlanToday({ identity: item => todayWork.identity(item), limit: todayWork.limit, - save: plan => planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan), + save: plan => weekWorkflow.active() ? weekWorkflow.save(plan) : + (planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan)), start: () => { qs('[data-work-filter="today"]').click(); startTodaySession(); @@ -3070,13 +3060,14 @@ } function openPlanToday(trigger, navigate = true, actualMinutes = null) { - if (!planningOwnerLogin) { + if (!planningOwnerLogin && !weekWorkflow.active()) { qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.'; return; } if (trigger) planTodayTrigger = trigger; - qs('#plan-today-title').textContent = planningTomorrow ? 'Plan Tomorrow' : - (pendingProtectToday ? 'Protect Today' : (rolloverReviewPlan ? 'New day review' : 'Plan Today')); + const weekCopy=weekWorkflow.copy(); + qs('#plan-today-title').textContent = weekCopy ? weekCopy.title : (planningTomorrow ? 'Plan Tomorrow' : + (pendingProtectToday ? 'Protect Today' : (rolloverReviewPlan ? 'New day review' : 'Plan Today'))); if (actualMinutes) pendingPlanActualMinutes = actualMinutes; if (navigate) { taskOverlayHistory.open('plan-today'); @@ -3093,22 +3084,24 @@ pendingPlanActualMinutes = null; const protectProposal = pendingProtectToday; const tomorrow = planningTomorrow ? tomorrowPlan.state() : null; + const weekDay = weekWorkflow.day(); const availablePlanningItems = [...todayMyWork, ...activeMyWork].filter((item, index, items) => items.findIndex(candidate => todayWork.identity(candidate) === todayWork.identity(item)) === index ); - const selected = planningTomorrow ? availablePlanningItems.filter(item => - tomorrow.ids.includes(todayWork.identity(item)) + const selected = (planningTomorrow || weekCopy) ? availablePlanningItems.filter(item => + (weekDay || tomorrow).ids.includes(todayWork.identity(item)) ) : (protectProposal?.selected || todayMyWork); - planToday.open(selected, activeMyWork, planningTomorrow ? tomorrow : todayWork.planning(), recommendations); - qs('#today-plan-heading').textContent = planningTomorrow ? 'Tomorrow, in order' : 'Today, in order'; - qs('.plan-today-available').firstChild.textContent = planningTomorrow ? 'Available tomorrow' : 'Available today'; - qs('#build-today-plan').textContent = planningTomorrow ? 'Build my Tomorrow' : 'Build my Today'; - qs('#save-and-start-today').hidden = planningTomorrow; + planToday.open(selected, activeMyWork, weekDay || (planningTomorrow ? tomorrow : todayWork.planning()), recommendations); + qs('#today-plan-heading').textContent = weekCopy ? weekCopy.heading : (planningTomorrow ? 'Tomorrow, in order' : 'Today, in order'); + qs('.plan-today-available').firstChild.textContent = weekCopy ? weekCopy.available : (planningTomorrow ? 'Available this day' : 'Available today'); + qs('#build-today-plan').textContent = weekCopy ? weekCopy.build : (planningTomorrow ? 'Build my Tomorrow' : 'Build my Today'); + qs('#save-and-start-today').hidden = planningTomorrow || Boolean(weekCopy); pendingProtectToday = null; qs('#discard-recap-replan').hidden = !todayRecapView.pendingReplan(); qs('#plan-today-error').textContent = ''; qs('#plan-today-sheet').hidden = false; document.body.classList.add('task-overlay-open'); + weekWorkflow.renderDates(); renderPlanToday(); if (protectProposal) qs('#plan-today-build-status').textContent = protectProposal.summary + (protectProposal.displaced.length ? '. Displaced work remains unchanged until you save.' : '. Review estimates and capacity before saving.'); diff --git a/frontend/index.html b/frontend/index.html index 703ad70..c782d61 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -460,6 +460,7 @@

Plan Today

Choose and order the work you want to finish next.

+