Source: 05a-export.js

/* ── Text export ── */

/**
 * Exports the currently viewed day's log as a plaintext file.
 * Groups entries by category and task, includes a header with day start/end
 * times and tracked time totals, and appends a pasteable billable summary.
 * Writes to the user's chosen save folder via the File System Access API,
 * or falls back to a browser download.
 */
function exportTxt() {
  const dayEntries = viewEntries().slice().reverse();
  if (!dayEntries.length) return;

  const dateStr = dk(viewDate);
  const isViewingToday = dateStr === dk(new Date());
  const timedEntries = dayEntries.filter(
    (entry) => entry.tsEnd && entry.tsEnd > entry.ts && entry.signifier !== 'cancelled'
  );

  const { dayStartTs, dayEndTs } = computeDayBounds(dayEntries, timedEntries, {
    isViewingToday,
    dayStart: isViewingToday ? getDayStart() : null,
    activeTimer,
    now: Date.now(),
  });

  const fmtTsHM = (ts) => {
    const d = new Date(ts);
    return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
  };

  // Body: tracked time grouped by category, then task (first-seen order)
  const { catOrder, catGrouped } = groupEntriesByCategory(dayEntries);
  const lines = formatGroupedLines(catOrder, catGrouped, fmtDurLong, getCatLabel);

  // Billable / non-billable breakdown
  const totalTrackedMs = timedEntries.reduce((sum, entry) => sum + (entry.tsEnd - entry.ts), 0);
  const billableTimed = timedEntries.filter((entry) => isEntryBillable(entry));
  const billableMs = billableTimed.reduce((sum, entry) => sum + (entry.tsEnd - entry.ts), 0);
  const nonBillableMs = totalTrackedMs - billableMs;

  const header = [`Work Log — ${dateStr}`];
  if (dayStartTs) {
    const startStr = fmtTsHM(dayStartTs);
    const endStr = dayEndTs ? fmtTsHM(dayEndTs) : '--:--';
    header.push(`Started: ${startStr}  |  Ended: ${endStr}`);
    if (dayEndTs) header.push(`Workday: ${fmtDurLong(dayEndTs - dayStartTs)}`);
  }
  if (totalTrackedMs > 0) {
    header.push(
      `Total tracked: ${fmtDurLong(totalTrackedMs)}  |  💰 Billable: ${fmtDurLong(billableMs)}  |  💸 Non-billable: ${fmtDurLong(nonBillableMs)}`
    );
  }
  header.push('---');

  // Pasteable billable summary — last line of the file.
  // Merge same-task entries separated by ≤30 min, then group by category.
  const billableMerged = mergeAdjacentEntries(billableTimed);
  const summaryParts = buildBillableSummaryParts(billableMerged, getCatLabel);
  const summaryLine = summaryParts.length ? summaryParts.join(', ') : '';

  const blob = new Blob(
    [[...header, ...lines, ...(summaryLine ? ['---', summaryLine] : [])].join('\n')],
    { type: 'text/plain' }
  );
  const filename = `work-log-${dateStr}.txt`;
  writeExportFile('timesheets', filename, blob);
}

/* ── JSON backup / restore ── */

/**
 * Reads and parses an optional JSON-array log from localStorage for inclusion
 * in a backup. If the stored value is present but not valid JSON, the failure
 * is logged via {@link wlLog} (so the data loss is diagnosable rather than
 * silent) and an empty array is returned so the rest of the backup still
 * succeeds. Absent keys legitimately yield an empty array.
 * @param {string} storeKey - The localStorage key to read.
 * @param {string} label    - Human-readable log name for the warning message.
 * @returns {Array} The parsed array, or `[]` if absent or unparseable.
 */
function readOptionalLogForBackup(storeKey, label) {
  try {
    return JSON.parse(localStorage.getItem(storeKey) || '[]');
  } catch (e) {
    wlLog.warn(
      `exportBackup: ${label} in localStorage is not valid JSON — backing up an empty array for it; the corrupt data is excluded from this backup`,
      e
    );
    return [];
  }
}

/**
 * Exports a full JSON backup of all application state: entries, categories,
 * plan tasks, time blocks, pomodoro log, dev log, distractions, and hidden
 * quick-pick items. Triggers a file download or writes to the save folder.
 */
function exportBackup() {
  const backup = {
    version: '1',
    exported: new Date().toISOString(),
    entries,
    categories,
    planTasks,
    blocks,
    pomoLog: readOptionalLogForBackup(STORE_POMO_LOG, 'pomoLog'),
    devLog: readOptionalLogForBackup(STORE_DEV_LOG, 'devLog'),
    distractions: readOptionalLogForBackup(STORE_DISTRACTIONS, 'distractions'),
    qpHidden: [...qpHidden],
  };
  const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
  const filename = `work-log-backup-${dk(new Date())}.json`;
  writeExportFile('JSON backups', filename, blob);
}

/**
 * Restores application state from a JSON backup file previously created by
 * {@link exportBackup}. Reads the file, validates its structure via
 * {@link validateBackupFile}, asks the user to confirm, writes all arrays to
 * their localStorage keys, then reloads the page so state is re-initialised
 * cleanly from the restored data.
 *
 * Assumption: import is a full replace, not a merge. All data currently in the
 * affected localStorage keys is overwritten after the user confirms. If
 * selective-date merging is ever needed, add a merge mode option here and update
 * the confirmation dialog accordingly.
 *
 * @param {File} file - The .json backup file selected by the user.
 * @returns {Promise<void>}
 */
async function importBackup(file) {
  let text;
  try {
    text = await file.text();
  } catch (e) {
    wlLog.warn('importBackup: failed to read file', e);
    alert('Could not read the file. Please try again.');
    return;
  }

  let backup;
  try {
    backup = JSON.parse(text);
  } catch (e) {
    wlLog.warn('importBackup: file is not valid JSON', e);
    alert('The selected file is not valid JSON. Please choose a work-log backup file.');
    return;
  }

  const { valid, error } = validateBackupFile(backup);
  if (!valid) {
    alert(error);
    return;
  }

  const entryCount = backup.entries.length;
  const dates = backup.entries
    .map((entry) => entry.date)
    .filter(Boolean)
    .sort();
  const dateRange =
    dates.length > 0 ? `${dates[0]} to ${dates[dates.length - 1]}` : 'no dated entries';
  const exportedAt = backup.exported ? new Date(backup.exported).toLocaleString() : 'unknown date';

  const confirmed = window.confirm(
    `Restore backup from ${exportedAt}?\n\n` +
      `${entryCount} entries (${dateRange})\n` +
      `${backup.categories.length} categories\n` +
      `${backup.planTasks.length} tasks\n\n` +
      `⚠️  This will replace your current data. The page will reload after import.`
  );
  if (!confirmed) return;

  try {
    // Write primary arrays — always present (validated above)
    localStorage.setItem(STORE_ENTRIES, JSON.stringify(backup.entries));
    localStorage.setItem(STORE_CATS, JSON.stringify(backup.categories));
    // STORE_PLAN is defined in 10-tasks.js; safe to reference at call-time
    localStorage.setItem(STORE_PLAN, JSON.stringify(backup.planTasks));

    // Write optional arrays — only if present in the backup
    // STORE_BLOCKS is defined in 11-timeblock.js
    if (Array.isArray(backup.blocks)) {
      localStorage.setItem(STORE_BLOCKS, JSON.stringify(backup.blocks));
    }
    if (Array.isArray(backup.pomoLog)) {
      localStorage.setItem(STORE_POMO_LOG, JSON.stringify(backup.pomoLog));
    }
    // STORE_DEV_LOG is defined in 12a-changelog.js
    if (Array.isArray(backup.devLog)) {
      localStorage.setItem(STORE_DEV_LOG, JSON.stringify(backup.devLog));
    }
    // STORE_DISTRACTIONS is defined in 12-misc.js
    if (Array.isArray(backup.distractions)) {
      localStorage.setItem(STORE_DISTRACTIONS, JSON.stringify(backup.distractions));
    }
    if (Array.isArray(backup.qpHidden)) {
      localStorage.setItem(STORE_QP_HIDDEN, JSON.stringify(backup.qpHidden));
    }

    wlLog.info(
      `importBackup: restored ${entryCount} entries, ` +
        `${backup.categories.length} categories, ` +
        `${backup.planTasks.length} tasks ` +
        `from backup exported ${backup.exported ?? 'unknown'}`
    );
    location.reload();
  } catch (e) {
    wlLog.warn('importBackup: failed to write to localStorage', e);
    alert(
      'Import failed — could not write to localStorage. Your existing data has not been changed.'
    );
  }
}