/* ── Today's tasks — state and persistence ── */
const STORE_PLAN = 'wl_plan_v1';
// planTasks lives in state.js (#423) — getPlanTasks()/setPlanTasks().
let planCollapsed = readCollapseState('planSection', false);
let pendingCollapsed = readCollapseState('pendingSection', true);
// eslint-disable-next-line prefer-const -- reassigned by 10b-tasks-events.js
let editingPlanId = null;
// eslint-disable-next-line prefer-const -- reassigned by 10b-tasks-events.js
let _pendingCommentId = null;
// eslint-disable-next-line prefer-const -- reassigned by 10b-tasks-events.js
let splitInputId = null;
// eslint-disable-next-line prefer-const -- reassigned by 10b-tasks-events.js
let _pendingCommentText = '';
// eslint-disable-next-line prefer-const -- reassigned by 10b-tasks-events.js
let _expandedHistoryId = null;
const _cpOpenIds = new Set();
// eslint-disable-next-line prefer-const -- reassigned by 10b-tasks-events.js
let _cpEditId = null; // pid of task whose checkpoint is being edited
// eslint-disable-next-line prefer-const -- reassigned by 10b-tasks-events.js
let _cpEditIdx = null; // index of checkpoint being edited
const _noteOpenIds = new Set();
/** Whether the Done column's older-history expander is open. */
// eslint-disable-next-line prefer-const -- reassigned by 10a-tasks-render.js
let doneHistoryOpen = false;
/** Whether the WIP-over warning banner has been dismissed this render cycle. */
// eslint-disable-next-line prefer-const -- reassigned by 10a-tasks-render.js, 10b-tasks-events.js
let wipWarnDismissed = false;
/**
* Loads plan tasks from localStorage into the shared plan-task list, filtering
* out invalid entries via `validPlanTask`. Drops are reported via wlLog.warn so
* data-quality issues are visible in DevTools. Resets to empty array on parse error.
* Each task is
* `{ id, text, status, tag, date, [billable], [notionUrl], [emoji], [checkpoints], [parentId], [priority], [note] }`.
* @returns {void}
*/
function loadPlan() {
try {
const raw = JSON.parse(localStorage.getItem(STORE_PLAN) || '[]');
const all = Array.isArray(raw) ? raw : [];
setPlanTasks(all.filter(validPlanTask));
if (getPlanTasks().length < all.length)
wlLog.warn(`loadPlan: dropped ${all.length - getPlanTasks().length} invalid task record(s)`, {
total: all.length,
kept: getPlanTasks().length,
});
} catch (err) {
setPlanTasks([]);
wlLog.error('loadPlan: failed to parse plan tasks from localStorage', err);
}
}
/**
* Persists the current `planTasks` array to localStorage.
* @returns {void}
*/
function savePlan() {
localStorage.setItem(STORE_PLAN, JSON.stringify(getPlanTasks()));
}
/**
* Sorts a flat list of plan tasks by status order and priority, inserting
* child tasks immediately after their parent. Tasks matching the currently
* running timer text are sorted first. Orphaned children (whose parent has
* been deleted or moved) are appended at the end.
* @param {Array<Object>} tasks - Array of plan task objects to sort.
* @returns {Array<Object>} New sorted array with children interleaved after parents.
*/
function flatSort(tasks) {
// Assumption: STATUS_ORDER defines the canonical sort priority for visible task sections.
// 'done' sorts last so completed work doesn't push active items down.
// 'upcoming' shares todo's rank on purpose: the To Do column absorbs both, so
// they interleave by priority then text. The `?? 1` fallback covers only
// unknown/legacy statuses.
const STATUS_ORDER = { inprogress: 0, todo: 1, upcoming: 1, pending: 2, blocked: 3, done: 4 };
const liveEntry = getActiveTimer()
? getEntries().find((entry) => entry.id === getActiveTimer().entryId)
: null;
const liveText = liveEntry ? liveEntry.text.toLowerCase() : null;
const parents = tasks.filter((task) => !task.parentId);
const children = tasks.filter((task) => !!task.parentId);
const sorted = [...parents].sort((taskA, taskB) => {
const aLive = liveText && taskA.text.toLowerCase() === liveText;
const bLive = liveText && taskB.text.toLowerCase() === liveText;
if (aLive && !bLive) return -1;
if (!aLive && bLive) return 1;
const aOrd = STATUS_ORDER[taskA.status || 'todo'] ?? 1;
const bOrd = STATUS_ORDER[taskB.status || 'todo'] ?? 1;
if (aOrd !== bOrd) return aOrd - bOrd;
// Within the same status: higher priority first (high=1, normal=0, low=-1)
const aPri = taskA.priority || 0;
const bPri = taskB.priority || 0;
if (aPri !== bPri) return bPri - aPri;
return taskA.text.localeCompare(taskB.text);
});
// Insert children right after their parent
const result = [];
sorted.forEach((parentTask) => {
result.push(parentTask);
const kids = children
.filter((child) => child.parentId === parentTask.id)
.sort((childA, childB) => childA.text.localeCompare(childB.text));
kids.forEach((kid) => result.push(kid));
});
// Orphaned children (parent deleted/moved) go at end
children
.filter((child) => !parents.find((parent) => parent.id === child.parentId))
.forEach((orphan) => result.push(orphan));
return result;
}
/**
* Reads the plan-input field and adds a new "todo" task to today's plan.
* Inherits the currently selected tag and that category's billable default.
* No-ops if the input is empty.
* @returns {void}
*/
function addPlanTask() {
const inp = document.getElementById('planInput');
const text = inp.value.trim();
if (!text) return;
getPlanTasks().push({
id: Date.now() + '',
text,
status: 'todo',
tag: getSelectedTag(),
date: dk(new Date()),
billable: getCat(getSelectedTag()).billable !== false,
});
inp.value = '';
savePlan();
renderPlan();
inp.focus();
}
/**
* Promotes today's plan task matching `text` to "in progress", if one
* exists and is still in a promotable state. Called whenever a timer starts
* on text that might correspond to an existing board card — the hero
* composer, a recent chip, the log capture input, and the board's own
* "track" button all funnel through this so the Kanban board stays in sync
* no matter which control was used to start tracking.
* @param {string} text - The entry text a timer was just started on.
* @returns {void}
*/
function promoteMatchingTaskToInProgress(text) {
const task = findPromotableTask(getPlanTasks(), text, dk(new Date()));
if (!task) return;
task.status = 'inprogress';
delete task.completedAt;
if (task.parentId) {
const parent = getPlanTasks().find((planTask) => planTask.id === task.parentId);
if (parent && parent.status === 'todo') parent.status = 'inprogress';
}
savePlan();
}
// Event listeners bound at parse time — safe because script runs after DOM is built.
document.getElementById('planAddBtn').addEventListener('click', addPlanTask);
document.getElementById('planInput').addEventListener('keydown', (event) => {
if (event.key === 'Enter') addPlanTask();
});
document.getElementById('planHeader').addEventListener('click', () => {
planCollapsed = !planCollapsed;
writeCollapseState('planSection', planCollapsed);
renderPlan();
});
let upcomingCollapsed = readCollapseState('upcomingSection', true);
document.getElementById('upcomingHeader').addEventListener('click', () => {
upcomingCollapsed = !upcomingCollapsed;
writeCollapseState('upcomingSection', upcomingCollapsed);
document.getElementById('upcomingSection').classList.toggle('collapsed', upcomingCollapsed);
});
document.getElementById('pendingHeader').addEventListener('click', () => {
pendingCollapsed = !pendingCollapsed;
writeCollapseState('pendingSection', pendingCollapsed);
renderPlan();
});