Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@

public record MilestoneResponse (
UUID id,

String name,

String description,

LocalDate date
) {
public static MilestoneResponse from(ProjectMilestone milestone) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package pl.edu.agh.project_manager.controller.dto.project;

import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment;
import pl.edu.agh.project_manager.domain.entity.user.User;

import java.util.List;
import java.util.UUID;

public record AssignmentsByEmployeeResponse(
UUID userId,
String name,
String surname,
String email,
List<ProjectAssignmentResponse> assignments
) {
public static AssignmentsByEmployeeResponse from(User user, List<ProjectAssignment> userAssignments) {
return new AssignmentsByEmployeeResponse(
user.getId(),
user.getName(),
user.getSurname(),
user.getEmail(),
userAssignments.stream()
.map(ProjectAssignmentResponse::from)
.toList()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pl.edu.agh.project_manager.controller.dto.project;

import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment;

import java.time.LocalDate;
import java.util.UUID;

public record ProjectAssignmentResponse(
UUID id,
LocalDate startDate,
LocalDate endDate,
AssignmentStatusResponse status,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Co prawda to nie powstało w tym PR chyba, ale w sumie po co to AssignmentStatusResponse. AssignmentStatus to tylko enum i stworzenie response nie daje nam tu żadnych korzyści, a nawet przeszkadza, bo wymaga dodatkowego mapowania, zwróciłbym po prostu AssignmentStatus.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tak, to nie powstało w ramach tego taska, skorzystałem z istniejącej struktury. Jestem zdania, że zawsze powinno się rozgraniczać te warstwy logiki, tzn AssignmentStatus mimo, że wygląda identycznie to on dotyczy encji bazodanowych. Więc tak samo jak nie korzystamy z encji, to dla enuma również robimy osobne mapowanie, a szczególnie jest to zauważalne w momencie kiedy enum bazodanoway zaczyna rozjeżdżać się z tym co chcemy przekazać na frontend, np. zobacz różnicę między UserRole oraz AdminAssignableRole to drugie nie ma w sobie admina.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tak ale AdminAssignableRole to enum, który do czegoś służy dodatkowo, czyli w tym wypadku nie chcemy zwracać Administrator, a tamto response to po prostu zduplikowany enum. Według mnie trochę przerost formy nad treścią, ale okej można zostawić.

String roleName,
int utilizationPercentage
) {
public static ProjectAssignmentResponse from(ProjectAssignment assignment) {
return new ProjectAssignmentResponse(
assignment.getId(),
assignment.getStartDate(),
assignment.getEndDate(),
AssignmentStatusResponse.from(assignment.getStatus()),
assignment.getRoleName(),
assignment.getUtilizationPercentage()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package pl.edu.agh.project_manager.controller.dto.project;

import pl.edu.agh.project_manager.controller.dto.milestone.MilestoneResponse;

import java.util.List;

public record ProjectTimelineResponse(
List<MilestoneResponse> milestones,
List<AssignmentsByEmployeeResponse> assignments
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ public ResponseEntity<ProjectMembersResponse> getProjectMembers(
ProjectMembersResponse project = projectService.getProjectMembers(projectId);
return ResponseEntity.ok(project);
}

@GetMapping("/{projectId}/timeline")
@PreAuthorize("hasAnyRole('ADMINISTRATOR', 'AUTHORITY') or @projectAccess.canAccessProject(#projectId, authentication.principal)")
public ResponseEntity<ProjectTimelineResponse> getTimelineData(
@PathVariable UUID projectId
) {
ProjectTimelineResponse response = projectService.getTimelineData(projectId);
return ResponseEntity.ok(response);
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,11 @@ public interface ProjectAssignmentRepository extends JpaRepository<ProjectAssign
"JOIN pa.user u " +
"WHERE pa.id = :assignmentId AND u.supervisor.id = :managerId")
boolean isManagerForAssignment(@Param("assignmentId") UUID assignmentId, @Param("managerId") UUID managerId);

@Query("SELECT pa FROM ProjectAssignment pa " +
"JOIN FETCH pa.user u " +
"WHERE pa.project.id = :projectId " +
"AND pa.status IN ('PENDING', 'ACCEPTED') " +
"ORDER BY pa.createdAt ASC")
List<ProjectAssignment> findActiveAndPendingByProjectIdOrderByCreatedAtAsc(@Param("projectId") UUID projectId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,7 @@ public interface ProjectRepository extends JpaRepository<Project, UUID> {

@EntityGraph(attributePaths = {"projectManager", "projectGroup"})
List<Project> findAll(Specification<Project> spec);

@EntityGraph(attributePaths = {"milestones"})
Optional<Project> findWithMilestonesById(UUID id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pl.edu.agh.project_manager.controller.dto.project.AssignmentResponse;
import pl.edu.agh.project_manager.controller.dto.project.AssignmentsByEmployeeResponse;
import pl.edu.agh.project_manager.controller.dto.project.ProjectAssignmentResponse;
import pl.edu.agh.project_manager.domain.entity.project.Project;
import pl.edu.agh.project_manager.domain.entity.user.User;
import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment;
Expand All @@ -15,12 +17,15 @@
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
import pl.edu.agh.project_manager.domain.exception.ApplicationException;
import pl.edu.agh.project_manager.repository.project.ProjectAssignmentRepository;
import pl.edu.agh.project_manager.repository.project.ProjectRepository;
import pl.edu.agh.project_manager.service.user.UserService;
import pl.edu.agh.project_manager.service.command.project.AssignmentCommand;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
package pl.edu.agh.project_manager.service.project;

import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Predicate;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import pl.edu.agh.project_manager.controller.dto.project.ProjectMembersResponse;
import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse;
import org.springframework.transaction.annotation.Transactional;
import pl.edu.agh.project_manager.controller.dto.milestone.MilestoneResponse;
import pl.edu.agh.project_manager.controller.dto.project.*;
import pl.edu.agh.project_manager.domain.entity.project.Project;
import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment;
import pl.edu.agh.project_manager.domain.entity.project.ProjectRisk;
import pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup;
import pl.edu.agh.project_manager.domain.entity.user.User;
import pl.edu.agh.project_manager.domain.entity.project.ProjectMilestone;
import pl.edu.agh.project_manager.domain.enums.GroupType;
import pl.edu.agh.project_manager.domain.exception.ApiErrorCode;
import pl.edu.agh.project_manager.domain.exception.ApplicationException;
import pl.edu.agh.project_manager.repository.project.ProjectAssignmentRepository;
import pl.edu.agh.project_manager.repository.project.ProjectRepository;
import pl.edu.agh.project_manager.security.UserPrincipal;
import pl.edu.agh.project_manager.service.command.project.MilestoneCommand;
import pl.edu.agh.project_manager.service.command.project.ProjectCreationCommand;
import pl.edu.agh.project_manager.service.command.project.RiskCommand;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import pl.edu.agh.project_manager.domain.enums.UserRole;
import java.util.stream.Collectors;

import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand;
import pl.edu.agh.project_manager.service.projectgroup.ProjectGroupsService;
import pl.edu.agh.project_manager.service.user.UserService;
Expand All @@ -37,6 +37,7 @@ public class ProjectService {
private final UserService userService;
private final ProjectGroupsService projectGroupService;
private final ProjectRepository projectRepository;
private final ProjectAssignmentRepository assignmentRepository;


@Transactional
Expand Down Expand Up @@ -83,6 +84,44 @@ public ProjectMembersResponse getProjectMembers(UUID projectId) {
return ProjectMembersResponse.from(project);
}

@Transactional(readOnly = true)
public ProjectTimelineResponse getTimelineData(UUID projectId) {
Project project = projectRepository.findWithMilestonesById(projectId)
.orElseThrow(() -> new ApplicationException(
ApiErrorCode.PROJECT_NOT_FOUND,
"Cannot find provided project - " + projectId
));

List<ProjectAssignment> assignments = assignmentRepository.findActiveAndPendingByProjectIdOrderByCreatedAtAsc(projectId);

return new ProjectTimelineResponse(
project.getMilestones().stream().map(MilestoneResponse::from).toList(),
groupAssignmentsByEmployee(assignments)
);
}

private List<AssignmentsByEmployeeResponse> groupAssignmentsByEmployee(List<ProjectAssignment> assignments) {
Map<User, List<ProjectAssignment>> assignmentsByUser = assignments.stream()
.collect(Collectors.groupingBy(ProjectAssignment::getUser, LinkedHashMap::new, Collectors.toList()));

return assignmentsByUser.entrySet().stream()
.map(entry -> {
User employee = entry.getKey();
List<ProjectAssignment> userAssignments = entry.getValue();

return new AssignmentsByEmployeeResponse(
employee.getId(),
employee.getName(),
employee.getSurname(),
employee.getEmail(),
userAssignments.stream()
.map(ProjectAssignmentResponse::from)
.toList()
);
})
.toList();
}

private Project buildProject(ProjectCreationCommand command, User projectManager) {
return Project.builder()
.title(command.title())
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const ENDPOINTS = {
BASE: `/projects/${id}`,
RISKS: `/projects/${id}/risks`,
MEMBERS: `/projects/${id}/members`,
ASSIGNMENTS: `/projects/${id}/assignments`
ASSIGNMENTS: `/projects/${id}/assignments`,
TIMELINE: `/projects/${id}/timeline`
}),
},
PROJECT_GROUP: {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function TooltipTrigger({

function TooltipContent({
className,
sideOffset = 0,
sideOffset = 8,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
Expand All @@ -46,7 +46,7 @@ function TooltipContent({
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
{/* <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" /> */}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ export const CreateAssignmentModal = ({ project }: CreateAssignmentModalProps) =
<Form {...form}>
<Dialog open={isOpenModal} onOpenChange={open => open ? setIsOpenModal(true) : closeModal()}>
<DialogTrigger asChild>
<Button variant="outline">Stwórz wniosek</Button>
<Button
variant="outline"
className="border-slate-900 bg-slate-900 text-white hover:bg-slate-800 hover:text-white shadow-sm"
>
Stwórz wniosek
</Button>
</DialogTrigger>

<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto" data-project-id={project.id}>
Expand Down
Loading
Loading