Source: scripts/board.js

/**
 * Initializes the board page by loading necessary components,
 * retrieving board columns from the DOM, fetching contacts and todos
 * from the Firebase database, and loading demo data.
 *
 * @returns {Promise<void>} A promise that resolves when the initialization is complete.
 */
async function init() {
  loadComponents();
  getBoardColumnsFromDOM();
  await getContactsFromData("guest");
  await getTodosFromData("guest");
  initRender();
}

/**
 * Loads all necessary components into the page.
 *
 * Currently, this function only loads the header and navbar components.
 * @returns {void}
 */
function loadComponents() {
  loadHeader();
  loadNavbar();
}

/**
 * Loads the header component into the element with the id "header".
 * If no element with that id exists, this function does nothing.
 * @returns {void}
 */
function loadHeader() {
  const header = document.getElementById("header");
  if (!header) return;
  header.innerHTML = getHeaderTemplate();
}

/**
 * Loads the navbar component into the element with the id "navbar".
 * If no element with that id exists, this function does nothing.
 *
 * @returns {void}
 */
function loadNavbar() {
  const navbar = document.getElementById("navbar");
  if (!navbar) return;
  navbar.innerHTML = getNavbarTemplate("board");
}

/**
 * Initializes the rendering of the board page by rendering all todos
 * and placeholder elements. This function is called once, when the page
 * is loaded.
 *
 * @returns {void}
 */
function initRender() {
  renderTodos(globalTodos);
  renderAllPlaceholder();
}

/**
 * Renders a list of todos on the board by inserting the corresponding HTML elements
 * into the todo, progress, feedback, or done columns based on the state of each todo.
 * Additionally, this function sets the tooltip for the progress bar of each todo and
 * renders the assigned members for each todo.
 *
 * @param {Todo[]} todos The list of todos to be rendered.
 *
 * @returns {void}
 */
function renderTodos(todos) {
  if (!todoColumn || !progressColumn || !feedbackColumn || !doneColumn) return;
  todos.forEach((todo, index) => {
    const todoElement = getTaskCardSmallTemplate(index, todo);
    switch (todo.state) {
      case "todo":
        todoColumn.insertAdjacentHTML("beforeend", todoElement);
        setProgressBarTooltip(index, todo.subTasks);
        break;
      case "progress":
        progressColumn.insertAdjacentHTML("beforeend", todoElement);
        setProgressBarTooltip(index, todo.subTasks);
        break;
      case "feedback":
        feedbackColumn.insertAdjacentHTML("beforeend", todoElement);
        setProgressBarTooltip(index, todo.subTasks);
        break;
      case "done":
        doneColumn.insertAdjacentHTML("beforeend", todoElement);
        setProgressBarTooltip(index, todo.subTasks);
        break;
    }
    renderAssignedMembersForTodo(index, todo);
    addMobileMenu(index, todo);
    handleDragEvents();
  });
}

/**
 * Adds a dropdown menu to the bottom of the card for switching between todo states
 * on mobile devices. The menu shows all states except for the current one.
 *
 * @param {number} index The index of the todo in `globalTodos`.
 * @param {Todo} todo The todo object to be rendered.
 *
 * @returns {void}
 */
function addMobileMenu(index, todo) {
  const cardElement = document.getElementById(`card-switch-state-${index}`);
  const stateButtonElements = Object.entries(todoStates)
    .filter(([state]) => state !== todo.state)
    .map(([state, label]) => {
      const buttonElement = document.createElement("button");
      buttonElement.classList.add("inter-medium");
      buttonElement.textContent = label;
      buttonElement.onclick = (event) => {
        event.stopPropagation();
        updateTodoMobile(index, state);
      };
      return buttonElement;
    });
  cardElement.append(...stateButtonElements);
}

/**
 * Updates the state of a todo item in the global todos array and patches the updated todos object
 * in the Firebase Realtime Database. If the update is successful, a success toast message is shown
 * and the board is re-rendered. If there is an error, an error toast message is displayed.
 *
 * @param {number} index - The index of the todo item to update.
 * @param {string} newState - The new state to set for the todo item.
 * @returns {Promise<void>} - A promise that resolves when the update operation is complete.
 */
async function updateTodoMobile(index, newState) {
  const updatedTodos = [...globalTodos];
  updatedTodos[index].state = newState;

  try {
    const response = await updateTodosInFirebase("guest", arrayToObject(updatedTodos));
    if (response.ok) {
      showToastMessage("todoUpdated", response);
      triggerRender();
    } else showToastMessage("error", response);
  } catch (error) {
    showToastMessage("error", error);
  }
}

/**
 * Handles the search bar input event.
 *
 * When the user types in the search bar, this function
 * filters the list of todos by the search term. If the search term is empty,
 * the function clears the board columns and renders all todos. Otherwise, it
 * clears the board columns and renders only the filtered todos.
 *
 * @param {Event} event The input event from the search bar.
 * @returns {void}
 */
function searchTodos(event) {
  const {
    key,
    target: { value: searchTerm },
  } = event;
  if (key === "Enter") return;

  const filteredTodos = globalTodos.filter(({ title, description }) =>
    [title, description].some((text) => text.toLowerCase().includes(searchTerm.toLowerCase()))
  );

  if (searchTerm === "") renderSpecificTodos(globalTodos);
  else renderSpecificTodos(filteredTodos);
}

/**
 * Opens the state change menu for a specific todo card, allowing the user to select a new state
 * for the todo. The menu is anchored to the todo card and remains open until a click outside the
 * menu is detected. Once an outside click is detected, the menu is closed and the original
 * onclick event for the todo card is restored.
 *
 * @param {Event} event - The event object from the click event that triggered the menu opening.
 * @param {number} todoIndex - The index of the todo card for which the state change menu should be opened.
 * @returns {void}
 */
function openStateChangeMenu(event, todoIndex) {
  event.stopPropagation();
  const stateChangeMenu = document.getElementById(`card-switch-state-${todoIndex}`);
  const todoCard = document.getElementById(`task-card-small-${todoIndex}`);
  const profileMenu = document.getElementById("profile-menu");

  if (profileMenu && !profileMenu.classList.contains("d_none")) profileMenu.classList.add("d_none");
  if (currentlyOpenMenu && currentlyOpenMenu !== stateChangeMenu) currentlyOpenMenu.classList.add("d_none");

  stateChangeMenu.classList.toggle("d_none");
  currentlyOpenMenu = stateChangeMenu.classList.contains("d_none") ? null : stateChangeMenu;
  todoCard.onclick = null;

  /**
   * Handles an outside click event by closing the state change menu, removing the
   * outside click event listener, and restoring the original onclick event for the
   * todo card.
   *
   * @param {{ target: HTMLElement }} event - The event object from the click event
   * @returns {void}
   */
  const handleOutsideClick = ({ target }) => {
    if (!stateChangeMenu.contains(target)) {
      stateChangeMenu.classList.add("d_none");
      document.removeEventListener("click", handleOutsideClick);
      todoCard.onclick = () => openTodoModal(todoIndex);
      currentlyOpenMenu = null;
    }
  };
  document.addEventListener("click", handleOutsideClick);
}

/**
 * Updates a todo in the global todos array and patches the todos object in the Firebase Realtime Database.
 *
 * @param {string} state - The new state of the todo.
 * @returns {Promise<void>} - A promise that resolves when the update is complete.
 */
async function updateTodo(state) {
  globalTodos[currentlyDraggedElement].state = state;
  const todosObject = arrayToObject(globalTodos);
  const response = await updateTodosInFirebase("guest", todosObject);
  if (response.status === 400) showToastMessage("error", response);
}

/**
 * Deletes the todo at the specified index from the global todos array and
 * from the Firebase Realtime Database. If the deletion is successful, a
 * toast message is shown. Otherwise, an error is logged to the console.
 *
 * @param {number} index - The index of the todo to be deleted.
 * @returns {Promise<void>} - A promise that resolves when the deletion is complete.
 */
async function deleteTodo(index) {
  const todo = globalTodos[index];
  if (!todo) return;

  const todoID = `TODO${todo.createdAt}`;
  const response = await deleteTodosInFirebase("guest", todoID);
  globalTodos.splice(index, 1);

  if (response.ok) showToastMessage("todoDeleted", response);
  else showToastMessage("error", response);
}

/**
 * Opens the big card modal in view mode for the todo item at the given index.
 * This will populate the big card modal with the todo item's information and
 * make the modal visible.
 *
 * @param {number} index - The index of the todo item in the globalTodos array
 * @returns {void}
 */
function openTodoModal(index, isFromEdit = false) {
  const currentTodo = globalTodos[index];
  const renderContainer = document.getElementById("big-card-modal-background");
  renderContainer.innerHTML = getTaskCardBigTemplate(currentTodo, index);
  const bigCardModalBackground = document.getElementById("big-card-modal-background");
  bigCardModalBackground.classList.remove("d_none");
  document.body.style.overflow = "hidden";

  if (!isFromEdit) applyCardAnimation("slide-in");
  checkScrollbar();
  toggleSubtaskModalWrapperVisibility();
  selectedOptions.length = 0;
}

/**
 * Applies the given animation to the big card modal content element.
 *
 * @param {string} animationType - The type of animation to apply.
 */
function applyCardAnimation(animationType) {
  const modalContent = document.getElementById("big-card-modal") || document.getElementById("closeEditContainer");
  modalContent.style.animation = `${animationType} 0.3s ease-out forwards`;
}

/**
 * Opens the big card modal in edit view for the todo item at the given index.
 * This will populate the big card modal with the todo item's information and
 * highlight the currently selected priority.
 *
 * @param {number} index - The index of the todo item in the globalTodos array
 * @returns {void}
 */
function openTodoModalEdit(index) {
  const currentTodo = globalTodos[index];
  const renderContainer = document.getElementById("big-card-modal-background");
  renderContainer.innerHTML = getTaskCardBigEditTemplate(currentTodo, index);

  const prioritySelects = document.querySelectorAll(".bc-prio-select");
  prioritySelects.forEach((select) => select.classList.remove("active"));

  switch (currentTodo.priority) {
    case "high":
      document.getElementById("bc-select-urgent").classList.add("active");
      break;
    case "medium":
      document.getElementById("bc-select-medium").classList.add("active");
      break;
    case "low":
      document.getElementById("bc-select-low").classList.add("active");
      break;
    default:
      break;
  }
  restrictPastDatePick();
  renderContactDropdown(currentTodo.assignedMembers);
  loadSubtasks(currentTodo);
  initializeBadges();
  checkScrollbar();
}

/**
 * Toggles the big card modal's visibility and animation based on the overflow state
 * of the body. If the body is set to "hidden", the big card modal is displayed and
 * the body is set to "auto", otherwise the big card modal is hidden and the body
 * is set to "hidden".
 *
 * @param {number} index - The index of the todo item in the globalTodos array
 * @returns {void}
 */
function toggleTodoModal(index) {
  checkScrollbar();
  document.body.style.overflow = document.body.style.overflow === "hidden" ? "auto" : "hidden";
  const bigCardModalBackground = document.getElementById("big-card-modal-background");
  if (!bigCardModalBackground) return;

  const closeEditContainer = bigCardModalBackground.querySelector("#closeEditContainer");
  if (closeEditContainer) openTodoModal(index);
  else {
    applyCardAnimation("slide-out");
    setTimeout(() => {
      bigCardModalBackground.classList.toggle("d_none");
    }, 300);
  }
}

/**
 * Saves the changes made to a todo item in the big card modal and updates the
 * corresponding element in the globalTodos array. After saving the changes, the
 * big card modal is closed and the board is re-rendered.
 *
 * @param {number} index - The index of the todo item in the globalTodos array
 * @returns {Promise<void>} - Resolves when the save operation is complete
 */
async function saveEditedTodo(index) {
  const todo = globalTodos[index];
  if (!todo) return;

  const { title, description, dueDate, selectedPriority, assignedMembers } = getFormData(
    "bc-select-urgent",
    "bc-select-medium",
    "bc-select-low",
    "bc-todo-titel",
    "bc-description-textarea",
    "due-date"
  );
  if (!validateTodoForm()) return;
  Object.assign(todo, { title, description, date: dueDate, priority: selectedPriority, subTasks, assignedMembers });

  try {
    const response = await updateTodosInFirebase("guest", arrayToObject(globalTodos));
    const messageType = response.ok ? "todoUpdated" : "error";
    showToastMessage(messageType, response);
  } catch (error) {
    showToastMessage("error", error);
  }

  openTodoModal(index, true);
  triggerRender();
  selectedOptions.length = 0;
  subTasks = {};
}

/**
 * Deletes a task card from the board and updates the UI.
 *
 * This function deletes a task card at the specified index from the global todos array,
 * closes the task card modal, clears all columns on the board, and re-renders
 * the remaining todos and placeholder elements.
 *
 * @param {number} index - The index of the task card to be deleted.
 * @returns {Promise<void>} - A promise that resolves when the deletion and UI update are complete.
 */
async function deleteTaskCard(index) {
  await deleteTodo(index);
  toggleTodoModal(index);
  triggerRender();
}