feat: carry wrap-up work into Tomorrow (Closes #1174)
CI / lint (pull_request) Successful in 3m38s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 3m56s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-20 09:49:08 +00:00
1 parent 58c2d3f5d8
commit c99d03b9bc
5 files changed
+167 -40

No files matched your search

+3 -1
View File
@@ -2044,11 +2044,13 @@
enqueueDurably:message => authoredOutbox.enqueueDurably(message),
});
todaySummaryView.resume();
const todayWrapUpView = setupTodayWrapUp({ todayWork, laterWork, todaySync, qs, escapeHtml,
const todayWrapUpView = setupTodayWrapUp({ todayWork, tomorrowPlan, todaySync, qs, escapeHtml,
onComplete:(_result, _actualMinutes, workedItems, tomorrowItems) => {
todayRecapView['completeReplan']();
refreshMyWorkView();
warmTodayOffline();
renderTomorrowQueueSummary();
syncPendingTomorrow();
todaySummaryView.open(workedItems, tomorrowItems);
},
});
+1 -1
View File
@@ -567,7 +567,7 @@
<div><div class="small">End your day intentionally</div><h2 id="today-wrap-up-title">Wrap up Today</h2></div>
<button id="close-today-wrap-up" type="button">Not now</button>
</div>
<p class="small muted">Choose unfinished work to schedule for tomorrow at 09:00. Unchecked work stays in Today.</p>
<p class="small muted">Carry selected work into the ordered Tomorrow plan. Unselected work stays in Today.</p>
<div id="today-wrap-up-status" class="small" role="status" aria-live="polite"></div>
<div id="today-wrap-up-items" class="today-wrap-up-items"></div>
<div class="today-wrap-up-actions">
+42 -12
View File
@@ -1,4 +1,4 @@
function createTodayWrapUp({ todayWork, laterWork, todaySync }) {
function createTodayWrapUp({ todayWork, tomorrowPlan, todaySync, limit = 5 }) {
let items = [];
const selected = new Set();
@@ -28,16 +28,39 @@ function createTodayWrapUp({ todayWork, laterWork, todaySync }) {
}
async function finish() {
const wake = laterWork.presetUntil('tomorrow');
let scheduled = 0;
for (const item of items) {
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 (!selected.has(identity) || !todayWork.contains(item)) continue;
const deferred = laterWork.defer(item, wake, {handoff:'today'});
if (deferred !== 'deferred') throw new Error('Tomorrow could not be saved. Your Today plan is unchanged.');
if (!todaySync.enqueue('remove', identity)) {
laterWork.restore?.(item);
throw new Error('Today sync could not be queued. Your Today plan is unchanged.');
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.');
@@ -45,7 +68,13 @@ function createTodayWrapUp({ todayWork, laterWork, todaySync }) {
scheduled += 1;
}
await todaySync.flush();
return { scheduled, left:items.length - scheduled, wake_at:wake.toISOString() };
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 };
@@ -69,7 +98,7 @@ function createTodayWrapUpView({ controller, qs, escapeHtml, onComplete = () =>
return '<div class="today-wrap-up-item"><span><strong>' + escapeHtml(title) + '</strong>' +
'<span class="small muted">' + escapeHtml(context) + '</span></span><label><input type="checkbox" ' +
'data-wrap-up-identity="' + escapeHtml(row.identity) + '"' + (row.schedule ? ' checked' : '') +
'> Tomorrow 09:00</label></div>';
'> Carry to Tomorrow</label></div>';
}).join('') || '<p class="small">Nothing unfinished remains in Today.</p>';
qs('#today-wrap-up-items').querySelectorAll('[data-wrap-up-identity]').forEach(input => {
input.addEventListener('change', () => controller.choose(input.dataset.wrapUpIdentity, input.checked));
@@ -97,7 +126,8 @@ function createTodayWrapUpView({ controller, qs, escapeHtml, onComplete = () =>
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 + ' scheduled for tomorrow · ' + result.left + ' left in Today.';
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) {