diff --git a/frontend/dashboard.css b/frontend/dashboard.css index af7c946..9e1800d 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -241,6 +241,25 @@ 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; } +.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; } +.tomorrow-conflict-plans h4 { margin:0 0 8px; } +.tomorrow-conflict-plan-summary { margin:0 0 8px; color:#bfdbfe; } +.tomorrow-conflict-plan-list { margin:0; padding-left:22px; overflow-wrap:anywhere; } +.tomorrow-conflict-plan-list li + li { margin-top:6px; } +.tomorrow-conflict-actions { display:flex; gap:8px; margin-top:14px; } +.tomorrow-conflict-actions button { min-height:44px; flex:1 1 0; } +.tomorrow-conflict-status { min-height:1.4em; margin-top:8px; } +.plan-today-sheet.tomorrow-conflict-mode .mobile-plan-today-nav, +.plan-today-sheet.tomorrow-conflict-mode #plan-today-fit, +.plan-today-sheet.tomorrow-conflict-mode #plan-today-selected, +.plan-today-sheet.tomorrow-conflict-mode #plan-today-available-work, +.plan-today-sheet.tomorrow-conflict-mode .plan-today-actions { display:none; } +@media (max-width:480px) { + .tomorrow-conflict-plans { grid-template-columns:1fr; } + .tomorrow-conflict-actions { flex-direction:column; } +} .mobile-plan-today-nav { position:sticky; top:0; z-index:5; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:4px; margin:0 -18px 10px; padding:4px 18px; background:rgba(11,21,38,.98); border-block:1px solid #2a496e; } .mobile-plan-today-nav button { min-width:0; min-height:44px; padding:4px; border-color:transparent; font-size:12px; } .mobile-plan-today-nav button[aria-current="location"] { color:#bfdbfe; background:#17365a; border-color:#31577f; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 90c5712..198d6e4 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -400,15 +400,19 @@ return saved; }).catch(error => { const conflict = tomorrowPlan.conflict(); + if (conflict) renderTomorrowQueueSummary({ids:[],sync_pending:false,conflict:true}); + qs('#mobile-tomorrow-summary').textContent = conflict ? 'Conflict · review required' : qs('#mobile-tomorrow-summary').textContent; qs('#my-work-action-status').textContent = conflict ? - 'Another device changed Tomorrow. Your phone plan is preserved; reopen Plan Tomorrow to review it.' : + 'Another device changed Tomorrow. Both plans are preserved; open Tomorrow to choose one.' : `${error.message || 'Tomorrow sync is unavailable.'} Saved on this phone · sync pending.`; return false; }); } async function refreshTomorrowQueueSummary() { try { - renderTomorrowQueueSummary(await tomorrowPlan.load()); + const loaded = await tomorrowPlan.load(); + if (tomorrowPlan.conflict()) qs('#mobile-tomorrow-summary').textContent = 'Conflict · review required'; + else renderTomorrowQueueSummary(loaded); return true; } catch (_error) { qs('#mobile-tomorrow-summary').textContent = 'Unavailable · tap to retry'; @@ -2698,6 +2702,8 @@ } planToday.cancel(); planningTomorrow = false; + qs('#tomorrow-conflict-review').hidden = true; + qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode'); qs('#plan-today-sheet').hidden = true; document.body.classList.remove('task-overlay-open'); if (planTodayTrigger?.dataset.mobileQueue === 'tomorrow') { @@ -3024,6 +3030,38 @@ }); let pendingPlanActualMinutes = null; + function tomorrowConflictPlanMarkup(value) { + const ids = value?.ids || []; + const estimates = value?.estimates || {}; + const estimated = ids.reduce((total, id) => total + (Number(estimates[id]) || 0), 0); + const capacity = Number(value?.capacity_minutes) || 0; + const summary = ids.length ? `${ids.length} planned` + + (estimated && capacity ? ` · ${estimated} of ${capacity} min` : '') : 'Nothing planned'; + const available = [...todayMyWork, ...activeMyWork]; + const items = ids.map(id => { + const item = available.find(candidate => todayWork.identity(candidate) === id); + const label = item?.title || item?.key || id; + const estimate = Number(estimates[id]) || 0; + return `
  • ${escapeHtml(label)}${estimate ? ` · ${estimate} min` : ''}
  • `; + }).join(''); + return `

    ${summary}

    ` + + (items ? `
      ${items}
    ` : '

    No work selected.

    '); + } + + function showTomorrowConflict(conflict) { + qs('#plan-today-title').textContent = 'Resolve Tomorrow conflict'; + qs('#tomorrow-conflict-phone-plan').innerHTML = tomorrowConflictPlanMarkup(conflict.local); + qs('#tomorrow-conflict-server-plan').innerHTML = tomorrowConflictPlanMarkup(conflict.remote); + qs('#tomorrow-conflict-status').textContent = ''; + qs('#tomorrow-conflict-review').hidden = false; + qs('#plan-today-sheet').classList.add('tomorrow-conflict-mode'); + qs('#plan-today-sheet').hidden = false; + document.body.classList.add('task-overlay-open'); + const keep = qs('#keep-phone-tomorrow'); + keep.focus(); + requestAnimationFrame(() => keep.focus()); + } + function openPlanToday(trigger, navigate = true, actualMinutes = null) { if (!planningOwnerLogin) { qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.'; @@ -3037,6 +3075,13 @@ taskOverlayHistory.open('plan-today'); return; } + const conflict = planningTomorrow ? tomorrowPlan.conflict() : null; + if (conflict) { + showTomorrowConflict(conflict); + return; + } + qs('#tomorrow-conflict-review').hidden = true; + qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode'); const recommendations = actualMinutes || pendingPlanActualMinutes || todayRecapView.pendingReplan()?.actual_minutes; pendingPlanActualMinutes = null; const protectProposal = pendingProtectToday; @@ -7758,6 +7803,35 @@ qs('#refresh').addEventListener('click', load); qs('#plan-today').addEventListener('click', event => openPlanToday(event.currentTarget)); qs('#plan-tomorrow').addEventListener('click', event => openTomorrowPlanner(event.currentTarget)); + qs('#keep-phone-tomorrow').addEventListener('click', async () => { + const keep = qs('#keep-phone-tomorrow'); + const use = qs('#use-server-tomorrow'); + keep.disabled = true; + use.disabled = true; + qs('#tomorrow-conflict-status').textContent = 'Saving this phone’s plan…'; + try { + const saved = await tomorrowPlan.keepLocal(); + renderTomorrowQueueSummary(saved); + qs('#my-work-action-status').textContent = 'This phone’s Tomorrow plan is saved to your account. Today was not changed.'; + closePlanToday(); + } catch (error) { + const conflict = tomorrowPlan.conflict(); + if (conflict) showTomorrowConflict(conflict); + qs('#tomorrow-conflict-status').textContent = error?.status === 409 ? + 'Tomorrow changed again. Both latest plans are still preserved; choose again.' : + `${error.message || 'Tomorrow could not be saved.'} Both plans are still preserved.`; + } finally { + keep.disabled = false; + use.disabled = false; + } + }); + qs('#use-server-tomorrow').addEventListener('click', () => { + const adopted = tomorrowPlan.useRemote(); + if (!adopted) return; + renderTomorrowQueueSummary(adopted); + qs('#my-work-action-status').textContent = 'Saved account Tomorrow plan selected. Today was not changed.'; + closePlanToday(); + }); qs('#cancel-plan-today').addEventListener('click', closePlanToday); qs('#plan-today-sheet').addEventListener('click', event => { if (event.target === qs('#plan-today-sheet')) closePlanToday(); diff --git a/frontend/index.html b/frontend/index.html index 3908fca..4f5b6a3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -449,6 +449,26 @@

    Plan Today

    Choose and order the work you want to finish next.

    +