Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**
Expand Down Expand Up @@ -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.
Expand All @@ -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.

---
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Long> stats = taskService.getDashboardStats(user);
model.addAllAttributes(stats);

List<Task> tasks = taskService.getTasksByUser(user);

// Data for status chart
Map<String, Long> statusCounts = tasks.stream()
.collect(Collectors.groupingBy(t -> t.getStatus().name(), Collectors.counting()));
model.addAttribute("statusCounts", statusCounts);

// Data for priority chart
Map<String, Long> priorityCounts = tasks.stream()
.collect(Collectors.groupingBy(t -> t.getPriority().name(), Collectors.counting()));
model.addAttribute("priorityCounts", priorityCounts);

return "dashboard";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
31 changes: 19 additions & 12 deletions src/main/java/com/lucc/taskmanager/init/DataLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/com/lucc/taskmanager/model/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Task, Integer>
{
List<Task> 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);
}
40 changes: 40 additions & 0 deletions src/main/java/com/lucc/taskmanager/service/TaskService.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,6 +29,9 @@ public List<Task> 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);
}

Expand All @@ -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);
}

Expand All @@ -57,4 +73,28 @@ public void deleteTask(int taskId, User user)

taskRepository.delete(task);
}

public Map<String, Long> getDashboardStats(User user) {
LocalDate now = LocalDate.now();
LocalDate weekAgo = now.minusDays(7);

Map<String, Long> 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);
}
}
Loading
Loading