ad23616c7b
- User, Project, TimeEntry JPA entities with SLF4J logging in services - Native SQL aggregate queries: per-day (weekly) and per-project (monthly) both using COALESCE(project_rate, user_rate) for cost calculation - ReportService parses ISO week/month strings, fills zero-day gaps, computes timeBudgetPct / costBudgetPct / breached per project - ProjectService piggybacks on aggregateAllByProject for budget status on GET /api/projects (no N+1) - GlobalExceptionHandler maps validation errors → 400 ProblemDetail, EntityNotFoundException → 404, IllegalArgumentException → 400 - Lombok on pom.xml (optional, used for @Getter/@Setter/@NoArgsConstructor) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
57 lines
1.9 KiB
Java
57 lines
1.9 KiB
Java
package com.example.app.project;
|
|
|
|
import com.example.app.project.ProjectDtos.CreateProjectRequest;
|
|
import com.example.app.project.ProjectDtos.ProjectResponse;
|
|
import com.example.app.time.TimeEntryRepository;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
@Service
|
|
public class ProjectService {
|
|
|
|
private final ProjectRepository projectRepository;
|
|
private final TimeEntryRepository timeEntryRepository;
|
|
|
|
public ProjectService(
|
|
ProjectRepository projectRepository, TimeEntryRepository timeEntryRepository) {
|
|
this.projectRepository = projectRepository;
|
|
this.timeEntryRepository = timeEntryRepository;
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
public List<ProjectResponse> findAll() {
|
|
List<Project> projects = projectRepository.findAll();
|
|
|
|
// one aggregate query for totals across all entries
|
|
Map<Long, long[]> totals = new HashMap<>();
|
|
for (Object[] row : timeEntryRepository.aggregateAllByProject()) {
|
|
long projectId = ((Number) row[0]).longValue();
|
|
long minutes = ((Number) row[1]).longValue();
|
|
long cost = ((Number) row[2]).longValue();
|
|
totals.put(projectId, new long[] {minutes, cost});
|
|
}
|
|
|
|
return projects.stream()
|
|
.map(
|
|
p -> {
|
|
long[] t = totals.getOrDefault(p.getId(), new long[] {0, 0});
|
|
return ProjectResponse.from(p, t[0], t[1]);
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
@Transactional
|
|
public ProjectResponse create(CreateProjectRequest req) {
|
|
Project p = new Project();
|
|
p.setName(req.name());
|
|
p.setTimeBudgetMinutes(req.timeBudgetMinutes());
|
|
p.setCostBudgetCents(req.costBudgetCents());
|
|
p.setHourlyRateCents(req.hourlyRateCents());
|
|
Project saved = projectRepository.save(p);
|
|
return ProjectResponse.from(saved, 0, 0);
|
|
}
|
|
}
|