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 @@ + +