Source: 10b-tasks-events.js

/* ── Today's tasks — event binding ── */

/**
 * Binds all plan list event handlers after each render.
 * @param {HTMLElement[]} lists - Column list elements (To Do, In Progress, Done).
 */
function bindPlanEvents(lists) {
  const qa = (sel) => lists.flatMap((listEl) => [...listEl.querySelectorAll(sel)]);

  // WIP warn dismiss — { once: true } so re-renders don't stack listeners
  document.querySelectorAll('.wip-warn__dismiss').forEach((btn) => {
    btn.addEventListener(
      'click',
      () => {
        wipWarnDismissed = true;
        renderPlan();
      },
      { once: true }
    );
  });
  qa('.plan-text').forEach((span) => {
    span.addEventListener('click', () => {
      const pid = span.closest('.plan-item').dataset.pid;
      if (pid) {
        editingPlanId = pid;
        renderPlan();
      }
    });
  });

  // Category picker
  qa('.plan-cat-line').forEach((line) => {
    line.addEventListener('click', () => {
      const pid = line.dataset.pid;
      const picker = document.getElementById('pcp-' + pid);
      const isOpen = picker.classList.contains('open');
      lists.forEach((listEl) =>
        listEl
          .querySelectorAll('.plan-cat-picker.open')
          .forEach((picker) => picker.classList.remove('open'))
      );
      if (!isOpen) picker.classList.add('open');
    });
  });
  qa('.plan-cat-picker .cat-opt').forEach((btn) => {
    btn.addEventListener('click', () => {
      const task = getPlanTasks().find((task) => task.id === btn.dataset.pid);
      if (task) {
        task.tag = btn.dataset.cat;
        savePlan();
        renderPlan();
      }
    });
  });
  qa('.plan-cat-picker .cat-cancel').forEach((btn) => {
    btn.addEventListener('click', () => {
      document.getElementById('pcp-' + btn.dataset.pid).classList.remove('open');
    });
  });

  // + new epic inside picker
  qa('.pcat-add-btn').forEach((btn) => {
    btn.addEventListener('click', () => {
      btn.style.display = 'none';
      const form = document.getElementById('pcaf-' + btn.dataset.pid);
      form.classList.add('open');
      form.querySelector('.pcat-add-input').focus();
    });
  });
  qa('.pcat-add-ok').forEach((btn) => {
    btn.addEventListener('click', () => {
      const form = document.getElementById('pcaf-' + btn.dataset.pid);
      const input = form.querySelector('.pcat-add-input');
      if (!input.value.trim()) {
        input.focus();
        return;
      }
      const category = createCategory(input.value);
      if (!category) {
        markInputInvalid(input);
        return;
      }
      const task = getPlanTasks().find((task) => task.id === btn.dataset.pid);
      if (task) task.tag = category.id;
      save();
      savePlan();
      renderTagRow();
      renderPlan();
    });
  });
  qa('.pcat-add-input').forEach((inp) => {
    inp.addEventListener('keydown', (event) => {
      if (event.key === 'Enter')
        inp.closest('.pcat-add-form').querySelector('.pcat-add-ok').click();
      if (event.key === 'Escape')
        inp.closest('.pcat-add-form').querySelector('.pcat-add-cancel2').click();
    });
  });
  qa('.pcat-add-cancel2').forEach((btn) => {
    btn.addEventListener('click', () => {
      const form = document.getElementById('pcaf-' + btn.dataset.pid);
      form.classList.remove('open');
      const addBtn = form.closest('.pcat-add-row').querySelector('.pcat-add-btn');
      if (addBtn) addBtn.style.display = '';
    });
  });

  // Status change — handles pending/blocked entry creation and in-flight comment carry-over
  qa('.plan-status').forEach((sel) => {
    sel.addEventListener('change', () => {
      const planTasks = getPlanTasks();
      const task = planTasks.find((planTask) => planTask.id === sel.dataset.pid);
      if (!task) return;
      const prevStatus = task.status;
      const newStatus = sel.value;
      wlLog.info('planTask: status changed', { id: task.id, from: prevStatus, to: newStatus });

      // Capture in-flight typed text BEFORE re-render
      let liveTyped = null;
      if (_pendingCommentId === task.id) {
        const inp = document.getElementById('pc-inp-' + task.id);
        liveTyped = inp ? inp.value : _pendingCommentText;
      }

      task.status = newStatus;
      if (newStatus === 'done' && !task.completedAt) task.completedAt = Date.now();
      if (newStatus !== 'done') delete task.completedAt;

      // If child goes inprogress, promote parent too (unless already done)
      if (newStatus === 'inprogress' && task.parentId) {
        const parent = planTasks.find((planTask) => planTask.id === task.parentId);
        if (parent && parent.status === 'todo') {
          parent.status = 'inprogress';
        }
      }
      // When marking done, retire older versions of the same task
      if (newStatus === 'done') {
        planTasks
          .filter(
            (planTask) =>
              planTask.id !== task.id &&
              planTask.text.toLowerCase() === task.text.toLowerCase() &&
              planTask.status !== 'done'
          )
          .forEach((planTask) => {
            planTask.status = 'done';
            if (!planTask.completedAt) planTask.completedAt = task.completedAt;
          });
      }
      // Auto-complete parent when all its children are done
      if (newStatus === 'done' && task.parentId) {
        const parent = planTasks.find((planTask) => planTask.id === task.parentId);
        if (parent && parent.status !== 'done') {
          const siblings = planTasks.filter(
            (child) => child.parentId === parent.id && child.date === task.date
          );
          if (
            siblings.length > 0 &&
            siblings.every((sibling) => sibling.status === 'done' || sibling.id === task.id)
          ) {
            parent.status = 'done';
            if (!parent.completedAt) parent.completedAt = Date.now();
          }
        }
      }
      // Auto-stop timer when active task is marked done
      if (newStatus === 'done' && getActiveTimer()) {
        const timerEntry = getEntries().find((entry) => entry.id === getActiveTimer().entryId);
        if (timerEntry && timerEntry.text.toLowerCase() === task.text.toLowerCase()) {
          stopTimer();
        }
      }

      // Pending/blocked transitions
      const wasPB = prevStatus === 'pending' || prevStatus === 'blocked';
      const isPB = newStatus === 'pending' || newStatus === 'blocked';

      if (isPB && newStatus !== prevStatus) {
        if (!task.statusComments) task.statusComments = [];
        const last = task.statusComments[task.statusComments.length - 1];
        const inFlight = _pendingCommentId === task.id;

        if (wasPB && inFlight && last && !last.comment) {
          // Same comment session — just relabel the unsaved entry,
          // preserving the typed-but-unsaved text via _pendingCommentText.
          last.status = newStatus;
          _pendingCommentText = liveTyped != null ? liveTyped : _pendingCommentText || '';
          // _pendingCommentId stays set
        } else {
          // Fresh session
          task.statusComments.push({ status: newStatus, comment: '', ts: Date.now() });
          _pendingCommentId = task.id;
          _pendingCommentText = '';
        }
      } else if (!isPB) {
        // Leaving pending/blocked — only drop a trailing unsaved entry
        // if this task had an in-flight comment session (otherwise it could
        // be a deliberately-saved empty entry).
        if (_pendingCommentId === task.id && task.statusComments && task.statusComments.length) {
          const last = task.statusComments[task.statusComments.length - 1];
          if (!last.comment && (last.status === 'pending' || last.status === 'blocked')) {
            task.statusComments.pop();
          }
        }
        if (_pendingCommentId === task.id) {
          _pendingCommentId = null;
          _pendingCommentText = '';
        }
      }

      savePlan();
      renderPlan();
      renderCompleted();
    });
  });

  bindPlanCommentEvents(qa);

  // Dismiss handoff note
  qa('.plan-handoff-dismiss').forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.stopPropagation();
      try {
        const notes = JSON.parse(localStorage.getItem('wl_handoff') || '{}');
        delete notes[btn.dataset.task];
        localStorage.setItem('wl_handoff', JSON.stringify(notes));
      } catch (err) {
        wlLog.warn('plan-handoff-dismiss: failed to update wl_handoff in localStorage', err);
      }
      renderPlan();
    });
  });

  bindPlanNoteEvents(qa);

  bindPlanCheckpointEvents(qa);

  // Edit task text
  qa('.plan-edit-btn').forEach((btn) => {
    btn.addEventListener('click', () => {
      editingPlanId = btn.dataset.pid;
      renderPlan();
    });
  });

  const editOk = document.getElementById('planEditOk');
  if (editOk) {
    const saveEdit = () => {
      const inp = document.getElementById('planEditInput');
      const text = inp ? inp.value.trim() : '';
      if (!text) {
        editingPlanId = null;
        renderPlan();
        return;
      }
      const task = getPlanTasks().find((task) => task.id === editOk.dataset.pid);
      if (task) task.text = text;
      editingPlanId = null;
      savePlan();
      renderPlan();
    };
    editOk.addEventListener('click', saveEdit);
    const inp = document.getElementById('planEditInput');
    if (inp) {
      inp.focus();
      inp.select();
      inp.addEventListener('keydown', (event) => {
        if (event.key === 'Enter') saveEdit();
        if (event.key === 'Escape') {
          editingPlanId = null;
          renderPlan();
        }
      });
    }
  }
  const editCancel = document.getElementById('planEditCancel');
  if (editCancel)
    editCancel.addEventListener('click', () => {
      editingPlanId = null;
      renderPlan();
    });

  // Start timer from task
  qa('.plan-log-btn').forEach((btn) => {
    btn.addEventListener('click', () => {
      const task = getPlanTasks().find((task) => task.id === btn.dataset.pid);
      const text = btn.dataset.text;
      const tag = task ? task.tag || 'other' : getSelectedTag();
      if (getActiveTimer()) stopTimer();
      const entry = createRestartedEntry(text, tag);
      getEntries().push(entry);
      promoteMatchingTaskToInProgress(text);
      ensureDayStarted();
      setViewDate(new Date());
      save();
      startTimer(entry.id);
      render();
    });
  });

  // Delete task (children become orphaned top-level tasks)
  qa('.plan-del-btn').forEach((btn) => {
    btn.addEventListener('click', () => {
      setPlanTasks(getPlanTasks().filter((task) => task.id !== btn.dataset.pid));
      savePlan();
      renderPlan();
    });
  });

  // Split into subtasks
  qa('.plan-split-btn').forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.stopPropagation();
      splitInputId = splitInputId === btn.dataset.pid ? null : btn.dataset.pid;
      renderPlan();
      if (splitInputId) {
        const inp = document.getElementById('splitInp-' + splitInputId);
        if (inp) inp.focus();
      }
    });
  });
  qa('.plan-split-done').forEach((btn) => {
    btn.addEventListener('click', () => {
      splitInputId = null;
      renderPlan();
    });
  });
  qa('.plan-split-input').forEach((inp) => {
    inp.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        const text = inp.value.trim();
        if (!text) return;
        const parentId = inp.closest('.plan-split-row').dataset.parent;
        const parent = getPlanTasks().find((task) => task.id === parentId);
        getPlanTasks().push({
          id: Date.now() + '',
          text,
          status: 'todo',
          date: dk(new Date()),
          tag: parent ? parent.tag : getSelectedTag(),
          parentId,
        });
        savePlan();
        inp.value = '';
        renderPlan();
        // Re-focus the new input after re-render
        const newInp = document.getElementById('splitInp-' + parentId);
        if (newInp) newInp.focus();
      } else if (event.key === 'Escape') {
        splitInputId = null;
        renderPlan();
      }
    });
  });
}