From 20c69bcb7e36fe3afd5009428f4f06b15717206f Mon Sep 17 00:00:00 2001 From: Gabriel Santana de Menezes Date: Sat, 9 May 2026 20:52:54 -0400 Subject: [PATCH] fix: include projectless tasks in org dashboard and member detail stats Tasks with no associated project were being excluded from org dashboard calculations because the query filtered by project.organization.id. Now fetches tasks by active member emails, so all tasks are counted. Tasks and history items with no project are grouped under "Geral" in project-specific charts and calculations. Co-Authored-By: Claude Sonnet 4.6 --- .../repository/TaskRepositoryImpl.java | 20 ++++++++ .../orgdashboard/OrgDashboardUseCase.java | 46 ++++++++++--------- .../memberdetail/MemberDetailUseCase.java | 13 +++--- .../usecase/repository/TaskRepository.java | 2 + frontend/src/components/MemberDetailModal.tsx | 2 +- frontend/src/pages/OrgDashboardPage.tsx | 2 +- frontend/src/services/orgDashboardService.ts | 4 +- 7 files changed, 57 insertions(+), 32 deletions(-) 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 57dab7c..bcaf024 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 @@ -97,4 +97,24 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository findTasksWorkedByUserEmailsInPeriod(List emails, LocalDateTime start, LocalDateTime end) { + if (emails.isEmpty()) return List.of(); + return find("user.email in ?1", emails).list().stream() + .filter(entity -> entity.getHistory().stream().anyMatch(h -> + Objects.nonNull(h.getStartAt()) && + !h.getStartAt().isAfter(end) && + (Objects.isNull(h.getEndAt()) || !h.getEndAt().isBefore(start)))) + .map(TaskEntityMapper::toDomain) + .toList(); + } + + @Override + public List findTasksFinishedByUserEmailsInPeriod(List emails, LocalDateTime start, LocalDateTime end) { + if (emails.isEmpty()) return List.of(); + return find("user.email in ?1 and status = ?2 and finishedAt >= ?3 and finishedAt <= ?4", + emails, TaskStatus.COMPLETED, start, end) + .list().stream().map(TaskEntityMapper::toDomain).toList(); + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/OrgDashboardUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/OrgDashboardUseCase.java index ed361ec..5f780e1 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/OrgDashboardUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/OrgDashboardUseCase.java @@ -7,6 +7,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import net.beetechgroup.beetask.entities.organization.UserOrganization; import net.beetechgroup.beetask.entities.organization.UserOrganizationStatus; @@ -33,11 +34,19 @@ public class OrgDashboardUseCase { public OrgDashboardOutput execute(OrgDashboardInput input) { authorizer.execute(input.userEmail(), input.organizationId()); - List workedTasks = taskRepository.findTasksWorkedByOrgInPeriod( - input.organizationId(), input.startDate(), input.endDate() + List members = userOrganizationRepository.findByOrganizationIdAndStatus( + input.organizationId(), UserOrganizationStatus.ACTIVE ); - List finishedTasks = taskRepository.findTasksFinishedByOrgInPeriod( - input.organizationId(), input.startDate(), input.endDate() + + List memberEmails = members.stream() + .map(uo -> uo.getUser().getEmail()) + .toList(); + + List workedTasks = taskRepository.findTasksWorkedByUserEmailsInPeriod( + memberEmails, input.startDate(), input.endDate() + ); + List finishedTasks = taskRepository.findTasksFinishedByUserEmailsInPeriod( + memberEmails, input.startDate(), input.endDate() ); Map projectStatsMap = new HashMap<>(); @@ -46,15 +55,14 @@ public class OrgDashboardUseCase { for (Task task : workedTasks) { long minutes = calculateMinutesInPeriod(task, input.startDate(), input.endDate()); - if (task.getProject() != null) { - Long projectId = task.getProject().getId(); - OrgProjectStats existing = projectStatsMap.getOrDefault(projectId, - new OrgProjectStats(projectId, task.getProject().getName(), 0L)); - projectStatsMap.put(projectId, new OrgProjectStats( - projectId, existing.projectName(), existing.totalMinutes() + minutes)); - } + Long projectId = Objects.nonNull(task.getProject()) ? task.getProject().getId() : null; + String projectName = Objects.nonNull(task.getProject()) ? task.getProject().getName() : "Geral"; + OrgProjectStats existing = projectStatsMap.getOrDefault(projectId, + new OrgProjectStats(projectId, projectName, 0L)); + projectStatsMap.put(projectId, new OrgProjectStats( + projectId, existing.projectName(), existing.totalMinutes() + minutes)); - if (task.getUser() != null) { + if (Objects.nonNull(task.getUser())) { memberMinutesMap.merge(task.getUser().getEmail(), minutes, Long::sum); } } @@ -62,7 +70,7 @@ public class OrgDashboardUseCase { List topTasks = workedTasks.stream() .map(t -> { long minutes = calculateMinutesInPeriod(t, input.startDate(), input.endDate()); - String projectName = t.getProject() != null ? t.getProject().getName() : "-"; + String projectName = Objects.nonNull(t.getProject()) ? t.getProject().getName() : "Geral"; return new OrgTopTask(t.getId(), t.getTitle(), projectName, minutes); }) .sorted(Comparator.comparingLong(OrgTopTask::totalMinutes).reversed()) @@ -71,15 +79,11 @@ public class OrgDashboardUseCase { Map finishedByMember = new HashMap<>(); for (Task task : finishedTasks) { - if (task.getUser() != null) { + if (Objects.nonNull(task.getUser())) { finishedByMember.merge(task.getUser().getEmail(), 1L, Long::sum); } } - List members = userOrganizationRepository.findByOrganizationIdAndStatus( - input.organizationId(), UserOrganizationStatus.ACTIVE - ); - List memberStats = members.stream() .map(uo -> { String email = uo.getUser().getEmail(); @@ -109,15 +113,15 @@ public class OrgDashboardUseCase { } private boolean isWithinPeriod(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) { - if (item.getStartAt() == null) return false; + if (Objects.isNull(item.getStartAt())) return false; if (item.getStartAt().isAfter(end)) return false; - if (item.getEndAt() != null && item.getEndAt().isBefore(start)) return false; + if (Objects.nonNull(item.getEndAt()) && 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(); + LocalDateTime effectiveEnd = Objects.isNull(item.getEndAt()) || 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/orgdashboard/memberdetail/MemberDetailUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailUseCase.java index e1ea938..55a75c5 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailUseCase.java @@ -67,13 +67,12 @@ public class MemberDetailUseCase { minutesPerMonth.merge(YearMonth.from(effectiveStart), minutes, Long::sum); } - if (Objects.nonNull(task.getProject())) { - Long projectId = task.getProject().getId(); - MemberDetailOutput.ProjectStats existing = projectStatsMap.getOrDefault(projectId, - new MemberDetailOutput.ProjectStats(projectId, task.getProject().getName(), 0L)); - projectStatsMap.put(projectId, new MemberDetailOutput.ProjectStats( - projectId, existing.projectName(), existing.totalMinutes() + minutes)); - } + Long projectId = Objects.nonNull(task.getProject()) ? task.getProject().getId() : null; + String projectName = Objects.nonNull(task.getProject()) ? task.getProject().getName() : "Geral"; + MemberDetailOutput.ProjectStats existing = projectStatsMap.getOrDefault(projectId, + new MemberDetailOutput.ProjectStats(projectId, projectName, 0L)); + projectStatsMap.put(projectId, new MemberDetailOutput.ProjectStats( + projectId, existing.projectName(), existing.totalMinutes() + minutes)); } } 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 d9a20c2..90c58b6 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 @@ -16,4 +16,6 @@ public interface TaskRepository { List findTasksFinishedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end); List findTasksWorkedByUserIdInPeriod(Long userId, LocalDateTime start, LocalDateTime end); List findTasksFinishedByUserIdInPeriod(Long userId, LocalDateTime start, LocalDateTime end); + List findTasksWorkedByUserEmailsInPeriod(List emails, LocalDateTime start, LocalDateTime end); + List findTasksFinishedByUserEmailsInPeriod(List emails, LocalDateTime start, LocalDateTime end); } \ No newline at end of file diff --git a/frontend/src/components/MemberDetailModal.tsx b/frontend/src/components/MemberDetailModal.tsx index fdb1af9..0275030 100644 --- a/frontend/src/components/MemberDetailModal.tsx +++ b/frontend/src/components/MemberDetailModal.tsx @@ -294,7 +294,7 @@ export const MemberDetailModal: React.FC = ({ const total = data.projectStats.reduce((s, x) => s + x.totalMinutes, 0); const pct = total > 0 ? ((p.totalMinutes / total) * 100).toFixed(0) : "0"; return ( -
+
{p.projectName} diff --git a/frontend/src/pages/OrgDashboardPage.tsx b/frontend/src/pages/OrgDashboardPage.tsx index 5d69f6f..ada1d28 100644 --- a/frontend/src/pages/OrgDashboardPage.tsx +++ b/frontend/src/pages/OrgDashboardPage.tsx @@ -246,7 +246,7 @@ export const OrgDashboardPage: React.FC = () => { .map((stat, i) => { const pct = totalMinutes > 0 ? ((stat.totalMinutes / totalMinutes) * 100).toFixed(1) : "0"; return ( -
+