function createTodayWrapUp({ todayWork, tomorrowPlan, todaySync, limit = 5 }) { let items = []; const selected = new Set(); function open(candidates) { const current = new Set(todayWork.read()); const byIdentity = new Map((candidates || []).map(item => [todayWork.identity(item), item])); items = todayWork.read().flatMap(identity => current.has(identity) && byIdentity.has(identity) ? [byIdentity.get(identity)] : [] ); selected.clear(); return snapshot(); } function choose(identity, schedule) { if (!items.some(item => todayWork.identity(item) === identity)) return false; if (schedule) selected.add(identity); else selected.delete(identity); return true; } function snapshot() { return items.map(item => ({ item, identity:todayWork.identity(item), schedule:selected.has(todayWork.identity(item)), })); } async function finish() { const loaded = await tomorrowPlan.load(); const currentIds = Array.isArray(loaded?.ids) ? [...loaded.ids] : []; const carried = items.filter(item => selected.has(todayWork.identity(item)) && todayWork.contains(item)); if (!carried.length) { return { scheduled:0, left:items.filter(item => todayWork.contains(item)).length, plan_count:currentIds.length, plan_date:loaded?.plan_date, sync_pending:Boolean(loaded?.sync_pending), }; } const combinedIds = [...currentIds]; for (const item of carried) { const identity = todayWork.identity(item); if (!combinedIds.includes(identity)) combinedIds.push(identity); } if (combinedIds.length > limit) { throw new Error(`Tomorrow can hold ${limit} items. Keep ${combinedIds.length - limit} in Today or edit Tomorrow first.`); } const estimates = {...(loaded?.estimates || {})}; const capacityMinutes = loaded?.capacity_minutes ?? null; const plannedMinutes = combinedIds.reduce((total, identity) => total + (Number(estimates[identity]) || 0), 0); if (capacityMinutes !== null && plannedMinutes > capacityMinutes) { throw new Error('Tomorrow is over capacity. Edit the plan before finishing wrap-up.'); } const staged = tomorrowPlan.stage({ids:combinedIds, capacity_minutes:capacityMinutes, estimates}); if (!staged) throw new Error('Tomorrow could not be saved on this device. Your Today plan is unchanged.'); let scheduled = 0; for (const item of carried) { const identity = todayWork.identity(item); if (!todaySync.enqueue('remove', identity)) { throw new Error('Today sync could not be queued. The item remains in Today.'); } if (todayWork.contains(item) && !todayWork.remove(item)) { throw new Error('Today could not be updated. Retry wrap-up.'); } scheduled += 1; } await todaySync.flush(); return { scheduled, left:items.length - scheduled, plan_count:combinedIds.length, plan_date:staged.plan_date, sync_pending:Boolean(staged.sync_pending), }; } return { open, choose, snapshot, finish }; } function createTodayWrapUpView({ controller, qs, escapeHtml, onComplete = () => {}, onClose = () => {} }) { let actualMinutes = {}; let workedItems = []; function close() { qs('#today-wrap-up-sheet').hidden = true; document.body.classList.remove('task-overlay-open'); onClose(); } function render() { const rows = controller.snapshot(); qs('#today-wrap-up-items').innerHTML = rows.map(row => { const title = String(row.item.title || row.identity).slice(0, 180); const context = String(row.item.key || row.item.repository || 'Work item').slice(0, 180); return '
Nothing unfinished remains in Today.
'; qs('#today-wrap-up-items').querySelectorAll('[data-wrap-up-identity]').forEach(input => { input.addEventListener('change', () => controller.choose(input.dataset.wrapUpIdentity, input.checked)); }); } function open(items, recapActualMinutes = {}, recapWorkedItems = []) { actualMinutes = { ...recapActualMinutes }; workedItems = recapWorkedItems.map(item => ({...item})); controller.open(items); qs('#today-wrap-up-status').textContent = ''; render(); qs('#today-wrap-up-sheet').hidden = false; document.body.classList.add('task-overlay-open'); requestAnimationFrame(() => (qs('#today-wrap-up-items input') || qs('#finish-today-wrap-up')).focus()); } async function finish(button) { button.disabled = true; qs('#today-wrap-up-status').textContent = 'Saving tomorrow’s plan…'; try { const tomorrowItems = controller.snapshot().filter(row => row.schedule).map(row => ({ identity:row.identity, title:String(row.item.title || row.identity).slice(0, 180), context:String(row.item.key || row.item.repository || 'Work item').slice(0, 180), })); const result = await controller.finish(); qs('#my-work-action-status').textContent = result.scheduled + ' carried to Tomorrow · ' + result.left + ' left in Today' + (result.sync_pending ? ' · sync pending.' : '.'); close(); onComplete(result, actualMinutes, workedItems, tomorrowItems); } catch (error) { qs('#today-wrap-up-status').textContent = error.message || 'Wrap-up could not be saved. Retry when ready.'; } finally { button.disabled = false; } } function bind() { qs('#close-today-wrap-up').addEventListener('click', close); qs('#finish-today-wrap-up').addEventListener('click', event => finish(event.currentTarget)); } return { open, close, finish, render, bind }; } function setupTodayWrapUp(options) { const controller = createTodayWrapUp(options); const view = createTodayWrapUpView({ ...options, controller }); view.bind(); return view; } if (typeof module !== 'undefined' && module.exports) { createTodayWrapUp.createView = createTodayWrapUpView; createTodayWrapUp.setup = setupTodayWrapUp; module.exports = createTodayWrapUp; }