From 1688cf092e0935c76a7ce497831044fbc78bce90 Mon Sep 17 00:00:00 2001 From: Gabriel Santana de Menezes Date: Sun, 3 May 2026 22:24:58 -0400 Subject: [PATCH] dashboard --- .../beetask/entities/task/Task.java | 28 +- .../entities/task/TaskHistoryItem.java | 10 + .../config/DashboardUseCaseConfig.java | 15 + .../frameworks/config/TaskUseCaseConfig.java | 5 +- .../persistence/entities/TaskEntity.java | 14 +- .../entities/TaskHistoryItemEntity.java | 47 ++- .../persistence/mapper/TaskEntityMapper.java | 9 +- .../mapper/TaskHistoryItemMapper.java | 6 + .../repository/TaskRepositoryImpl.java | 13 + .../dashboard/DashboardController.java | 47 +++ .../controllers/task/CreateTaskResponse.java | 3 +- .../controllers/task/TaskController.java | 8 +- .../task/TaskControllerMapper.java | 7 +- .../usecase/dashboard/DashboardInput.java | 9 + .../usecase/dashboard/DashboardOutput.java | 11 + .../dashboard/DashboardProjectStats.java | 7 + .../usecase/dashboard/DashboardUseCase.java | 93 ++++++ .../exceptions/UserNotFoundException.java | 7 + .../usecase/repository/TaskRepository.java | 3 + .../usecase/task/create/CreateTaskMapper.java | 1 + .../usecase/task/create/CreateTaskOutput.java | 5 +- .../usecase/task/start/StartTaskInput.java | 3 +- .../usecase/task/start/StartTaskUseCase.java | 11 +- frontend/src/components/AppShell.tsx | 16 + frontend/src/pages/DashboardPage.tsx | 310 ++++++++++++++++++ frontend/src/pages/TaskBoardPage.tsx | 2 +- frontend/src/pages/TaskListPage.tsx | 4 +- frontend/src/router.tsx | 5 + frontend/src/services/dashboardService.ts | 25 ++ frontend/src/types/task.ts | 3 +- 30 files changed, 696 insertions(+), 31 deletions(-) create mode 100644 backend/src/main/java/net/beetechgroup/beetask/frameworks/config/DashboardUseCaseConfig.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/dashboard/DashboardController.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardInput.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardOutput.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardProjectStats.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardUseCase.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/UserNotFoundException.java create mode 100644 frontend/src/pages/DashboardPage.tsx create mode 100644 frontend/src/services/dashboardService.ts diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/task/Task.java b/backend/src/main/java/net/beetechgroup/beetask/entities/task/Task.java index c8663a4..19cb0c7 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/task/Task.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/task/Task.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import net.beetechgroup.beetask.entities.Project; +import net.beetechgroup.beetask.entities.User; public class Task { private Long id; @@ -12,6 +13,7 @@ public class Task { private String description; private TaskStatus status; private Project project; + private LocalDateTime finishedAt; private List history = new ArrayList<>(); public Long getId() { @@ -45,19 +47,26 @@ public class Task { public void setStatus(TaskStatus status) { if (this.status == status) return; - if (status == TaskStatus.IN_PROGRESS) { - if (!isRunning()) { - start(); - } - } else if (this.status == TaskStatus.IN_PROGRESS || isRunning()) { - if (isRunning()) { - stop(); - } + // Note: Automatic start/stop removed from setStatus to avoid + // compilation errors and ensure user attribution via StartTaskUseCase. + + if (status == TaskStatus.COMPLETED) { + this.finishedAt = LocalDateTime.now(); + } else { + this.finishedAt = null; } this.status = status; } + public LocalDateTime getFinishedAt() { + return finishedAt; + } + + public void setFinishedAt(LocalDateTime finishedAt) { + this.finishedAt = finishedAt; + } + public Project getProject() { return project; } @@ -85,7 +94,7 @@ public class Task { .orElse(null); } - public void start() { + public void start(User user) { TaskHistoryItem current = getCurrentRunningItem(); if (current != null) { @@ -95,6 +104,7 @@ public class Task { this.status = TaskStatus.IN_PROGRESS; TaskHistoryItem item = new TaskHistoryItem(); item.setStartAt(LocalDateTime.now()); + item.setUser(user); this.history.add(item); } diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/task/TaskHistoryItem.java b/backend/src/main/java/net/beetechgroup/beetask/entities/task/TaskHistoryItem.java index c053b16..7a7258b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/task/TaskHistoryItem.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/task/TaskHistoryItem.java @@ -1,10 +1,12 @@ package net.beetechgroup.beetask.entities.task; import java.time.LocalDateTime; +import net.beetechgroup.beetask.entities.User; public class TaskHistoryItem { private LocalDateTime startAt; private LocalDateTime endAt; + private User user; public void setEndAt(LocalDateTime endAt) { this.endAt = endAt; @@ -25,4 +27,12 @@ public class TaskHistoryItem { public boolean isRunning() { return endAt == null; } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/DashboardUseCaseConfig.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/DashboardUseCaseConfig.java new file mode 100644 index 0000000..d702212 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/DashboardUseCaseConfig.java @@ -0,0 +1,15 @@ +package net.beetechgroup.beetask.frameworks.config; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import net.beetechgroup.beetask.usecase.dashboard.DashboardUseCase; +import net.beetechgroup.beetask.usecase.repository.TaskRepository; + +@ApplicationScoped +public class DashboardUseCaseConfig { + + @Produces + public DashboardUseCase dashboardUseCase(TaskRepository taskRepository) { + return new DashboardUseCase(taskRepository); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java index 0678b59..c239792 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java @@ -4,6 +4,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import net.beetechgroup.beetask.usecase.repository.ProjectRepository; import net.beetechgroup.beetask.usecase.repository.TaskRepository; +import net.beetechgroup.beetask.usecase.repository.UserRepository; import net.beetechgroup.beetask.usecase.task.create.CreateTaskUseCase; import net.beetechgroup.beetask.usecase.task.listall.ListAllTasksUseCase; import net.beetechgroup.beetask.usecase.task.start.StartTaskUseCase; @@ -19,8 +20,8 @@ public class TaskUseCaseConfig { } @Produces - public StartTaskUseCase startTaskUseCase(TaskRepository taskRepository) { - return new StartTaskUseCase(taskRepository); + public StartTaskUseCase startTaskUseCase(TaskRepository taskRepository, UserRepository userRepository) { + return new StartTaskUseCase(taskRepository, userRepository); } @Produces diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskEntity.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskEntity.java index af0e664..c3f7041 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskEntity.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskEntity.java @@ -2,6 +2,7 @@ package net.beetechgroup.beetask.frameworks.persistence.entities; import java.util.List; +import jakarta.persistence.CascadeType; import jakarta.persistence.CollectionTable; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; @@ -10,6 +11,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import net.beetechgroup.beetask.entities.task.TaskStatus; @@ -22,12 +24,12 @@ public class TaskEntity { private String title; private String description; private TaskStatus status; + private java.time.LocalDateTime finishedAt; @ManyToOne @JoinColumn(name = "project_id") private ProjectEntity project; - @ElementCollection - @CollectionTable(name = "task_history", joinColumns = @JoinColumn(name = "task_id")) + @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) private List history; public void setHistory(List history) { @@ -70,6 +72,14 @@ public class TaskEntity { this.status = status; } + public java.time.LocalDateTime getFinishedAt() { + return finishedAt; + } + + public void setFinishedAt(java.time.LocalDateTime finishedAt) { + this.finishedAt = finishedAt; + } + public ProjectEntity getProject() { return project; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskHistoryItemEntity.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskHistoryItemEntity.java index 21c4636..e36a809 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskHistoryItemEntity.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/TaskHistoryItemEntity.java @@ -2,13 +2,40 @@ package net.beetechgroup.beetask.frameworks.persistence.entities; import java.time.LocalDateTime; -import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; -@Embeddable +@Entity +@Table(name = "task_history") public class TaskHistoryItemEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private LocalDateTime startAt; private LocalDateTime endAt; + @ManyToOne + @JoinColumn(name = "user_id") + private UserEntity user; + + @ManyToOne + @JoinColumn(name = "task_id") + private TaskEntity task; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + public LocalDateTime getStartAt() { return startAt; } @@ -24,4 +51,20 @@ public class TaskHistoryItemEntity { public void setEndAt(LocalDateTime endAt) { this.endAt = endAt; } + + public UserEntity getUser() { + return user; + } + + public void setUser(UserEntity user) { + this.user = user; + } + + public TaskEntity getTask() { + return task; + } + + public void setTask(TaskEntity task) { + this.task = task; + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskEntityMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskEntityMapper.java index 5b3d6f5..2c8cad3 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskEntityMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskEntityMapper.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity; +import net.beetechgroup.beetask.frameworks.persistence.entities.TaskHistoryItemEntity; public class TaskEntityMapper { @@ -13,8 +14,13 @@ public class TaskEntityMapper { entity.setTitle(task.getTitle()); entity.setDescription(task.getDescription()); entity.setStatus(task.getStatus()); + entity.setFinishedAt(task.getFinishedAt()); entity.setProject(ProjectEntityMapper.toEntity(task.getProject())); - entity.setHistory(new ArrayList<>(task.getHistory().stream().map(TaskHistoryItemMapper::toEntity).toList())); + entity.setHistory(new ArrayList<>(task.getHistory().stream().map(h -> { + TaskHistoryItemEntity he = TaskHistoryItemMapper.toEntity(h); + he.setTask(entity); + return he; + }).toList())); return entity; } @@ -26,6 +32,7 @@ public class TaskEntityMapper { task.setTitle(entity.getTitle()); task.setDescription(entity.getDescription()); task.setStatus(entity.getStatus()); + task.setFinishedAt(entity.getFinishedAt()); task.setProject(ProjectEntityMapper.toDomain(entity.getProject())); task.setHistory(new ArrayList<>(entity.getHistory().stream().map(TaskHistoryItemMapper::toDomain).toList())); diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskHistoryItemMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskHistoryItemMapper.java index bb4aef4..a6424b4 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskHistoryItemMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/TaskHistoryItemMapper.java @@ -8,6 +8,9 @@ public class TaskHistoryItemMapper { TaskHistoryItemEntity entity = new TaskHistoryItemEntity(); entity.setStartAt(item.getStartAt()); entity.setEndAt(item.getEndAt()); + if (item.getUser() != null) { + entity.setUser(UserEntityMapper.toEntity(item.getUser())); + } return entity; } @@ -17,6 +20,9 @@ public class TaskHistoryItemMapper { TaskHistoryItem item = new TaskHistoryItem(); item.setStartAt(entity.getStartAt()); item.setEndAt(entity.getEndAt()); + if (entity.getUser() != null) { + item.setUser(UserEntityMapper.toDomain(entity.getUser())); + } return item; } } \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java index 4a28f85..14ba7ea 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java @@ -1,6 +1,7 @@ package net.beetechgroup.beetask.frameworks.persistence.repository; import java.util.List; +import java.time.LocalDateTime; import io.quarkus.hibernate.orm.panache.PanacheRepository; import jakarta.enterprise.context.ApplicationScoped; @@ -37,4 +38,16 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository findAllTasks() { return findAll().list().stream().map(TaskEntityMapper::toDomain).toList(); } + + @Override + public List findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) { + return find("select distinct t from TaskEntity t join t.history h where h.user.email = ?1 and h.startAt >= ?2 and h.startAt <= ?3", + email, start, end).list().stream().map(TaskEntityMapper::toDomain).toList(); + } + + @Override + public List findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) { + return find("select distinct t from TaskEntity t join t.history h where h.user.email = ?1 and t.finishedAt >= ?2 and t.finishedAt <= ?3", + email, start, end).list().stream().map(TaskEntityMapper::toDomain).toList(); + } } \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/dashboard/DashboardController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/dashboard/DashboardController.java new file mode 100644 index 0000000..ca27100 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/dashboard/DashboardController.java @@ -0,0 +1,47 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.dashboard; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import net.beetechgroup.beetask.usecase.dashboard.DashboardInput; +import net.beetechgroup.beetask.usecase.dashboard.DashboardOutput; +import net.beetechgroup.beetask.usecase.dashboard.DashboardUseCase; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Path("/dashboard") +@Produces(MediaType.APPLICATION_JSON) +@Authenticated +public class DashboardController { + + @Inject + DashboardUseCase dashboardUseCase; + + @Inject + SecurityIdentity securityIdentity; + + @GET + public DashboardOutput getDashboard( + @QueryParam("startDate") String startDateStr, + @QueryParam("endDate") String endDateStr) { + + String email = securityIdentity.getPrincipal().getName(); + + LocalDateTime start = (startDateStr != null) + ? LocalDateTime.parse(startDateStr, DateTimeFormatter.ISO_DATE_TIME) + : LocalDateTime.now().minusMonths(1); + + LocalDateTime end = (endDateStr != null) + ? LocalDateTime.parse(endDateStr, DateTimeFormatter.ISO_DATE_TIME) + : LocalDateTime.now(); + + DashboardInput input = new DashboardInput(email, start, end); + return dashboardUseCase.execute(input); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java index 4d53dc4..7b6a65e 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/CreateTaskResponse.java @@ -10,7 +10,8 @@ public record CreateTaskResponse( String title, String description, TaskStatus status, - String project, + String projectName, + LocalDateTime finishedAt, List history) { } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java index 4e526b8..f3ef7b8 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskController.java @@ -10,6 +10,8 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import io.quarkus.security.identity.SecurityIdentity; +import net.beetechgroup.beetask.usecase.task.start.StartTaskInput; import java.util.List; @@ -47,6 +49,9 @@ public class TaskController { @Inject UpdateTaskStatusUseCase updateTaskStatusUseCase; + @Inject + SecurityIdentity securityIdentity; + @POST @Operation(summary = "Create a new task", description = "Creates a new task in the system") @APIResponse(responseCode = "201", description = "Task created successfully") @@ -61,7 +66,8 @@ public class TaskController { @Operation(summary = "Start a task", description = "Starts a task in the system") @APIResponse(responseCode = "200", description = "Task started successfully") public CreateTaskResponse startTask(@PathParam("id") Long id) { - CreateTaskOutput output = startTaskUseCase.execute(TaskControllerMapper.toStartTaskInput(id)); + String email = securityIdentity.getPrincipal().getName(); + CreateTaskOutput output = startTaskUseCase.execute(new StartTaskInput(id, email)); return TaskControllerMapper.toCreateTaskResponse(output); } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java index e4096e2..9333bdb 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/task/TaskControllerMapper.java @@ -22,13 +22,14 @@ public class TaskControllerMapper { output.title(), output.description(), output.status(), - output.project(), + output.projectName(), + output.finishedAt(), output.history().stream().map(taskHistoryItemOutput -> new TaskHistoryItemResponse(taskHistoryItemOutput.startAt(), taskHistoryItemOutput.endAt())).toList() ); } - public static StartTaskInput toStartTaskInput(Long id) { - return new StartTaskInput(id); + public static StartTaskInput toStartTaskInput(Long id, String email) { + return new StartTaskInput(id, email); } public static StopTaskInput toStopTaskInput(Long id) { diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardInput.java new file mode 100644 index 0000000..a1f74ad --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardInput.java @@ -0,0 +1,9 @@ +package net.beetechgroup.beetask.usecase.dashboard; + +import java.time.LocalDateTime; + +public record DashboardInput( + String userEmail, + LocalDateTime startDate, + LocalDateTime endDate +) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardOutput.java new file mode 100644 index 0000000..95ad680 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardOutput.java @@ -0,0 +1,11 @@ +package net.beetechgroup.beetask.usecase.dashboard; + +import java.util.List; +import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput; + +public record DashboardOutput( + Long totalMinutesWorked, + List projectStats, + List yesterdayTasks, + List finishedTasksInPeriod +) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardProjectStats.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardProjectStats.java new file mode 100644 index 0000000..df1aae5 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardProjectStats.java @@ -0,0 +1,7 @@ +package net.beetechgroup.beetask.usecase.dashboard; + +public record DashboardProjectStats( + Long projectId, + String projectName, + Long totalMinutes +) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardUseCase.java new file mode 100644 index 0000000..78f47ba --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/dashboard/DashboardUseCase.java @@ -0,0 +1,93 @@ +package net.beetechgroup.beetask.usecase.dashboard; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import net.beetechgroup.beetask.entities.task.Task; +import net.beetechgroup.beetask.entities.task.TaskHistoryItem; +import net.beetechgroup.beetask.usecase.repository.TaskRepository; +import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper; + +public class DashboardUseCase { + + private final TaskRepository taskRepository; + + public DashboardUseCase(TaskRepository taskRepository) { + this.taskRepository = taskRepository; + } + + public DashboardOutput execute(DashboardInput input) { + // 1. Fetch tasks worked by user in period + List workedTasks = taskRepository.findTasksWorkedByUserInPeriod( + input.userEmail(), input.startDate(), input.endDate() + ); + + // 2. Fetch tasks finished by user in period + List finishedTasks = taskRepository.findTasksFinishedByUserInPeriod( + input.userEmail(), input.startDate(), input.endDate() + ); + + // 3. Yesterday's tasks + LocalDateTime startOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MIN); + LocalDateTime endOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MAX); + List yesterdayTasks = taskRepository.findTasksWorkedByUserInPeriod( + input.userEmail(), startOfYesterday, endOfYesterday + ); + + // Calculate total minutes and project stats + long totalMinutes = 0; + Map projectStatsMap = new HashMap<>(); + + for (Task task : workedTasks) { + long taskMinutesInPeriod = task.getHistory().stream() + .filter(h -> h.getUser() != null && h.getUser().getEmail().equals(input.userEmail())) + .filter(h -> isWithinPeriod(h, input.startDate(), input.endDate())) + .mapToLong(h -> calculateMinutesInRange(h, input.startDate(), input.endDate())) + .sum(); + + totalMinutes += taskMinutesInPeriod; + + if (task.getProject() != null) { + Long projectId = task.getProject().getId(); + DashboardProjectStats stats = projectStatsMap.getOrDefault(projectId, + new DashboardProjectStats(projectId, task.getProject().getName(), 0L)); + + projectStatsMap.put(projectId, new DashboardProjectStats( + projectId, + stats.projectName(), + stats.totalMinutes() + taskMinutesInPeriod + )); + } + } + + return new DashboardOutput( + totalMinutes, + new ArrayList<>(projectStatsMap.values()), + yesterdayTasks.stream().map(CreateTaskMapper::toCreateTaskOutput).toList(), + finishedTasks.stream().map(CreateTaskMapper::toCreateTaskOutput).toList() + ); + } + + private boolean isWithinPeriod(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) { + if (item.getStartAt() == null) return false; + // If it started after end or ended before start, it's out + if (item.getStartAt().isAfter(end)) return false; + if (item.getEndAt() != null && item.getEndAt().isBefore(start)) return false; + return true; + } + + private long calculateMinutesInRange(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) { + LocalDateTime effectiveStart = item.getStartAt().isBefore(start) ? start : item.getStartAt(); + LocalDateTime effectiveEnd = item.getEndAt() == null || item.getEndAt().isAfter(end) ? end : item.getEndAt(); + + if (effectiveStart.isAfter(effectiveEnd)) return 0; + + return Duration.between(effectiveStart, effectiveEnd).toMinutes(); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/UserNotFoundException.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/UserNotFoundException.java new file mode 100644 index 0000000..35b89fb --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/UserNotFoundException.java @@ -0,0 +1,7 @@ +package net.beetechgroup.beetask.usecase.exceptions; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String email) { + super("User not found with email: " + email); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java index 4dfebb2..5d10f72 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java @@ -1,6 +1,7 @@ package net.beetechgroup.beetask.usecase.repository; import java.util.List; +import java.time.LocalDateTime; import net.beetechgroup.beetask.entities.task.Task; @@ -8,4 +9,6 @@ public interface TaskRepository { Task saveTask(Task task); Task findTaskById(Long id); List findAllTasks(); + List findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end); + List findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end); } \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskMapper.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskMapper.java index 0101d25..c59eb45 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskMapper.java @@ -11,6 +11,7 @@ public class CreateTaskMapper { task.getDescription(), task.getStatus(), task.getProject() != null ? task.getProject().getName() : null, + task.getFinishedAt(), task.getHistory().stream().map(CreateTaskMapper::toTaskHistoryItemOutput).toList()); } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskOutput.java index d0a6fb0..ba3292a 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskOutput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskOutput.java @@ -1,7 +1,7 @@ package net.beetechgroup.beetask.usecase.task.create; import java.util.List; - +import java.time.LocalDateTime; import net.beetechgroup.beetask.entities.task.TaskStatus; public record CreateTaskOutput( @@ -9,7 +9,8 @@ public record CreateTaskOutput( String title, String description, TaskStatus status, - String project, + String projectName, + LocalDateTime finishedAt, List history) { } \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskInput.java index 71fbbe4..d3aa3f3 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskInput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskInput.java @@ -1,4 +1,3 @@ package net.beetechgroup.beetask.usecase.task.start; -public record StartTaskInput(Long id) { -} \ No newline at end of file +public record StartTaskInput(Long id, String userEmail) {} \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java index 3b038f7..28508cc 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java @@ -1,21 +1,28 @@ package net.beetechgroup.beetask.usecase.task.start; +import net.beetechgroup.beetask.entities.User; import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.usecase.repository.TaskRepository; +import net.beetechgroup.beetask.usecase.repository.UserRepository; import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper; import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput; +import net.beetechgroup.beetask.usecase.exceptions.UserNotFoundException; public class StartTaskUseCase { private final TaskRepository taskRepository; + private final UserRepository userRepository; - public StartTaskUseCase(TaskRepository taskRepository) { + public StartTaskUseCase(TaskRepository taskRepository, UserRepository userRepository) { this.taskRepository = taskRepository; + this.userRepository = userRepository; } public CreateTaskOutput execute(StartTaskInput input) { Task task = taskRepository.findTaskById(input.id()); - task.start(); + User user = userRepository.findByEmail(input.userEmail()) + .orElseThrow(() -> new UserNotFoundException(input.userEmail())); + task.start(user); return CreateTaskMapper.toCreateTaskOutput(taskRepository.saveTask(task)); } } diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 9e644fd..31a4907 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -11,6 +11,7 @@ import { ChevronDown, Shield, FolderKanban, + BarChart3, } from "lucide-react"; import { NavLink, Outlet, Navigate, Link } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; @@ -180,6 +181,11 @@ export function AppShell() {