function createTodaySummary({ storage = null, getLogin = () => '', share = null, copy = null } = {}) { let draft = null; let draftKey = ''; const storageKey = () => { const login = String(getLogin() || '').trim().toLowerCase(); return login ? 'stackchain.today-summary-draft.v1.' + login : ''; }; const syncAccount = () => { const key = storageKey(); if (key !== draftKey) { draft = null; draftKey = key; } }; const copyRows = rows => rows.map(row => ({ ...row })); const snapshot = () => { syncAccount(); return draft ? { ...draft, worked:copyRows(draft.worked), tomorrow:copyRows(draft.tomorrow), } : null; }; const persist = () => { const key = storageKey(); if (storage && key && draft) storage.setItem(key, JSON.stringify(draft)); }; const validRows = (rows, includeTime) => Array.isArray(rows) && rows.length <= 20 && rows.every(row => row && typeof row.identity === 'string' && row.identity.length > 0 && row.identity.length <= 500 && typeof row.title === 'string' && row.title.length <= 180 && typeof row.context === 'string' && row.context.length <= 180 && typeof row.include === 'boolean' && (!includeTime || (Number.isInteger(row.actual_minutes) && row.actual_minutes >= 0 && row.actual_minutes <= 1440)) ); const load = () => { const key = storageKey(); if (!storage || !key) return null; try { const saved = JSON.parse(storage.getItem(key) || 'null'); if (!saved || !validRows(saved.worked, true) || !validRows(saved.tomorrow, false) || typeof saved.include_actuals !== 'boolean' || typeof saved.note !== 'string' || saved.note.length > 1000) { if (saved !== null) storage.removeItem(key); return null; } return saved; } catch (_error) { try { storage.removeItem(key); } catch (_ignored) {} return null; } }; const cleanRow = (row, includeTime) => { const cleaned = { identity:String(row.identity), title:String(row.title || row.label || row.identity).slice(0, 180), context:String(row.context || '').slice(0, 180), }; if (includeTime) cleaned.actual_minutes = Math.min(1440, Math.max(0, Number(row.actual_minutes) || 0)); cleaned.include = true; return cleaned; }; const selectedLines = (rows, showTime = false) => rows.filter(row => row.include).map(row => '- ' + row.title + (showTime ? ' (' + row.actual_minutes + 'm)' : '') ); const discard = () => { const key = storageKey(); if (storage && key) storage.removeItem(key); draft = null; }; return { begin(worked, tomorrow) { draftKey = storageKey(); draft = { worked:(worked || []).filter(row => row?.identity).slice(0, 20).map(row => cleanRow(row, true)), tomorrow:(tomorrow || []).filter(row => row?.identity).slice(0, 20).map(row => cleanRow(row, false)), include_actuals:false, note:'', }; persist(); return snapshot(); }, snapshot, restore() { draftKey = storageKey(); draft = load(); return Boolean(draft); }, choose(section, identity, include) { syncAccount(); const row = draft?.[section]?.find(candidate => candidate.identity === identity); if (!row || typeof include !== 'boolean') return false; row.include = include; persist(); return true; }, includeActuals(include) { syncAccount(); if (!draft || typeof include !== 'boolean') return false; draft.include_actuals = include; persist(); return true; }, setNote(note) { syncAccount(); if (!draft) return false; draft.note = String(note || '').trim().slice(0, 1000); persist(); return true; }, discard, text() { syncAccount(); if (!draft) return ''; const sections = []; const worked = selectedLines(draft.worked, draft.include_actuals); const tomorrow = selectedLines(draft.tomorrow); if (worked.length) sections.push(['Today', ...worked].join('\n')); if (tomorrow.length) sections.push(['Tomorrow', ...tomorrow].join('\n')); if (draft.note) sections.push('Note\n' + draft.note); return sections.join('\n\n'); }, async shareSummary() { const text = this.text(); if (!text) throw new Error('Select at least one summary item or add a note.'); if (typeof share === 'function') { try { await share({ title:'Today summary', text }); discard(); return { status:'shared' }; } catch (error) { if (error?.name === 'AbortError') return { status:'canceled' }; throw error; } } if (typeof copy !== 'function') throw new Error('Sharing is unavailable on this device.'); await copy(text); discard(); return { status:'copied' }; }, }; } function createTodaySummaryView({ summary, qs, escapeHtml }) { const rowMarkup = (section, row) => ''; function close() { qs('#today-summary-sheet').hidden = true; document.body.classList.remove('task-overlay-open'); qs('#start-work-session')?.focus(); } function render() { const draft = summary.snapshot(); if (!draft) return; qs('#today-summary-worked').innerHTML = draft.worked.map(row => rowMarkup('worked', row)).join('') || '

No worked items selected.

'; qs('#today-summary-tomorrow').innerHTML = draft.tomorrow.map(row => rowMarkup('tomorrow', row)).join('') || '

Nothing scheduled for tomorrow.

'; qs('#today-summary-include-actuals').checked = draft.include_actuals; qs('#today-summary-note').value = draft.note; qs('#today-summary-preview').textContent = summary.text(); qs('#today-summary-sheet').querySelectorAll('[data-summary-identity]').forEach(input => { input.addEventListener('change', () => { summary.choose(input.dataset.summarySection, input.dataset.summaryIdentity, input.checked); render(); }); }); } function show() { qs('#today-summary-status').textContent = ''; render(); qs('#today-summary-sheet').hidden = false; document.body.classList.add('task-overlay-open'); requestAnimationFrame(() => (qs('#today-summary-worked input') || qs('#share-today-summary')).focus()); } function open(worked, tomorrow) { summary.begin(worked, tomorrow); show(); } async function shareDraft(button) { button.disabled = true; qs('#today-summary-status').textContent = 'Opening share options…'; try { const result = await summary.shareSummary(); if (result.status === 'canceled') { qs('#today-summary-status').textContent = 'Share canceled. Your private draft is still here.'; return; } qs('#my-work-action-status').textContent = result.status === 'shared' ? 'Today summary shared.' : 'Today summary copied to your clipboard.'; close(); } catch (error) { qs('#today-summary-status').textContent = error.message || 'Summary could not be shared. Your draft is unchanged.'; } finally { button.disabled = false; } } function bind() { qs('#close-today-summary').addEventListener('click', close); qs('#discard-today-summary').addEventListener('click', () => { summary.discard(); close(); }); qs('#today-summary-include-actuals').addEventListener('change', event => { summary.includeActuals(event.currentTarget.checked); render(); }); qs('#today-summary-note').addEventListener('input', event => { summary.setNote(event.currentTarget.value); qs('#today-summary-preview').textContent = summary.text(); }); qs('#share-today-summary').addEventListener('click', event => shareDraft(event.currentTarget)); } return { open, close, render, bind, shareDraft, resume() { if (summary.restore()) show(); }, }; } function setupTodaySummary({ qs, escapeHtml, getLogin }) { const summary = createTodaySummary({ storage:localStorage, getLogin, share:typeof navigator.share === 'function' ? payload => navigator.share(payload) : null, copy:typeof navigator.clipboard?.writeText === 'function' ? text => navigator.clipboard.writeText(text) : null, }); const view = createTodaySummaryView({ summary, qs, escapeHtml }); view.bind(); return view; } if (typeof module !== 'undefined' && module.exports) { createTodaySummary.createView = createTodaySummaryView; createTodaySummary.setup = setupTodaySummary; module.exports = createTodaySummary; }