/**
* @file 12c-gapreport.js — End-of-week gap report.
*
* Lists this calendar week's (Mon–Sun) finished, non-cancelled, billable log
* entries that have neither a proof link nor a note, via the pure
* findGapReportEntries() (pure-fns.js), so the user can catch missing
* documentation themselves before a weekly report is due. Non-billable
* entries are never flagged — there's nothing to bill, so nothing to prove.
* Clicking a flagged entry jumps straight to it in the Log view with its
* proof-link/note editor already open — see
* buildEntryMetaHtml()/bindEntryMetaEvents() in 04a-render-entry-meta.js for the editor
* itself (added alongside the entry.link/note fields this report reads).
*/
// Element that had focus when the report was opened, restored on close —
// same convention as the EOD/expiry modal focus management in 12-misc.js.
let _gapReportTrigger = null;
/**
* Builds one gap-report row: date label, task text (with any leading Jira
* key linkified), and a "+ fix" button that jumps to the entry.
* @param {Object} entry - A flagged log entry (from findGapReportEntries()).
* @returns {string} HTML string.
*/
function buildGapReportRowHtml(entry) {
return `<div class="gap-report-row">
<span class="gap-report-date">${fmtDateLabel(entry.date)}</span>
<span class="gap-report-text">${jiraTicketHtml(entry.text)}</span>
<button type="button" class="gap-report-fix" data-id="${entry.id}" data-date="${entry.date}">+ fix</button>
</div>`;
}
/**
* Opens the gap report: this calendar week's billable entries missing both a
* proof link and a note. Builds the list fresh from current `entries` state
* every time it's opened, so it always reflects the latest data.
*/
function openGapReportOverlay() {
const overlay = document.getElementById('gapReportOverlay');
if (!overlay) return;
_gapReportTrigger = document.activeElement;
const weekStart = mondayOfWeek();
const weekEnd = weekStart + 7 * 24 * 60 * 60 * 1000;
// findGapReportEntries() is a pure function with no access to the
// category/task lookups isEntryBillable() needs, so billable status is
// resolved here, up front, and passed in as each entry's _billable flag
// (same convention as exportTxt's entriesWithBillingStatus) — this is what
// lets non-billable entries be excluded without flagging them for a
// missing note/link.
const entriesWithBillingStatus = annotateBillableStatus(getEntries());
const flagged = findGapReportEntries(entriesWithBillingStatus, weekStart, weekEnd);
const subtitleEl = document.getElementById('gapReportSubtitle');
const listEl = document.getElementById('gapReportList');
if (subtitleEl) {
subtitleEl.textContent = flagged.length
? `${flagged.length} ${flagged.length === 1 ? 'entry needs' : 'entries need'} a proof link or a note this week`
: 'All caught up 🎉 — every entry this week has a proof link or a note.';
}
if (listEl) {
listEl.innerHTML = flagged.length
? flagged.map(buildGapReportRowHtml).join('')
: '<div class="gap-report-empty">Nothing to fix.</div>';
}
overlay.classList.add('show');
setTimeout(() => {
const first = overlay.querySelector('button, [tabindex]:not([tabindex="-1"])');
if (first) first.focus();
}, 50);
}
/** Restores focus to whatever triggered the report, then clears it. */
function restoreGapReportFocus() {
if (_gapReportTrigger) {
_gapReportTrigger.focus();
_gapReportTrigger = null;
}
}
/** Hides the gap-report overlay and restores focus to its trigger. */
function closeGapReportOverlay() {
const overlay = document.getElementById('gapReportOverlay');
if (overlay) overlay.classList.remove('show');
restoreGapReportFocus();
}
/**
* Jumps from the gap report straight to a flagged entry: closes the report,
* switches the viewed day and the Today's Flow tab to that entry's Log view,
* opens its proof-link/note editor, and scrolls the row into view. This is
* the "find it yourself, fix it yourself" action the report exists for.
* @param {string} entryId - ID of the entry to jump to.
* @param {string} entryDate - The entry's date (YYYY-MM-DD).
*/
function jumpToGapReportEntry(entryId, entryDate) {
const overlay = document.getElementById('gapReportOverlay');
if (overlay) overlay.classList.remove('show');
_gapReportTrigger = null; // navigating away — nothing to restore focus to
setViewDate(new Date(entryDate + 'T12:00:00'));
setFlowView('log');
_entryMetaEditId = entryId;
render();
setTimeout(() => {
const row = document.querySelector(`.entry[data-id="${entryId}"]`);
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
document.getElementById('emli-' + entryId)?.focus();
}, 0);
}
// Wrapped in a named IIFE (as 12-misc.js does) so these element lookups are not
// top-level consts in the concatenated script.js, where a same-named binding in
// another file would be a redeclaration SyntaxError.
(function bindGapReportOverlay() {
const gapReportBtn = document.getElementById('gapReportBtn');
const gapReportOverlay = document.getElementById('gapReportOverlay');
const gapReportClose = document.getElementById('gapReportClose');
const gapReportList = document.getElementById('gapReportList');
if (gapReportBtn) gapReportBtn.addEventListener('click', openGapReportOverlay);
if (gapReportClose) gapReportClose.addEventListener('click', closeGapReportOverlay);
if (gapReportOverlay) {
gapReportOverlay.addEventListener('click', (e) => {
if (e.target === gapReportOverlay) closeGapReportOverlay();
});
gapReportOverlay.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeGapReportOverlay();
if (e.key === 'Tab') trapFocusInOverlay(gapReportOverlay, e);
});
}
if (gapReportList) {
gapReportList.addEventListener('click', (e) => {
const btn = e.target.closest('.gap-report-fix');
if (btn) jumpToGapReportEntry(btn.dataset.id, btn.dataset.date);
});
}
})();