stackchain-dashboard/frontend/today-wrap-up.js
timmy b3a359760c
Some checks failed
CI / lint (pull_request) Successful in 3m20s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Failing after 3m9s
CI / release-candidate (pull_request) Has been skipped
feat: share private Today summaries (Closes #1090)
2026-08-18 18:39:01 +00:00

130 lines
5.0 KiB
JavaScript
Raw Permalink 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, {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.');
}
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 = {};
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 '<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 = {}, 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 tomorrows 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 + ' scheduled for tomorrow · ' + result.left + ' left in Today.';
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;
}