Source: 25-rollingsummary.js

/**
 * @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);
  });
}