diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg index fe9c87b..e9ee233 100644 --- a/.github/badges/branches.svg +++ b/.github/badges/branches.svg @@ -1 +1 @@ -branches31.2% \ No newline at end of file +branches43.3% \ No newline at end of file diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index bf86c61..b649c50 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -1 +1 @@ -coverage72.9% \ No newline at end of file +coverage72% \ No newline at end of file diff --git a/README.md b/README.md index cb76029..1d0c5e1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ This project is designed to be easily testable by anyone, whether they are a dev ### **1. The Full Experience (Web UI)** * **Register a New Account**: Navigate to `http://localhost:8080/register` and create your own user. -* **Manage Your Tasks**: Add, edit, and delete tasks. Notice the **AJAX-powered deletion** that happens instantly without a page reload. +* **Explore the Dashboard**: Visit the new **Dashboard** to see live statistics like pending tasks, overdue items, and your weekly completion rate. Check out the interactive charts for status and priority distribution. +* **Manage Your Tasks**: Add tasks with **Due Dates**. Use the new **Quick Toggle** (Done/Undone) to update task status instantly from the list. * **Security Check**: Try to access `http://localhost:8080/tasks` without logging in—you'll be automatically redirected to the secure login page. ### **2. Interactive API Testing (Swagger)** @@ -67,7 +68,8 @@ To run the tests locally, use: ### **Frontend & UX** * **Thymeleaf**: Modern server-side Java template engine. * **Bootstrap 5**: Responsive, mobile-first design for a professional look. -* **AJAX (Fetch API)**: Interactive, "snappy" UI features like task deletion without full-page reloads. +* **Chart.js**: Interactive data visualization for task statistics. +* **AJAX (Fetch API)**: Interactive, "snappy" UI features like task deletion and status toggling without full-page reloads. ### **DevOps & Documentation** * **Docker & Docker Compose**: Full containerization for one-command environment setup. @@ -77,10 +79,12 @@ To run the tests locally, use: --- ## ✨ Key Features +* **Interactive Dashboard**: Real-time summary of pending, overdue, and recently completed tasks with visual charts. * **Full CRUD Cycle**: Create, Read, Update, and Delete tasks. +* **Due Date Tracking**: Set deadlines for tasks and track overdue items. +* **Quick Status Toggle**: Instantly mark tasks as Done or Undone from the main list view. * **User Lifecycle**: Public registration and secure login system. * **Task Metadata**: Categorize tasks by **Status** (TODO, DONE) and **Priority** (HIGH, MEDIUM, LOW). -* **Interactive UI**: Smooth, JavaScript-powered deletions with real-time DOM updates. * **RESTful API**: Comprehensive API endpoints for programmatic task management, fully documented with Swagger. --- diff --git a/src/main/java/com/lucc/taskmanager/controller/DashboardController.java b/src/main/java/com/lucc/taskmanager/controller/DashboardController.java new file mode 100644 index 0000000..cecd83d --- /dev/null +++ b/src/main/java/com/lucc/taskmanager/controller/DashboardController.java @@ -0,0 +1,45 @@ +package com.lucc.taskmanager.controller; + +import com.lucc.taskmanager.model.Task; +import com.lucc.taskmanager.model.User; +import com.lucc.taskmanager.service.TaskService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Controller +@RequestMapping("/dashboard") +public class DashboardController { + + private final TaskService taskService; + + public DashboardController(TaskService taskService) { + this.taskService = taskService; + } + + @GetMapping + public String getDashboard(@AuthenticationPrincipal User user, Model model) { + Map stats = taskService.getDashboardStats(user); + model.addAllAttributes(stats); + + List tasks = taskService.getTasksByUser(user); + + // Data for status chart + Map statusCounts = tasks.stream() + .collect(Collectors.groupingBy(t -> t.getStatus().name(), Collectors.counting())); + model.addAttribute("statusCounts", statusCounts); + + // Data for priority chart + Map priorityCounts = tasks.stream() + .collect(Collectors.groupingBy(t -> t.getPriority().name(), Collectors.counting())); + model.addAttribute("priorityCounts", priorityCounts); + + return "dashboard"; + } +} diff --git a/src/main/java/com/lucc/taskmanager/controller/TaskController.java b/src/main/java/com/lucc/taskmanager/controller/TaskController.java index 954e088..f19a799 100644 --- a/src/main/java/com/lucc/taskmanager/controller/TaskController.java +++ b/src/main/java/com/lucc/taskmanager/controller/TaskController.java @@ -51,4 +51,11 @@ public void deleteTask(@PathVariable int taskId, @AuthenticationPrincipal @Param { taskService.deleteTask(taskId, user); } + + @PatchMapping("/{taskId}/toggle") + @Operation(summary = "Toggle task status between TODO and DONE") + public Task toggleTaskStatus(@PathVariable int taskId, @AuthenticationPrincipal @Parameter(hidden = true) User user) + { + return taskService.toggleTaskStatus(taskId, user); + } } diff --git a/src/main/java/com/lucc/taskmanager/init/DataLoader.java b/src/main/java/com/lucc/taskmanager/init/DataLoader.java index d1c9535..e8371e2 100644 --- a/src/main/java/com/lucc/taskmanager/init/DataLoader.java +++ b/src/main/java/com/lucc/taskmanager/init/DataLoader.java @@ -12,6 +12,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import java.time.LocalDate; import java.util.List; @Component @@ -39,20 +40,26 @@ public void run(String ... args) throws Exception userRepository.saveAll(List.of(admin, user)); System.out.println("Users loaded"); - Task adminTask1 = new Task("Pizza", "Pizza is a dish of Italian origin made by a mixture of fried bread and tomatoes, usually sliced thinly and topped with a slice of cheese.", admin, Status.TODO, Priority.MEDIUM); - Task adminTask2 = new Task("Burger", "A burger is a large, flat, round, steak dressed in lettuce, tomato, cheese, and meat sauce.", admin, Status.TODO, Priority.MEDIUM); - Task adminTask3 = new Task("Pasta", "Pasta is a food made from a mixture of flour, eggs, and water, typically cooked under high heat.", admin, Status.TODO, Priority.MEDIUM); - Task adminTask4 = new Task("Sandwich", "A sandwich is a flat bread with lettuce and tomato on one side, and a slice of cheese on the other side.", admin, Status.TODO, Priority.MEDIUM); - Task adminTask5 = new Task("Coffee", "Coffee is a brewed drink made from roasted coffee beans, ground coffee, and sugar.", admin, Status.TODO, Priority.MEDIUM); - Task adminTask6 = new Task("Coffee Shaker", "A coffee shaker is a container for storing and shaking coffee.", admin, Status.TODO, Priority.MEDIUM); - Task adminTask7 = new Task("Coffee Mug", "A coffee mug is a container for storing and shaking coffee.", admin, Status.TODO, Priority.MEDIUM); + LocalDate today = LocalDate.now(); + Task adminTask1 = new Task("Pizza", "Pizza description", admin, Status.TODO, Priority.MEDIUM, today.plusDays(2)); + Task adminTask2 = new Task("Burger", "Burger description", admin, Status.TODO, Priority.HIGH, today.minusDays(1)); // Overdue + Task adminTask3 = new Task("Pasta", "Pasta description", admin, Status.DONE, Priority.LOW, today.minusDays(2)); + adminTask3.setCompletionDate(today.minusDays(1)); // Completed this week + + Task adminTask4 = new Task("Sandwich", "Sandwich description", admin, Status.TODO, Priority.MEDIUM, today.plusDays(5)); + Task adminTask5 = new Task("Coffee", "Coffee description", admin, Status.TODO, Priority.MEDIUM, today.plusDays(1)); + Task adminTask6 = new Task("Coffee Shaker", "Coffee shaker description", admin, Status.TODO, Priority.MEDIUM, today.plusDays(3)); + Task adminTask7 = new Task("Coffee Mug", "Coffee mug description", admin, Status.DONE, Priority.MEDIUM, today.minusDays(10)); + adminTask7.setCompletionDate(today.minusDays(8)); // Completed more than a week ago - Task userTask1 = new Task("Pizza", "Pizza is a dish of Italian origin made by a mixture of fried bread and tomatoes, usually sliced thinly and topped with a slice of cheese.", user, Status.TODO, Priority.MEDIUM); - Task userTask2 = new Task("Burger", "A burger is a large, flat, round, steak dressed in lettuce, tomato, cheese, and meat sauce.", user, Status.TODO, Priority.MEDIUM); - Task userTask3 = new Task("Pasta", "Pasta is a food made from a mixture of flour, eggs, and water, typically cooked under high heat.", user, Status.TODO, Priority.MEDIUM); - Task userTask4 = new Task("Sandwich", "A sandwich is a flat bread with lettuce and tomato on one side, and a slice of cheese on the other side.", user, Status.TODO, Priority.MEDIUM); - Task userTask5 = new Task("Coffee", "Coffee is a brewed drink made from roasted coffee beans, ground coffee, and sugar.", user, Status.TODO, Priority.MEDIUM); + Task userTask1 = new Task("Study Spring Boot", "Learn about Spring Boot security and data.", user, Status.TODO, Priority.HIGH, today.plusDays(3)); + Task userTask2 = new Task("Workout", "Go to the gym for 1 hour.", user, Status.TODO, Priority.MEDIUM, today.minusDays(2)); // Overdue + Task userTask3 = new Task("Buy Groceries", "Buy milk, eggs, and bread.", user, Status.DONE, Priority.LOW, today.minusDays(1)); + userTask3.setCompletionDate(today); // Completed today + + Task userTask4 = new Task("Read a Book", "Read at least 30 pages.", user, Status.TODO, Priority.LOW, today.plusDays(7)); + Task userTask5 = new Task("Clean the Room", "Deep clean the bedroom.", user, Status.TODO, Priority.MEDIUM, today.plusDays(1)); taskRepository.saveAll(List.of(adminTask1, adminTask2, adminTask3, adminTask4, adminTask5, adminTask6, adminTask7, userTask1, userTask2, userTask3, userTask4, userTask5)); System.out.println("Tasks loaded"); diff --git a/src/main/java/com/lucc/taskmanager/model/Task.java b/src/main/java/com/lucc/taskmanager/model/Task.java index 19d620d..5b74415 100644 --- a/src/main/java/com/lucc/taskmanager/model/Task.java +++ b/src/main/java/com/lucc/taskmanager/model/Task.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import java.time.LocalDate; @Entity public class Task @@ -25,6 +26,10 @@ public class Task @Enumerated(EnumType.STRING) private Priority priority; + private LocalDate dueDate; + + private LocalDate completionDate; + @ManyToOne @JoinColumn(name = "user_id") private User user; @@ -40,6 +45,16 @@ public Task(String title, String description, User user, Status status, Priority this.priority = priority; } + public Task(String title, String description, User user, Status status, Priority priority, LocalDate dueDate) + { + this.title = title; + this.description = description; + this.user = user; + this.status = status; + this.priority = priority; + this.dueDate = dueDate; + } + public String getTitle() { return title; @@ -80,6 +95,26 @@ public void setPriority(Priority priority) this.priority = priority; } + public LocalDate getDueDate() + { + return dueDate; + } + + public void setDueDate(LocalDate dueDate) + { + this.dueDate = dueDate; + } + + public LocalDate getCompletionDate() + { + return completionDate; + } + + public void setCompletionDate(LocalDate completionDate) + { + this.completionDate = completionDate; + } + public User getUser() { return user; diff --git a/src/main/java/com/lucc/taskmanager/repository/TaskRepository.java b/src/main/java/com/lucc/taskmanager/repository/TaskRepository.java index 114bf62..eea33da 100644 --- a/src/main/java/com/lucc/taskmanager/repository/TaskRepository.java +++ b/src/main/java/com/lucc/taskmanager/repository/TaskRepository.java @@ -1,12 +1,20 @@ package com.lucc.taskmanager.repository; +import com.lucc.taskmanager.model.Status; import com.lucc.taskmanager.model.Task; import com.lucc.taskmanager.model.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDate; import java.util.List; public interface TaskRepository extends JpaRepository { List findByUser(User user); + + long countByUserAndStatus(User user, Status status); + + long countByUserAndStatusAndDueDateBefore(User user, Status status, LocalDate date); + + long countByUserAndStatusAndCompletionDateBetween(User user, Status status, LocalDate start, LocalDate end); } diff --git a/src/main/java/com/lucc/taskmanager/service/TaskService.java b/src/main/java/com/lucc/taskmanager/service/TaskService.java index 4d92a8a..9639692 100644 --- a/src/main/java/com/lucc/taskmanager/service/TaskService.java +++ b/src/main/java/com/lucc/taskmanager/service/TaskService.java @@ -1,11 +1,15 @@ package com.lucc.taskmanager.service; +import com.lucc.taskmanager.model.Status; import com.lucc.taskmanager.model.Task; import com.lucc.taskmanager.model.User; import com.lucc.taskmanager.repository.TaskRepository; import org.springframework.stereotype.Service; +import java.time.LocalDate; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service public class TaskService @@ -25,6 +29,9 @@ public List getTasksByUser(User user) public Task addTask(Task task, User user) { task.setUser(user); + if (task.getStatus() == Status.DONE && task.getCompletionDate() == null) { + task.setCompletionDate(LocalDate.now()); + } return taskRepository.save(task); } @@ -41,10 +48,19 @@ public Task getTaskById(int taskId, User user) public Task updateTask(int taskId, Task updatedTask, User user) { Task existingTask = getTaskById(taskId, user); + + // Handle completion date logic + if (existingTask.getStatus() != Status.DONE && updatedTask.getStatus() == Status.DONE) { + existingTask.setCompletionDate(LocalDate.now()); + } else if (existingTask.getStatus() == Status.DONE && updatedTask.getStatus() != Status.DONE) { + existingTask.setCompletionDate(null); + } + existingTask.setTitle(updatedTask.getTitle()); existingTask.setDescription(updatedTask.getDescription()); existingTask.setStatus(updatedTask.getStatus()); existingTask.setPriority(updatedTask.getPriority()); + existingTask.setDueDate(updatedTask.getDueDate()); return taskRepository.save(existingTask); } @@ -57,4 +73,28 @@ public void deleteTask(int taskId, User user) taskRepository.delete(task); } + + public Map getDashboardStats(User user) { + LocalDate now = LocalDate.now(); + LocalDate weekAgo = now.minusDays(7); + + Map stats = new HashMap<>(); + stats.put("pending", taskRepository.countByUserAndStatus(user, Status.TODO)); + stats.put("overdue", taskRepository.countByUserAndStatusAndDueDateBefore(user, Status.TODO, now)); + stats.put("completedThisWeek", taskRepository.countByUserAndStatusAndCompletionDateBetween(user, Status.DONE, weekAgo, now)); + + return stats; + } + + public Task toggleTaskStatus(int taskId, User user) { + Task task = getTaskById(taskId, user); + if (task.getStatus() == Status.TODO) { + task.setStatus(Status.DONE); + task.setCompletionDate(LocalDate.now()); + } else { + task.setStatus(Status.TODO); + task.setCompletionDate(null); + } + return taskRepository.save(task); + } } diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html new file mode 100644 index 0000000..443235f --- /dev/null +++ b/src/main/resources/templates/dashboard.html @@ -0,0 +1,180 @@ + + + + + + Dashboard - Task Manager + + + + + + + + + + + +
+

Task Dashboard

+ + +
+
+
+
+
Pending Tasks
+

0

+
+
+
+
+
+
+
Overdue Tasks
+

0

+
+
+
+
+
+
+
Completed (7 Days)
+

0

+
+
+
+
+ + +
+
+
+
+
Tasks by Status
+
+ +
+
+
+
+
+
+
+
Tasks by Priority
+
+ +
+
+
+
+
+
+ + + + + + + + diff --git a/src/main/resources/templates/edit-task.html b/src/main/resources/templates/edit-task.html index 2b2bd6e..07299cf 100644 --- a/src/main/resources/templates/edit-task.html +++ b/src/main/resources/templates/edit-task.html @@ -46,6 +46,10 @@

Edit Task

+
+ + +
Cancel diff --git a/src/main/resources/templates/tasks.html b/src/main/resources/templates/tasks.html index 6b9214d..6f8dc7e 100644 --- a/src/main/resources/templates/tasks.html +++ b/src/main/resources/templates/tasks.html @@ -9,17 +9,32 @@ + + @@ -37,12 +52,22 @@

My Tasks

+
+ + Due: + +
+ Edit
@@ -83,6 +108,10 @@

Add New Task

+
+ + +
@@ -95,6 +124,29 @@

Add New Task