/**
* @file 26-eod-entry.js — End-of-day hours entry form: rendering,
* clipboard copy, and server submission.
*
* The browser cannot submit directly (CORS + SSO),
* so submissions go to the local PowerShell server's /api/eod-submit,
* which drives a saved browser session via scripts/eod-submit.mjs.
*/
import { buildTimesheetDayPayload, findTimesheetEntryProblem } from './pure-fns-hours-entry.js';
import { EOD_SUBMIT_ENABLED } from './00-config.js';
import { getEntries } from './state.js';
import { wlLog } from './logger.js';
import { getCatLabel } from './cat-utils.js';
/** Status-line suffix per submission route the server reports. */
const TIMESHEET_ROUTE_NOTE = {
chrome: ' (via Claude in Chrome)',
playwright: ' (via Playwright fallback)',
};
/**
* Returns the given day's entries that have a positive tracked duration and
* are not cancelled — the same filter the plaintext export uses.
* @param {string} dateKey - Day to read, `YYYY-MM-DD`.
* @returns {Array<Object>} Timed entries for that day.
*/
function timedEntriesForDay(dateKey) {
return getEntries().filter(
(entry) =>
entry.date === dateKey &&
entry.tsEnd &&
entry.tsEnd > entry.ts &&
entry.signifier !== 'cancelled'
);
}
/**
* Shows a status message under the timesheet form. Plain text only: the
* element is an aria-live region, so emoji would be read aloud to screen-reader
* users; the error modifier class carries the visual distinction instead.
* @param {string} message - Text to display.
* @param {boolean} [isError] - Adds the error modifier class when true.
*/
function setTimesheetStatus(message, isError = false) {
const statusEl = document.getElementById('eodEntryStatus');
statusEl.textContent = message;
statusEl.classList.toggle('eod-entry__status--error', isError);
}
/**
* Fills the End of Day timesheet form with today's draft entry. With nothing
* tracked the form is disabled rather than submitting an empty day.
* @param {string} dateKey - Day being ended, `YYYY-MM-DD`.
*/
export function renderEodTimesheet(dateKey) {
const payload = buildTimesheetDayPayload(dateKey, timedEntriesForDay(dateKey), getCatLabel);
const hoursEl = document.getElementById('eodEntryHours');
const descEl = document.getElementById('eodEntryDesc');
const submitBtn = document.getElementById('eodEntrySubmit');
const copyBtn = document.getElementById('eodEntryCopy');
hoursEl.value = payload ? String(payload.hours) : '';
descEl.value = payload ? payload.description : '';
hoursEl.dataset.date = dateKey;
submitBtn.hidden = !EOD_SUBMIT_ENABLED;
[hoursEl, descEl, submitBtn, copyBtn].forEach((el) => (el.disabled = !payload));
setTimesheetStatus(payload ? '' : 'Nothing tracked today — no timesheet entry to draft.');
wlLog.info(
`renderEodTimesheet: ${payload ? `draft ${payload.hours}h for ${dateKey}` : 'no tracked time, form disabled'}`
);
}
/**
* Copies the description as currently edited in the form to the clipboard, so
* it can be pasted into the timesheet by hand.
* @returns {Promise<void>} Resolves once the status message is set.
*/
async function copyEodTimesheetDescription() {
try {
await navigator.clipboard.writeText(document.getElementById('eodEntryDesc').value);
setTimesheetStatus('Description copied');
} catch (err) {
wlLog.warn('copyEodTimesheetDescription: clipboard write failed', err);
setTimesheetStatus('Could not copy — select the text and copy it manually', true);
}
}
/**
* Posts the (possibly edited) entry to the local server and reports the
* outcome. An expired SSO session gets its own message with the fix.
* @returns {Promise<void>} Resolves once the status message is set.
*/
async function submitEodTimesheet() {
const hoursEl = document.getElementById('eodEntryHours');
const submitBtn = document.getElementById('eodEntrySubmit');
const body = {
date: hoursEl.dataset.date,
hours: Number(hoursEl.value),
description: document.getElementById('eodEntryDesc').value.trim(),
};
const problem = findTimesheetEntryProblem(body);
if (problem) {
setTimesheetStatus(problem, true);
return;
}
submitBtn.disabled = true;
setTimesheetStatus('Submitting…');
try {
const res = await fetch('/api/eod-submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
if (res.ok) {
const route = TIMESHEET_ROUTE_NOTE[data.method] || '';
setTimesheetStatus(`Timesheet entry saved for ${body.date}${route}`);
return;
}
wlLog.warn('submitEodTimesheet: server rejected the entry', res.status, data);
setTimesheetStatus(data.error || `Server returned ${res.status}`, true);
} catch (err) {
wlLog.warn('submitEodTimesheet: request failed', err);
setTimesheetStatus('Could not reach the local server — is start-server.ps1 running?', true);
} finally {
submitBtn.disabled = false;
}
}
document.getElementById('eodEntrySubmit').addEventListener('click', submitEodTimesheet);
document.getElementById('eodEntryCopy').addEventListener('click', copyEodTimesheetDescription);