diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 0e48ad8..df19e86 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -82,6 +82,20 @@ textarea { resize: vertical; min-height: 120px; } .event { padding: 8px 0; border-bottom: 1px solid #1b2d45; } .event:last-child { border-bottom: 0; } .my-work { grid-column: 1 / -1; } +.plan-today-sheet { position:fixed; inset:0; z-index:75; display:flex; justify-content:flex-end; background:rgba(5,12,21,.78); backdrop-filter:blur(4px); } +.plan-today-sheet[hidden] { display:none; } +.plan-today-panel { box-sizing:border-box; width:min(620px,100%); height:100%; overflow:auto; overflow-x:hidden; padding:18px; background:#0b1526; border-left:1px solid #2a496e; } +.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, .plan-today-list button, .plan-today-candidates button { min-height:44px; } +.plan-today-capacity { position:sticky; top:0; z-index:2; margin:8px 0; padding:10px 12px; border:1px solid #31577f; border-radius:10px; background:#10233a; font-weight:700; } +.plan-today-error { min-height:1.4em; color:#fca5a5; } +.plan-today-list, .plan-today-candidates { display:grid; gap:8px; } +.plan-today-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:center; padding:10px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; } +.plan-today-item-copy { min-width:0; overflow-wrap:anywhere; } +.plan-today-item-actions { display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end; } +.plan-today-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:16px -6px -6px; padding:12px 6px; padding-bottom:calc(12px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; } +.plan-today-actions button { min-height:44px; width:100%; } .my-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; } .work-settings { width:100%; } .work-settings > summary { display:none; } @@ -335,6 +349,10 @@ textarea { resize: vertical; min-height: 120px; } .work-settings:not([open]) > .work-settings-panel { display:none; } .work-settings-panel { display:grid; gap:10px; margin-top:8px; } .my-work-list { grid-template-columns:1fr; } + .plan-today-panel { width:100%; border-left:0; padding:14px; } + .plan-today-item { grid-template-columns:1fr; } + .plan-today-item-actions { display:grid; grid-template-columns:repeat(3,1fr); width:100%; } + .plan-today-item-actions button { min-width:0; padding-inline:4px; } .work-filters { width:100%; } .work-filter { flex:1 1 calc(50% - 8px); } .review-sheet-panel { width:100%; border-left:0; padding:14px; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 509a870..ab0fec7 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -688,6 +688,87 @@ }, }); + let planTodayTrigger = null; + function planTodayItemMarkup(item, selected, index = -1) { + const id = todayWork.identity(item); + const key = escapeHtml(item.key || (item.repository + '#' + (item.number || ''))); + const title = escapeHtml(item.title || 'Untitled work'); + const controls = selected ? + '
' : + ''; + return '
' + key + '' + title + '
' + controls + '
'; + } + + function renderPlanToday() { + const state = planToday.snapshot(); + qs('#plan-today-capacity').textContent = state.count + ' of ' + state.limit + ' selected'; + qs('#plan-today-list').innerHTML = state.ids.length ? state.ids.map((id, index) => + planTodayItemMarkup(planToday.item(id), true, index) + ).join('') : '

No work selected yet.

'; + const candidates = planToday.candidates(); + qs('#plan-today-candidates').innerHTML = candidates.length ? candidates.map(item => + planTodayItemMarkup(item, false) + ).join('') : '

All available work is already selected.

'; + document.querySelectorAll('[data-plan-add]').forEach(button => button.addEventListener('click', () => { + const result = planToday.toggle(planToday.item(button.dataset.planAdd)); + qs('#plan-today-error').textContent = result === 'full' ? 'Today is full. Remove an item before adding another.' : ''; + renderPlanToday(); + })); + document.querySelectorAll('[data-plan-remove]').forEach(button => button.addEventListener('click', () => { + planToday.toggle(planToday.item(button.dataset.planRemove)); + qs('#plan-today-error').textContent = ''; + renderPlanToday(); + })); + document.querySelectorAll('[data-plan-move]').forEach(button => button.addEventListener('click', () => { + planToday.move(button.dataset.planId, button.dataset.planMove); + renderPlanToday(); + document.querySelector('[data-plan-id="' + CSS.escape(button.dataset.planId) + '"][data-plan-move="' + button.dataset.planMove + '"]')?.focus(); + })); + } + + function closePlanToday() { + planToday.cancel(); + qs('#plan-today-sheet').hidden = true; + document.body.classList.remove('task-overlay-open'); + planTodayTrigger?.focus(); + } + + function saveTodayPlan(ids) { + const previous = todayWork.read(); + const operations = previous.map(id => ['remove', id]).concat(ids.map(id => ['add', id])); + if (!operations.every(([action, id]) => todaySync.enqueue(action, id))) return false; + if (!todayWork.replace(ids)) return false; + refreshMyWorkView(); + todaySync.flush(); + warmTodayOffline(); + qs('#my-work-action-status').textContent = ids.length ? 'Today plan saved in your chosen order.' : 'Today plan cleared.'; + return true; + } + + const planToday = createPlanToday({ + identity: item => todayWork.identity(item), + limit: todayWork.limit, + save: saveTodayPlan, + start: () => { + qs('[data-work-filter="today"]').click(); + workSession.start(); + }, + }); + + function openPlanToday(trigger) { + if (!planningOwnerLogin) { + qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.'; + return; + } + planTodayTrigger = trigger; + planToday.open(todayMyWork, activeMyWork); + qs('#plan-today-error').textContent = ''; + qs('#plan-today-sheet').hidden = false; + document.body.classList.add('task-overlay-open'); + renderPlanToday(); + qs('#cancel-plan-today').focus(); + } + const detailDefer = createDetailDefer({ laterWork, session: workSession, @@ -3402,6 +3483,23 @@ window.addEventListener('online', reconnectLiveData); qs('#refresh').addEventListener('click', load); + qs('#plan-today').addEventListener('click', event => openPlanToday(event.currentTarget)); + qs('#cancel-plan-today').addEventListener('click', closePlanToday); + qs('#plan-today-sheet').addEventListener('click', event => { + if (event.target === qs('#plan-today-sheet')) closePlanToday(); + }); + qs('#save-today-plan').addEventListener('click', () => { + const result = planToday.commit(); + if (result === 'saved') closePlanToday(); + else qs('#plan-today-error').textContent = 'Could not save the plan on this device. Free storage and retry.'; + }); + qs('#save-and-start-today').addEventListener('click', () => { + const result = planToday.commit({ start:true }); + if (result === 'saved') { + qs('#plan-today-sheet').hidden = true; + document.body.classList.remove('task-overlay-open'); + } else qs('#plan-today-error').textContent = 'Could not save the plan on this device. Free storage and retry.'; + }); qs('#start-work-session').addEventListener('click', () => { const sessionItems = selectedWorkFilter === 'today' ? todayMyWork : filterMyWork(lastMyWork, selectedWorkFilter); if (!sessionItems.length) { diff --git a/frontend/index.html b/frontend/index.html index 60343ec..6e4dabd 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -48,6 +48,7 @@
Loading assigned work…
+ @@ -170,6 +171,29 @@ + +