stackchain-dashboard/frontend/today-wrap-up.js
timmy 110fd860b3
All checks were successful
CI / lint (pull_request) Successful in 2m44s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 1m36s
CI / release-candidate (pull_request) Has been skipped
feat: schedule unfinished work during Today wrap-up (Closes #990)
2026-08-16 22:07:04 +00:00

123 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function createTodayWrapUp({ todayWork, laterWork, todaySync }) {
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 wake = laterWork.presetUntil('tomorrow');
let scheduled = 0;
for (const item of items) {
const identity = todayWork.identity(item);
if (!selected.has(identity) || !todayWork.contains(item)) continue;
const deferred = laterWork.defer(item, wake);
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.');
}
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, wake_at:wake.toISOString() };
}
return { open, choose, snapshot, finish };
}
function createTodayWrapUpView({ controller, qs, escapeHtml, onComplete = () => {}, onClose = () => {} }) {
let actualMinutes = {};
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 '<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>';
}).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));
});
}
function open(items, recapActualMinutes = {}) {
actualMinutes = { ...recapActualMinutes };
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 tomorrows plan…';
try {
const result = await controller.finish();
qs('#my-work-action-status').textContent = result.scheduled + ' scheduled for tomorrow · ' + result.left + ' left in Today.';
close();
onComplete(result, actualMinutes);
} 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;
}