/**
* @file 25-rollingsummary.js — Rolling 7-day summary tab inside Today's Flow.
*
* Shows per-day rows (date · location · session times · tracked total · top 3 tasks)
* and a week total. Includes a "Copy" button for standup use.
*
* Pure data calculation lives in buildRollingSummary (pure-fns-rollingsummary.js) and is
* unit-tested there. This module is the localStorage + DOM glue around it.
*/
import { dk, escHtml, fmtTime, fmtDur } from './pure-fns-format.js';
import { WORK_LOCATIONS, locationFor } from './pure-fns-tasks.js';
import { loadLocationMap } from './24-location.js';
import { buildRollingSummary } from './pure-fns-rollingsummary.js';
import { getEntries } from './state.js';
import { wlLog } from './logger.js';
/**
* Returns the last `n` YYYY-MM-DD date keys ending at (and including) today,
* ordered most-recent-first.
* @param {number} [n=7]
* @returns {string[]}
*/
function rollingDateKeys(n = 7) {
const keys = [];
const d = new Date();
for (let i = 0; i < n; i++) {
keys.push(dk(d));
d.setDate(d.getDate() - 1);
}
return keys;
}
/**
* Reads the SOD timestamp for a date key from localStorage.
* @param {string} dateKey
* @returns {number|null}
*/
function summaryGetSodTs(dateKey) {
return parseInt(localStorage.getItem('wl_sod_' + dateKey) || '0') || null;
}
/**
* Reads the EOD timestamp for a date key from localStorage.
* @param {string} dateKey
* @returns {number|null}
*/
function summaryGetEodTs(dateKey) {
return parseInt(localStorage.getItem('wl_eod_' + dateKey) || '0') || null;
}
/** Day-of-week abbreviations used by fmtDateLabel — module-level to avoid per-call allocation. */
const DATE_LABEL_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
/** Month abbreviations used by fmtDateLabel — module-level to avoid per-call allocation. */
const DATE_LABEL_MONTHS = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
/**
* Formats a YYYY-MM-DD date key as a short day-of-week + date label.
* e.g. '2026-06-04' → 'Wed 04 Jun'
* @param {string} dateKey
* @returns {string}
*/
export function fmtDateLabel(dateKey) {
const [y, m, d] = dateKey.split('-').map(Number);
const date = new Date(y, m - 1, d);
return `${DATE_LABEL_DAYS[date.getDay()]} ${String(d).padStart(2, '0')} ${DATE_LABEL_MONTHS[m - 1]}`;
}
/**
* Builds the plain-text standup block from computed rows.
* Pure function — no DOM or clipboard access.
* @param {object[]} rows - Output of buildRollingSummary.
* @param {number} weekTotalMs - Sum of all days' tracked time in ms.
* @param {Record<string, string>} locationMap - Pre-read location map.
* @returns {string}
*/
export function buildSummaryText(rows, weekTotalMs, locationMap) {
const lines = rows
.filter((row) => row.sodTs || row.totalMs > 0)
.map((row) => {
const locLabel = WORK_LOCATIONS[locationFor(locationMap, row.dateKey)].label;
const sodStr = row.sodTs ? fmtTime(row.sodTs) : '—';
const eodStr = row.eodTs ? fmtTime(row.eodTs) : '—';
const totalStr = row.totalMs > 0 ? fmtDur(row.totalMs) : '—';
let line = `${row.locationEmoji} ${fmtDateLabel(row.dateKey)} ${locLabel} · ${sodStr}–${eodStr} · ${totalStr}`;
if (row.topTasks.length) {
const tasks = row.topTasks
.map((task) => `${task.text} (${fmtDur(task.totalMs)})`)
.join(', ');
line += `\n ${tasks}`;
}
return line;
});
if (weekTotalMs > 0) lines.push(`\nWeek total: ${fmtDur(weekTotalMs)}`);
return lines.join('\n');
}
/**
* Copies the standup text to the clipboard.
* Shows a brief "Copied!" confirmation on success, or logs a warning on failure.
* @param {object[]} rows - Output of buildRollingSummary.
* @param {number} weekTotalMs - Sum of all days' tracked time in ms.
* @param {Record<string, string>} locationMap - Pre-read location map.
* @returns {void}
*/
function copySummaryText(rows, weekTotalMs, locationMap) {
navigator.clipboard
.writeText(buildSummaryText(rows, weekTotalMs, locationMap))
.then(() => {
const fb = document.getElementById('rsCopyFeedback');
if (fb) {
fb.textContent = 'Copied!';
setTimeout(() => {
fb.textContent = '';
}, 2000);
}
})
.catch((err) => {
wlLog.warn('copySummaryText: clipboard write failed', err);
});
}
/**
* Renders the 7-day rolling summary into #tfSummaryPane.
* No-ops when the pane element is absent (e.g. reduced test DOM).
* @returns {void}
*/
export function renderRollingSummary() {
const el = document.getElementById('tfSummaryPane');
if (!el) return;
// Read the location map once; the closure below and copySummaryText both share it.
const locationMap = loadLocationMap();
const dateKeys = rollingDateKeys(7);
const rows = buildRollingSummary(dateKeys, {
entries: getEntries(),
getDayStartTs: summaryGetSodTs,
getDayEodTs: summaryGetEodTs,
getLocationEmoji: (dateKey) => WORK_LOCATIONS[locationFor(locationMap, dateKey)].emoji,
});
const weekTotalMs = rows.reduce((sum, r) => sum + r.totalMs, 0);
const rowsHtml = rows
.map((row) => {
const sodStr = row.sodTs ? fmtTime(row.sodTs) : '—';
const eodStr = row.eodTs ? fmtTime(row.eodTs) : '—';
const totalStr = row.totalMs > 0 ? fmtDur(row.totalMs) : '—';
const hasData = !!(row.sodTs || row.totalMs > 0);
const tasksHtml = row.topTasks.length
? row.topTasks
.map(
(task) =>
`<span class="rs-task">${escHtml(task.text)}<span class="rs-task-dur"> ${fmtDur(task.totalMs)}</span></span>`
)
.join('')
: '<span class="rs-no-tasks">no tracked entries</span>';
return `<div class="rs-row${hasData ? '' : ' rs-row--empty'}">
<div class="rs-row-head">
<span class="rs-emoji" aria-hidden="true">${row.locationEmoji}</span>
<span class="rs-date">${fmtDateLabel(row.dateKey)}</span>
<span class="rs-session"><span class="sr-only">Session: </span>${sodStr} – ${eodStr}</span>
<span class="rs-total"><span class="sr-only">Tracked total: </span>${totalStr}</span>
</div>
<div class="rs-tasks" role="group" aria-label="Top tasks for ${fmtDateLabel(row.dateKey)}">${tasksHtml}</div>
</div>`;
})
.join('');
el.innerHTML = `<div class="rs-wrap">
<div class="rs-rows">${rowsHtml}</div>
<div class="rs-week-total">Week total: <strong>${fmtDur(weekTotalMs)}</strong></div>
<button type="button" class="rs-copy-btn" id="rsCopyBtn"><span aria-hidden="true">📋</span> Copy for standup</button>
<div class="rs-copy-feedback" id="rsCopyFeedback" aria-live="polite"></div>
</div>`;
document.getElementById('rsCopyBtn')?.addEventListener('click', () => {
copySummaryText(rows, weekTotalMs, locationMap);
});
}