CodingNic

Checkpoint Project

Project - Task Manager

Checkpoint Project 90 min read

Project - Task Manager

Project: Task Manager

You’re going to build a console-based Task Manager: a small program that can add tasks, mark them complete, remove them, and list them with filters, all held in memory while the script runs. There’s no file saving and no browser involved, everything lives in variables for the life of the program.

This is a fitting capstone because a task manager touches almost everything from this course: a class to represent a task, a class to manage a collection of tasks, a closure to generate unique ids, array methods to search and filter, template literals to format output, and custom errors to handle a task that doesn’t exist. Nothing here is new. The goal is to combine it all in one working program.

Requirements

Build this in a single Node script. Work through the sections in order, since later pieces depend on earlier ones.

The Task Class

Write a Task class with a constructor that accepts id, title, and priority, and sets:

  • id: the value passed in
  • title: the value passed in
  • priority: the value passed in, one of the strings "low", "medium", or "high"
  • completed: always starts as false
  • createdAt: a new Date() created at construction time

The class itself does not generate the id. That’s the closure generator’s job, described next.

The Closure-Based Id Generator

Write a function createIdGenerator that returns another function. The outer function holds a count variable starting at 0. Each time the returned function is called, it increments count and returns the new value. This is the closures pattern from Module 4: the inner function keeps a private reference to count that nothing outside can reach directly.

javascript
function createIdGenerator() {
  let count = 0;
  return function nextId() {
    count += 1;
    return count;
  };
}

const generateId = createIdGenerator();

Call generateId() once per new task, and use the result as that task’s id. Every task in the program should share the same generator, so ids never repeat.

The TaskManager Class

Write a TaskManager class that stores its tasks in a private field, #tasks, initialized to an empty array. Nothing outside the class should be able to read or overwrite #tasks directly, this is the encapsulation pattern from Module 7.

Implement these methods:

  • addTask(title, priority): creates a new Task using generateId() for the id, pushes it onto #tasks, and returns it.
  • findTask(id): uses .find() to look for a task with a matching id. If no task matches, throw a TaskNotFoundError (see below). Otherwise return the task.
  • completeTask(id): calls findTask(id) to get the task (which throws if it’s missing), sets completed to true, and returns the task.
  • removeTask(id): calls findTask(id) to confirm the task exists (which throws if it’s missing), then uses .filter() to rebuild #tasks without that task, and returns the removed task.
  • listTasks({ filterByPriority, onlyIncomplete } = {}): accepts a single options object, destructured with a default empty object so listTasks() works with no arguments at all. When filterByPriority is provided, keep only tasks with a matching priority using .filter(). When onlyIncomplete is truthy, keep only tasks where completed is false, also with .filter(). Return the matching tasks formatted as strings using .map() (see formatting below), not raw Task objects.

Every one of these methods must use .find(), .filter(), or .map() internally, don’t reach for a manual for loop where an array method already does the job.

Error Handling

Write a TaskNotFoundError class that extends Error. In its constructor, accept the missing id, call super() with a message like Task with id 5 was not found., and set this.name = "TaskNotFoundError".

findTask, completeTask, and removeTask must all throw a TaskNotFoundError when given an id with no matching task, instead of returning undefined or silently doing nothing. This is the custom error pattern from Module 8.

Callers should catch it like this:

javascript
try {
  manager.completeTask(999);
} catch (error) {
  if (error instanceof TaskNotFoundError) {
    console.log(`Caught error: ${error.message}`);
  } else {
    throw error;
  }
}

Formatting Output

Write a formatTask(task) function that returns a single-line string using a template literal, in this exact shape:

text
[HIGH] Buy groceries (incomplete)

The priority is uppercased, wrapped in square brackets, followed by the title, followed by (complete) or (incomplete) depending on the task’s completed value. Use this function inside listTasks so every returned line is already formatted and ready to print.

Example Session

Here is a full reference run. The code below was actually executed to produce the output shown, so you can build this same sequence yourself and expect identical results.

javascript
const manager = new TaskManager();

const t1 = manager.addTask("Buy groceries", "high");
const t2 = manager.addTask("Read chapter 4", "medium");
const t3 = manager.addTask("Email Priya about the project", "low");
const t4 = manager.addTask("Prepare demo for Jordan Reyes", "high");

console.log("All tasks after adding four:");
manager.listTasks().forEach((line) => console.log(line));

manager.completeTask(t1.id);
console.log("\nAfter completing 'Buy groceries':");
console.log(formatTask(manager.findTask(t1.id)));

console.log("\nTrying to complete a task that does not exist:");
try {
  manager.completeTask(999);
} catch (error) {
  if (error instanceof TaskNotFoundError) {
    console.log(`Caught error: ${error.message}`);
  } else {
    throw error;
  }
}

console.log("\nIncomplete high priority tasks:");
manager.listTasks({ filterByPriority: "high", onlyIncomplete: true }).forEach((line) => console.log(line));

console.log("\nAll incomplete tasks:");
manager.listTasks({ onlyIncomplete: true }).forEach((line) => console.log(line));

console.log("\nRemoving 'Read chapter 4':");
manager.removeTask(t2.id);
manager.listTasks().forEach((line) => console.log(line));

Running this prints:

text
All tasks after adding four:
[HIGH] Buy groceries (incomplete)
[MEDIUM] Read chapter 4 (incomplete)
[LOW] Email Priya about the project (incomplete)
[HIGH] Prepare demo for Jordan Reyes (incomplete)

After completing 'Buy groceries':
[HIGH] Buy groceries (complete)

Trying to complete a task that does not exist:
Caught error: Task with id 999 was not found.

Incomplete high priority tasks:
[HIGH] Prepare demo for Jordan Reyes (incomplete)

All incomplete tasks:
[MEDIUM] Read chapter 4 (incomplete)
[LOW] Email Priya about the project (incomplete)
[HIGH] Prepare demo for Jordan Reyes (incomplete)

Removing 'Read chapter 4':
[HIGH] Buy groceries (complete)
[LOW] Email Priya about the project (incomplete)
[HIGH] Prepare demo for Jordan Reyes (incomplete)

Notice that after completing “Buy groceries”, it no longer shows up in “Incomplete high priority tasks”, but it still shows up in the full list, marked (complete). That’s the filtering logic working as intended.

Stretch Goals

These are optional. Try them once the required program above is fully working.

Recursive priority counter. Add a countTasksByPriority(priority) method to TaskManager. A loop or .filter().length would solve this in one line, and that’s fine in real code, but for practice, implement it recursively instead: walk the internal array one index at a time, adding 1 to an accumulator when the priority matches, and calling itself with the next index until it runs past the end of the array. For example:

javascript
countTasksByPriority(priority, index = 0, count = 0) {
  if (index >= this.#tasks.length) {
    return count;
  }
  const matches = this.#tasks[index].priority === priority ? 1 : 0;
  return this.countTasksByPriority(priority, index + 1, count + matches);
}

Due dates. Add an optional dueDate field to Task (a Date or null if not set). Then add a getOverdueTasks() method to TaskManager that uses .filter() to return every incomplete task whose dueDate is earlier than new Date().

With this working, you’ve built one complete program from everything this course has taught. Next: a set of standalone practical challenges to sharpen the same skills on problems you haven’t seen laid out for you first.