From ea1b5a77f56e9f243d1935246d72a83bc9d310cf Mon Sep 17 00:00:00 2001 From: timmy Date: Thu, 20 Aug 2026 20:39:53 +0000 Subject: [PATCH] feat: plan Search results into Week Ahead (Closes #1194) --- README.md | 7 ++ frontend/dashboard.css | 14 +++ frontend/dashboard.js | 16 +++ frontend/index.html | 19 ++++ frontend/search-week-plan.js | 115 ++++++++++++++++++++++ frontend/service-worker.js | 1 + frontend/week-plan.js | 23 ++++- src/frontend_bundle.py | 2 +- tests/e2e/test_mobile_search_week_plan.py | 43 ++++++++ tests/test_search_week_plan.py | 101 +++++++++++++++++++ tests/test_search_week_plan_ui.py | 51 ++++++++++ tests/test_service_worker.py | 1 + tests/test_week_plan_frontend.py | 26 +++++ 13 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 frontend/search-week-plan.js create mode 100644 tests/e2e/test_mobile_search_week_plan.py create mode 100644 tests/test_search_week_plan.py create mode 100644 tests/test_search_week_plan_ui.py diff --git a/README.md b/README.md index f230cb3..c09717d 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,13 @@ Today or interrupting active work. Cancel and browser Back preserve the Search p confirmation claims only when needed, syncs the Later plan across devices, and returns to the preserved query, filters, results, and scroll position. If assignment succeeds but Later storage fails, the issue remains recoverable in My Work and the dashboard reports the partial outcome instead of claiming success. +Eligible open issue previews also expose **Plan ahead**: choose one of the next seven local dates, review +that day's current load and capacity, and enter a required estimate without leaving Search. Existing Week +Ahead placement is moved only after an explicit day change, never duplicated; full days are blocked and +over-capacity admission requires a second confirmation. The action claims unassigned work only after these +checks, stages the canonical issue through the account-bound Week Ahead outbox, and preserves the Search +preview through cancel or browser Back. Offline saves report **sync pending**; if assignment succeeds but +local planning fails, the dashboard reports **Assigned, not planned** and leaves the issue recoverable in My Work. Commentable mobile Search previews support camera capture and gallery selection for up to five ordered photos, including captions, crop/annotation/redaction review, and metadata-stripping re-encoding. Operators can queue photo-only, text-only, or mixed replies without leaving their Search pass, including while offline. diff --git a/frontend/dashboard.css b/frontend/dashboard.css index dfd98ac..a8201c8 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -50,6 +50,20 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex .app-menu-panel { display:flex; gap:10px; align-items:center; flex-wrap:wrap; } #open-insights { display:none; } button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #2a496e; color:#e5e7eb; padding:8px 12px; border-radius:10px; cursor:pointer; } +.search-week-plan-sheet { position:fixed; inset:0; z-index:118; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); } +.search-week-plan-sheet[hidden] { display:none; } +.search-week-plan-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #60a5fa; border-radius:18px 18px 0 0; background:#0b1526; } +.search-week-plan-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } +.search-week-plan-panel h2 { margin:.2rem 0; } +.search-week-plan-panel fieldset { min-width:0; margin:12px 0; padding:0; border:0; } +.search-week-plan-days { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; } +.search-week-plan-days button { display:grid; min-width:0; min-height:44px; text-align:left; overflow-wrap:anywhere; } +.search-week-plan-days button[aria-pressed="true"] { border-color:#60a5fa; box-shadow:0 0 0 1px #60a5fa inset; } +.search-week-plan-days small { color:#9fb3c8; } +.search-week-plan-estimate { display:grid; gap:6px; } +.search-week-plan-estimate input, #confirm-search-week-plan, #cancel-search-week-plan { box-sizing:border-box; min-height:44px; } +#confirm-search-week-plan { width:100%; } +@media (max-width:359px) { .search-week-plan-days { grid-template-columns:1fr; } } .notification-undo { position:fixed; } .notification-undo button { min-height:44px; } .notification-undo, .today-completion-undo { position:fixed; z-index:110; left:50%; bottom:calc(88px + env(safe-area-inset-bottom)); transform:translateX(-50%); box-sizing:border-box; width:min(520px,calc(100vw - 24px)); display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 12px; border:1px solid #60a5fa; border-radius:12px; background:#10233d; box-shadow:0 12px 36px rgba(0,0,0,.5); overflow-wrap:anywhere; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 8606b63..404417e 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -5611,6 +5611,7 @@ const claimButton = qs('#claim-search-result'); const deferButton = qs('#defer-search-result'); const queueButton = qs('#queue-search-result'); + const planButton = qs('#plan-search-result'); const startButton = qs('#start-search-result'); const shareButton = qs('#share-search-result'); @@ -5627,6 +5628,8 @@ deferButton.hidden = true; deferButton.disabled = false; queueButton.hidden = true; + planButton.hidden = true; + planButton.disabled = false; startButton.hidden = true; startButton.disabled = false; shareButton.disabled = true; @@ -5675,6 +5678,9 @@ queueButton.hidden = !(detail.kind === 'issue' && detail.state === 'open' && (detail.claimable || detail.assigned_to_me)); queueButton.textContent = detail.assigned_to_me ? 'Add to Today' : 'Assign & add to Today'; + planButton.hidden = !searchWeekPlan.eligible(detail); + planButton.textContent = detail.assigned_to_me ? 'Plan ahead' : 'Assign & plan ahead'; + planButton.disabled = searchWeekPlan.pending(); startButton.hidden = !(detail.reviewable || (detail.kind === 'issue' && (detail.reopenable || (detail.state === 'open' && (detail.claimable || detail.assigned_to_me))))); startButton.textContent = detail.reviewable ? 'Review now' : detail.reopenable ? 'Reopen & resume' : @@ -5732,6 +5738,15 @@ session:[()=>commandSearchState, commandSearch, item=>taskOverlayHistory.update({preview:item})], onState: renderSearchPreview, }); + const searchWeekPlan = createSearchWeekPlan({ + week:weekPlan,claim:detail=>searchPreview.claim(detail),accept:acceptClaimedIssue, + identity:item=>todayWork.identity(item), + }); + createSearchWeekPlanUI({planner:searchWeekPlan,document,window,escapeHtml,escapeAttribute:escAttr, + getDetail:()=>searchPreviewDetail, + announce:message=>{qs('#search-preview-status').textContent=message;}, + onPlanned:item=>{searchPreviewDetail={...item,assigned_to_me:true,claimable:false};}, + }); searchBatchPlanning = mountSearchBatchPlanning( document, createBatchFindWork, todayWork, ()=>planningOwnerLogin, fetchReviewJson, searchPreviewPath, queueToday, todaySync, acceptClaimedIssue, i=>commandItems[i]?.result, @@ -6089,6 +6104,7 @@ (!detail.claimable && !detail.assigned_to_me) || searchDefer.pending()) return; laterPicker.open(detail, event.currentTarget, 'search'); }); + qs('#queue-search-result').addEventListener('click', async () => { const detail = searchPreviewDetail; if (!detail || detail.kind !== 'issue' || detail.state !== 'open' || diff --git a/frontend/index.html b/frontend/index.html index 9cbf82e..951eeb1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -853,6 +853,7 @@ + @@ -862,6 +863,23 @@ + +