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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package pl.edu.agh.project_manager.controller.dto.project;

import pl.edu.agh.project_manager.controller.dto.project_group.GroupBasicResponse;
import pl.edu.agh.project_manager.domain.entity.project.Project;
import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment;

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

public record UserProjectMembershipResponse(
UUID id,
String title,
String description,
LocalDate startDate,
LocalDate endDate,
Boolean isActive,
GroupBasicResponse group,
List<UserProjectRoleResponse> roles
) {
public static UserProjectMembershipResponse from(Project project, List<ProjectAssignment> userAssignments) {
return new UserProjectMembershipResponse(
project.getId(),
project.getTitle(),
project.getDescription(),
project.getStartDate(),
project.getEndDate(),
project.getIsActive(),
GroupBasicResponse.from(project.getProjectGroup()),
userAssignments.stream().map(UserProjectRoleResponse::from).toList()
);
}

public record UserProjectRoleResponse(
String roleName,
LocalDate startDate,
LocalDate endDate,
Integer utilizationPercentage
) {
public static UserProjectRoleResponse from(ProjectAssignment assignment) {
return new UserProjectRoleResponse(
assignment.getRoleName(),
assignment.getStartDate(),
assignment.getEndDate(),
assignment.getUtilizationPercentage()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package pl.edu.agh.project_manager.controller.dto.project_group;

import pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup;
import pl.edu.agh.project_manager.domain.enums.GroupType;

import java.util.UUID;

public record OwnedGroupResponse(
UUID id,
String name,
String description,
GroupType groupType,
int projectCount,
int activeProjectCount,
boolean isOwner
) {
public static OwnedGroupResponse from(ProjectGroup group, boolean isOwner) {
int total = group.getProjects() != null ? group.getProjects().size() : 0;
int active = group.getProjects() != null
? (int) group.getProjects().stream().filter(p -> Boolean.TRUE.equals(p.getIsActive())).count()
: 0;
Comment on lines +17 to +21
return new OwnedGroupResponse(
group.getId(),
group.getName(),
group.getDescription(),
group.getGroupType(),
total,
active,
isOwner
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public record UserResponse(
UserRole role,
UserStatus status,
String supervisorEmail,
SimpleUserResponse supervisor,
List<QualificationResponse> qualifications
) {
public static UserResponse from(User user) {
Expand All @@ -27,6 +28,7 @@ public static UserResponse from(User user) {
user.getUserRole(),
user.getUserStatus(),
user.getSupervisor() != null ? user.getSupervisor().getEmail() : null,
user.getSupervisor() != null ? SimpleUserResponse.fromUser(user.getSupervisor()) : null,

user.getQualifications() != null ?
user.getQualifications().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import pl.edu.agh.project_manager.controller.dto.auth.ChangePasswordRequest;
import pl.edu.agh.project_manager.controller.dto.user.UserResponse;
import pl.edu.agh.project_manager.security.UserPrincipal;
import pl.edu.agh.project_manager.service.user.UserService;

Expand All @@ -17,6 +18,12 @@ public class ProfileController {

private final UserService userService;

@GetMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<UserResponse> getMyProfile(@AuthenticationPrincipal UserPrincipal principal) {
return ResponseEntity.ok(userService.getUser(principal.userId()));
}

@PostMapping("/password")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<Void> changePassword(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
import pl.edu.agh.project_manager.controller.dto.invitation.AdminUserInvitationRequest;
import pl.edu.agh.project_manager.controller.dto.invitation.ResendInvitationRequest;
import pl.edu.agh.project_manager.controller.dto.project.ProjectAssignmentUserWorkloadResponse;
import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse;
import pl.edu.agh.project_manager.controller.dto.project.UserProjectMembershipResponse;
import pl.edu.agh.project_manager.controller.dto.project_group.OwnedGroupResponse;
import pl.edu.agh.project_manager.controller.dto.qualification.QualificationResponse;
import pl.edu.agh.project_manager.controller.dto.user.SimpleUserResponse;
import pl.edu.agh.project_manager.controller.dto.user.UserResponse;
import pl.edu.agh.project_manager.domain.enums.UserRole;
import pl.edu.agh.project_manager.domain.enums.UserStatus;
import pl.edu.agh.project_manager.service.approval.AssignmentManagementService;
import pl.edu.agh.project_manager.service.user.QualificationService;
import pl.edu.agh.project_manager.service.user.UserInvitationService;
import pl.edu.agh.project_manager.service.user.UserService;
import pl.edu.agh.project_manager.service.command.invitation.AdminInviteUserCommand;
Expand All @@ -30,6 +35,7 @@ class UserController {
private final UserInvitationService invitationService;
private final UserService userService;
private final AssignmentManagementService assignmentManagementService;
private final QualificationService qualificationService;

@GetMapping("/users")
@PreAuthorize("isAuthenticated()")
Expand Down Expand Up @@ -95,4 +101,40 @@ public ResponseEntity<List<SimpleUserResponse>> searchUsers(
public ResponseEntity<ProjectAssignmentUserWorkloadResponse> getUserWorkload(@PathVariable UUID userId) {
return ResponseEntity.ok(assignmentManagementService.getUserWorkload(userId));
}

@GetMapping("/users/{userId}")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<UserResponse> getUser(@PathVariable UUID userId) {
return ResponseEntity.ok(userService.getUser(userId));
}

@GetMapping("/users/{userId}/qualifications")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<List<QualificationResponse>> getUserQualifications(@PathVariable UUID userId) {
return ResponseEntity.ok(qualificationService.getUserQualifications(userId));
}

@GetMapping("/users/{userId}/subordinates")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<List<UserResponse>> getSubordinates(@PathVariable UUID userId) {
return ResponseEntity.ok(userService.getSubordinates(userId));
}

@GetMapping("/users/{userId}/projects")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<List<ProjectResponse>> getManagedProjects(@PathVariable UUID userId) {
return ResponseEntity.ok(userService.getManagedProjects(userId));
}

@GetMapping("/users/{userId}/memberships")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<List<UserProjectMembershipResponse>> getProjectMemberships(@PathVariable UUID userId) {
return ResponseEntity.ok(userService.getProjectMemberships(userId));
}

@GetMapping("/users/{userId}/groups")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<List<OwnedGroupResponse>> getRelatedGroups(@PathVariable UUID userId) {
return ResponseEntity.ok(userService.getRelatedProjectGroups(userId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
public interface ProjectGroupRepository extends JpaRepository<ProjectGroup, UUID> {

List<ProjectGroup> getSingleGroupByGroupType(GroupType groupType);

List<ProjectGroup> findAllByOwner_IdOrderByNameAsc(UUID ownerId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ public interface UserRepository extends JpaRepository<User, UUID>, JpaSpecificat

@Query("SELECT u FROM User u WHERE LOWER(CONCAT(u.name, ' ', u.surname)) LIKE LOWER(CONCAT('%', :query, '%')) AND u.userRole = :role")
List<User> searchUserByFullNameAndRole(@Param("query") String query, @Param("role") UserRole role);

List<User> findAllBySupervisor_Id(UUID supervisorId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,42 @@
import org.springframework.transaction.annotation.Transactional;
import pl.edu.agh.project_manager.controller.dto.PagedResponse;
import pl.edu.agh.project_manager.controller.dto.project.ProjectAssignmentUserWorkloadResponse;
import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse;
import pl.edu.agh.project_manager.controller.dto.project.UserProjectMembershipResponse;
import pl.edu.agh.project_manager.controller.dto.project_group.OwnedGroupResponse;
import pl.edu.agh.project_manager.controller.dto.user.SimpleUserResponse;
import pl.edu.agh.project_manager.controller.dto.user.UserResponse;
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.user.User;
import pl.edu.agh.project_manager.domain.enums.AssignmentStatus;
import pl.edu.agh.project_manager.domain.enums.UserRole;
import pl.edu.agh.project_manager.domain.enums.UserStatus;
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.repository.projectgroup.ProjectGroupRepository;
import pl.edu.agh.project_manager.repository.user.UserRepository;
import pl.edu.agh.project_manager.util.assignments.AssignmentsUtil;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;
private final ProjectRepository projectRepository;
private final ProjectAssignmentRepository projectAssignmentRepository;
private final ProjectGroupRepository projectGroupRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
Expand Down Expand Up @@ -111,6 +125,77 @@ public User getUserEntityOrThrow(UUID userId) {
.orElseThrow(() -> new ApplicationException(ApiErrorCode.USER_NOT_FOUND, "Cannot find user: " + userId));
}

@Transactional(readOnly = true)
public UserResponse getUser(UUID userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ApplicationException(ApiErrorCode.USER_NOT_FOUND, "Cannot find user: " + userId));
return UserResponse.from(user);
}

@Transactional(readOnly = true)
public List<UserResponse> getSubordinates(UUID supervisorId) {
if (!userRepository.existsById(supervisorId)) {
throw new ApplicationException(ApiErrorCode.USER_NOT_FOUND, "Cannot find user: " + supervisorId);
}
return userRepository.findAllBySupervisor_Id(supervisorId).stream()
.map(UserResponse::from)
.toList();
}

@Transactional(readOnly = true)
public List<ProjectResponse> getManagedProjects(UUID managerId) {
if (!userRepository.existsById(managerId)) {
throw new ApplicationException(ApiErrorCode.USER_NOT_FOUND, "Cannot find user: " + managerId);
}
return projectRepository.findAllByProjectManagerId(managerId).stream()
.map(ProjectResponse::from)
.toList();
}

@Transactional(readOnly = true)
public List<OwnedGroupResponse> getRelatedProjectGroups(UUID userId) {
if (!userRepository.existsById(userId)) {
throw new ApplicationException(ApiErrorCode.USER_NOT_FOUND, "Cannot find user: " + userId);
}

List<pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup> owned =
projectGroupRepository.findAllByOwner_IdOrderByNameAsc(userId);

Map<UUID, pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup> related =
new LinkedHashMap<>();
owned.forEach(g -> related.put(g.getId(), g));

projectRepository.findAllByProjectManagerId(userId).stream()
.map(p -> p.getProjectGroup())
.filter(g -> g != null)
.forEach(g -> related.putIfAbsent(g.getId(), g));

Set<UUID> ownedIds = owned.stream().map(g -> g.getId()).collect(Collectors.toSet());

return related.values().stream()
.map(g -> OwnedGroupResponse.from(g, ownedIds.contains(g.getId())))
.sorted(Comparator
.comparing(OwnedGroupResponse::isOwner).reversed()
.thenComparing(OwnedGroupResponse::name))
.toList();
}

@Transactional(readOnly = true)
public List<UserProjectMembershipResponse> getProjectMemberships(UUID userId) {
if (!userRepository.existsById(userId)) {
throw new ApplicationException(ApiErrorCode.USER_NOT_FOUND, "Cannot find user: " + userId);
}
List<ProjectAssignment> assignments = projectAssignmentRepository
.findAllByUserIdAndStatus(userId, AssignmentStatus.ACCEPTED);

Map<Project, List<ProjectAssignment>> byProject = assignments.stream()
.collect(Collectors.groupingBy(ProjectAssignment::getProject, LinkedHashMap::new, Collectors.toList()));

return byProject.entrySet().stream()
.map(e -> UserProjectMembershipResponse.from(e.getKey(), e.getValue()))
.toList();
}
Comment on lines +183 to +197

public List<User> getUsersByIdsOrThrow(List<UUID> userIds, String errorMessage) {
long uniqueCount = userIds.stream().distinct().count();
List<User> users = userRepository.findAllById(userIds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@ class ProjectPdfGeneratorTest {

@BeforeEach
void setUp() {
UserResponse manager = new UserResponse(UUID.randomUUID(),"john.doe@example.com", "John", "Doe", null,null, null, List.of()
UserResponse manager = new UserResponse(
UUID.randomUUID(),
"john.doe@example.com",
"John",
"Doe",
null,
null,
null,
null,
List.of()
);
projectData = new ProjectResponse(UUID.randomUUID(), "Test Project", "Description", LocalDate.now(), LocalDate.now().plusMonths(6), true, manager, null);
}
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ export const ENDPOINTS = {
DETAIL: (id: string) => `/users/${id}`,
RESEND_INVITATION: '/users/invitation',
WORKLOAD: (id?: string) => `/users/${id}/workload`,
SEARCH_USERS: '/users/search'
SEARCH_USERS: '/users/search',
QUALIFICATIONS: (id: string) => `/users/${id}/qualifications`,
SUBORDINATES: (id: string) => `/users/${id}/subordinates`,
PROJECTS: (id: string) => `/users/${id}/projects`,
MEMBERSHIPS: (id: string) => `/users/${id}/memberships`,
GROUPS: (id: string) => `/users/${id}/groups`,
},
ME: {
PROFILE: '/me',
QUALIFICATIONS: '/me/qualifications',
QUALIFICATION: (id: string) => `/me/qualifications/${id}`,
PASSWORD: '/me/password',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const employeeAssignmentsKeys = createQueryKeys('employee-assignments', {
})

export const qualificationsKeys = createQueryKeys('qualifications', {
mine: null, // me/qualifications
mine: null,
byUser: (userId: string) => [userId],
suggestions: (query: string) => [query],
});

Expand Down
3 changes: 1 addition & 2 deletions frontend/src/features/notification/notification.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export const getNotificationUrl = (type: NotificationType, referenceId: string):
return `${PATHS.PROJECT_REQUESTS}?${QUERY_PARAMS.REQUEST_ID}=${referenceId}`;

case 'ASSIGNMENT_ACCEPTED':
return PATHS.PROJECT(referenceId);
case 'ASSIGNMENT_REJECTED':
return PATHS.PROJECT(referenceId);

Expand All @@ -19,7 +18,7 @@ export const getNotificationUrl = (type: NotificationType, referenceId: string):
return PATHS.PROFILE;

case 'SYSTEM_NEW_EMPLOYEE':
return PATHS.PROFILE; // TODO: Powinno przenosic na profil pracownika, ale nie ma obecnie takiej mozliwosci
return PATHS.EMPLOYEE_DETAILS(referenceId);

default:
return PATHS.ROOT;
Expand Down
Loading
Loading