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 <noreply@anthropic.com>
This commit is contained in:
parent
824d33f283
commit
20c69bcb7e
7 changed files with 57 additions and 32 deletions
|
|
@ -97,4 +97,24 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository<Tas
|
|||
userId, TaskStatus.COMPLETED, start, end)
|
||||
.list().stream().map(TaskEntityMapper::toDomain).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> findTasksWorkedByUserEmailsInPeriod(List<String> 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<Task> findTasksFinishedByUserEmailsInPeriod(List<String> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Task> workedTasks = taskRepository.findTasksWorkedByOrgInPeriod(
|
||||
input.organizationId(), input.startDate(), input.endDate()
|
||||
List<UserOrganization> members = userOrganizationRepository.findByOrganizationIdAndStatus(
|
||||
input.organizationId(), UserOrganizationStatus.ACTIVE
|
||||
);
|
||||
List<Task> finishedTasks = taskRepository.findTasksFinishedByOrgInPeriod(
|
||||
input.organizationId(), input.startDate(), input.endDate()
|
||||
|
||||
List<String> memberEmails = members.stream()
|
||||
.map(uo -> uo.getUser().getEmail())
|
||||
.toList();
|
||||
|
||||
List<Task> workedTasks = taskRepository.findTasksWorkedByUserEmailsInPeriod(
|
||||
memberEmails, input.startDate(), input.endDate()
|
||||
);
|
||||
List<Task> finishedTasks = taskRepository.findTasksFinishedByUserEmailsInPeriod(
|
||||
memberEmails, input.startDate(), input.endDate()
|
||||
);
|
||||
|
||||
Map<Long, OrgProjectStats> 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();
|
||||
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, task.getProject().getName(), 0L));
|
||||
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<OrgTopTask> 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<String, Long> 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<UserOrganization> members = userOrganizationRepository.findByOrganizationIdAndStatus(
|
||||
input.organizationId(), UserOrganizationStatus.ACTIVE
|
||||
);
|
||||
|
||||
List<OrgMemberStats> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,15 +67,14 @@ public class MemberDetailUseCase {
|
|||
minutesPerMonth.merge(YearMonth.from(effectiveStart), minutes, Long::sum);
|
||||
}
|
||||
|
||||
if (Objects.nonNull(task.getProject())) {
|
||||
Long projectId = task.getProject().getId();
|
||||
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, task.getProject().getName(), 0L));
|
||||
new MemberDetailOutput.ProjectStats(projectId, projectName, 0L));
|
||||
projectStatsMap.put(projectId, new MemberDetailOutput.ProjectStats(
|
||||
projectId, existing.projectName(), existing.totalMinutes() + minutes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<MemberDetailOutput.PeriodStats> periodStats = new ArrayList<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -16,4 +16,6 @@ public interface TaskRepository {
|
|||
List<Task> findTasksFinishedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end);
|
||||
List<Task> findTasksWorkedByUserIdInPeriod(Long userId, LocalDateTime start, LocalDateTime end);
|
||||
List<Task> findTasksFinishedByUserIdInPeriod(Long userId, LocalDateTime start, LocalDateTime end);
|
||||
List<Task> findTasksWorkedByUserEmailsInPeriod(List<String> emails, LocalDateTime start, LocalDateTime end);
|
||||
List<Task> findTasksFinishedByUserEmailsInPeriod(List<String> emails, LocalDateTime start, LocalDateTime end);
|
||||
}
|
||||
|
|
@ -294,7 +294,7 @@ export const MemberDetailModal: React.FC<MemberDetailModalProps> = ({
|
|||
const total = data.projectStats.reduce((s, x) => s + x.totalMinutes, 0);
|
||||
const pct = total > 0 ? ((p.totalMinutes / total) * 100).toFixed(0) : "0";
|
||||
return (
|
||||
<div key={p.projectId} className="flex items-center gap-2">
|
||||
<div key={p.projectId ?? "geral"} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full shrink-0" style={{ background: CHART_COLORS[i % CHART_COLORS.length] }} />
|
||||
<span className="text-xs font-bold text-text-main truncate flex-1">{p.projectName}</span>
|
||||
<span className="text-[10px] font-black text-text-muted shrink-0">
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ export const OrgDashboardPage: React.FC = () => {
|
|||
.map((stat, i) => {
|
||||
const pct = totalMinutes > 0 ? ((stat.totalMinutes / totalMinutes) * 100).toFixed(1) : "0";
|
||||
return (
|
||||
<div key={stat.projectId} className="flex items-center gap-3">
|
||||
<div key={stat.projectId ?? "geral"} className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full shrink-0"
|
||||
style={{ background: CHART_COLORS[i % CHART_COLORS.length] }}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { apiFetch } from '../lib/api'
|
||||
|
||||
export interface OrgProjectStats {
|
||||
projectId: number
|
||||
projectId: number | null
|
||||
projectName: string
|
||||
totalMinutes: number
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ export interface PeriodStats {
|
|||
}
|
||||
|
||||
export interface MemberProjectStats {
|
||||
projectId: number
|
||||
projectId: number | null
|
||||
projectName: string
|
||||
totalMinutes: number
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue