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)
|
userId, TaskStatus.COMPLETED, start, end)
|
||||||
.list().stream().map(TaskEntityMapper::toDomain).toList();
|
.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.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import net.beetechgroup.beetask.entities.organization.UserOrganization;
|
import net.beetechgroup.beetask.entities.organization.UserOrganization;
|
||||||
import net.beetechgroup.beetask.entities.organization.UserOrganizationStatus;
|
import net.beetechgroup.beetask.entities.organization.UserOrganizationStatus;
|
||||||
|
|
@ -33,11 +34,19 @@ public class OrgDashboardUseCase {
|
||||||
public OrgDashboardOutput execute(OrgDashboardInput input) {
|
public OrgDashboardOutput execute(OrgDashboardInput input) {
|
||||||
authorizer.execute(input.userEmail(), input.organizationId());
|
authorizer.execute(input.userEmail(), input.organizationId());
|
||||||
|
|
||||||
List<Task> workedTasks = taskRepository.findTasksWorkedByOrgInPeriod(
|
List<UserOrganization> members = userOrganizationRepository.findByOrganizationIdAndStatus(
|
||||||
input.organizationId(), input.startDate(), input.endDate()
|
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<>();
|
Map<Long, OrgProjectStats> projectStatsMap = new HashMap<>();
|
||||||
|
|
@ -46,15 +55,14 @@ public class OrgDashboardUseCase {
|
||||||
for (Task task : workedTasks) {
|
for (Task task : workedTasks) {
|
||||||
long minutes = calculateMinutesInPeriod(task, input.startDate(), input.endDate());
|
long minutes = calculateMinutesInPeriod(task, input.startDate(), input.endDate());
|
||||||
|
|
||||||
if (task.getProject() != null) {
|
Long projectId = Objects.nonNull(task.getProject()) ? task.getProject().getId() : null;
|
||||||
Long projectId = task.getProject().getId();
|
String projectName = Objects.nonNull(task.getProject()) ? task.getProject().getName() : "Geral";
|
||||||
OrgProjectStats existing = projectStatsMap.getOrDefault(projectId,
|
OrgProjectStats existing = projectStatsMap.getOrDefault(projectId,
|
||||||
new OrgProjectStats(projectId, task.getProject().getName(), 0L));
|
new OrgProjectStats(projectId, projectName, 0L));
|
||||||
projectStatsMap.put(projectId, new OrgProjectStats(
|
projectStatsMap.put(projectId, new OrgProjectStats(
|
||||||
projectId, existing.projectName(), existing.totalMinutes() + minutes));
|
projectId, existing.projectName(), existing.totalMinutes() + minutes));
|
||||||
}
|
|
||||||
|
|
||||||
if (task.getUser() != null) {
|
if (Objects.nonNull(task.getUser())) {
|
||||||
memberMinutesMap.merge(task.getUser().getEmail(), minutes, Long::sum);
|
memberMinutesMap.merge(task.getUser().getEmail(), minutes, Long::sum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +70,7 @@ public class OrgDashboardUseCase {
|
||||||
List<OrgTopTask> topTasks = workedTasks.stream()
|
List<OrgTopTask> topTasks = workedTasks.stream()
|
||||||
.map(t -> {
|
.map(t -> {
|
||||||
long minutes = calculateMinutesInPeriod(t, input.startDate(), input.endDate());
|
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);
|
return new OrgTopTask(t.getId(), t.getTitle(), projectName, minutes);
|
||||||
})
|
})
|
||||||
.sorted(Comparator.comparingLong(OrgTopTask::totalMinutes).reversed())
|
.sorted(Comparator.comparingLong(OrgTopTask::totalMinutes).reversed())
|
||||||
|
|
@ -71,15 +79,11 @@ public class OrgDashboardUseCase {
|
||||||
|
|
||||||
Map<String, Long> finishedByMember = new HashMap<>();
|
Map<String, Long> finishedByMember = new HashMap<>();
|
||||||
for (Task task : finishedTasks) {
|
for (Task task : finishedTasks) {
|
||||||
if (task.getUser() != null) {
|
if (Objects.nonNull(task.getUser())) {
|
||||||
finishedByMember.merge(task.getUser().getEmail(), 1L, Long::sum);
|
finishedByMember.merge(task.getUser().getEmail(), 1L, Long::sum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<UserOrganization> members = userOrganizationRepository.findByOrganizationIdAndStatus(
|
|
||||||
input.organizationId(), UserOrganizationStatus.ACTIVE
|
|
||||||
);
|
|
||||||
|
|
||||||
List<OrgMemberStats> memberStats = members.stream()
|
List<OrgMemberStats> memberStats = members.stream()
|
||||||
.map(uo -> {
|
.map(uo -> {
|
||||||
String email = uo.getUser().getEmail();
|
String email = uo.getUser().getEmail();
|
||||||
|
|
@ -109,15 +113,15 @@ public class OrgDashboardUseCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWithinPeriod(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) {
|
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.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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long calculateMinutesInRange(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) {
|
private long calculateMinutesInRange(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) {
|
||||||
LocalDateTime effectiveStart = item.getStartAt().isBefore(start) ? start : item.getStartAt();
|
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;
|
if (effectiveStart.isAfter(effectiveEnd)) return 0;
|
||||||
return Duration.between(effectiveStart, effectiveEnd).toMinutes();
|
return Duration.between(effectiveStart, effectiveEnd).toMinutes();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,15 +67,14 @@ public class MemberDetailUseCase {
|
||||||
minutesPerMonth.merge(YearMonth.from(effectiveStart), minutes, Long::sum);
|
minutesPerMonth.merge(YearMonth.from(effectiveStart), minutes, Long::sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Objects.nonNull(task.getProject())) {
|
Long projectId = Objects.nonNull(task.getProject()) ? task.getProject().getId() : null;
|
||||||
Long projectId = task.getProject().getId();
|
String projectName = Objects.nonNull(task.getProject()) ? task.getProject().getName() : "Geral";
|
||||||
MemberDetailOutput.ProjectStats existing = projectStatsMap.getOrDefault(projectId,
|
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(
|
projectStatsMap.put(projectId, new MemberDetailOutput.ProjectStats(
|
||||||
projectId, existing.projectName(), existing.totalMinutes() + minutes));
|
projectId, existing.projectName(), existing.totalMinutes() + minutes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
List<MemberDetailOutput.PeriodStats> periodStats = new ArrayList<>();
|
List<MemberDetailOutput.PeriodStats> periodStats = new ArrayList<>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,6 @@ public interface TaskRepository {
|
||||||
List<Task> findTasksFinishedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end);
|
List<Task> findTasksFinishedByOrgInPeriod(Long orgId, LocalDateTime start, LocalDateTime end);
|
||||||
List<Task> findTasksWorkedByUserIdInPeriod(Long userId, LocalDateTime start, LocalDateTime end);
|
List<Task> findTasksWorkedByUserIdInPeriod(Long userId, LocalDateTime start, LocalDateTime end);
|
||||||
List<Task> findTasksFinishedByUserIdInPeriod(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 total = data.projectStats.reduce((s, x) => s + x.totalMinutes, 0);
|
||||||
const pct = total > 0 ? ((p.totalMinutes / total) * 100).toFixed(0) : "0";
|
const pct = total > 0 ? ((p.totalMinutes / total) * 100).toFixed(0) : "0";
|
||||||
return (
|
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] }} />
|
<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-xs font-bold text-text-main truncate flex-1">{p.projectName}</span>
|
||||||
<span className="text-[10px] font-black text-text-muted shrink-0">
|
<span className="text-[10px] font-black text-text-muted shrink-0">
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ export const OrgDashboardPage: React.FC = () => {
|
||||||
.map((stat, i) => {
|
.map((stat, i) => {
|
||||||
const pct = totalMinutes > 0 ? ((stat.totalMinutes / totalMinutes) * 100).toFixed(1) : "0";
|
const pct = totalMinutes > 0 ? ((stat.totalMinutes / totalMinutes) * 100).toFixed(1) : "0";
|
||||||
return (
|
return (
|
||||||
<div key={stat.projectId} className="flex items-center gap-3">
|
<div key={stat.projectId ?? "geral"} className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full shrink-0"
|
className="w-3 h-3 rounded-full shrink-0"
|
||||||
style={{ background: CHART_COLORS[i % CHART_COLORS.length] }}
|
style={{ background: CHART_COLORS[i % CHART_COLORS.length] }}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { apiFetch } from '../lib/api'
|
import { apiFetch } from '../lib/api'
|
||||||
|
|
||||||
export interface OrgProjectStats {
|
export interface OrgProjectStats {
|
||||||
projectId: number
|
projectId: number | null
|
||||||
projectName: string
|
projectName: string
|
||||||
totalMinutes: number
|
totalMinutes: number
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ export interface PeriodStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemberProjectStats {
|
export interface MemberProjectStats {
|
||||||
projectId: number
|
projectId: number | null
|
||||||
projectName: string
|
projectName: string
|
||||||
totalMinutes: number
|
totalMinutes: number
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue