Add optional assignee to task form
This commit is contained in:
parent
7c3ba32b3f
commit
ae0d536ff7
15 changed files with 328 additions and 23 deletions
|
|
@ -9,6 +9,7 @@ import net.beetechgroup.beetask.usecase.organization.requests.ListPendingRequest
|
||||||
import net.beetechgroup.beetask.usecase.organization.requests.ListUserJoinRequestsUseCase;
|
import net.beetechgroup.beetask.usecase.organization.requests.ListUserJoinRequestsUseCase;
|
||||||
import net.beetechgroup.beetask.usecase.organization.requests.HandleJoinRequestUseCase;
|
import net.beetechgroup.beetask.usecase.organization.requests.HandleJoinRequestUseCase;
|
||||||
import net.beetechgroup.beetask.usecase.organization.auth.AuthorizeOrganizationAdminUseCase;
|
import net.beetechgroup.beetask.usecase.organization.auth.AuthorizeOrganizationAdminUseCase;
|
||||||
|
import net.beetechgroup.beetask.usecase.organization.members.ListOrganizationMembersUseCase;
|
||||||
import net.beetechgroup.beetask.usecase.repository.OrganizationRepository;
|
import net.beetechgroup.beetask.usecase.repository.OrganizationRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||||
|
|
@ -58,4 +59,10 @@ public class OrganizationUseCaseConfig {
|
||||||
UserRepository userRepository) {
|
UserRepository userRepository) {
|
||||||
return new ListUserJoinRequestsUseCase(userOrganizationRepository, userRepository);
|
return new ListUserJoinRequestsUseCase(userOrganizationRepository, userRepository);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Produces
|
||||||
|
public ListOrganizationMembersUseCase listOrganizationMembersUseCase(UserRepository userRepository,
|
||||||
|
UserOrganizationRepository userOrganizationRepository) {
|
||||||
|
return new ListOrganizationMembersUseCase(userRepository, userOrganizationRepository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import jakarta.enterprise.inject.Produces;
|
||||||
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskUseCase;
|
import net.beetechgroup.beetask.usecase.task.create.CreateTaskUseCase;
|
||||||
import net.beetechgroup.beetask.usecase.task.listall.ListAllTasksUseCase;
|
import net.beetechgroup.beetask.usecase.task.listall.ListAllTasksUseCase;
|
||||||
|
|
@ -20,8 +21,9 @@ public class TaskUseCaseConfig {
|
||||||
|
|
||||||
@Produces
|
@Produces
|
||||||
public CreateTaskUseCase createTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
public CreateTaskUseCase createTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||||
return new CreateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository);
|
UserOrganizationRepository userOrganizationRepository) {
|
||||||
|
return new CreateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository, userOrganizationRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Produces
|
@Produces
|
||||||
|
|
@ -51,8 +53,9 @@ public class TaskUseCaseConfig {
|
||||||
|
|
||||||
@Produces
|
@Produces
|
||||||
public UpdateTaskUseCase updateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
public UpdateTaskUseCase updateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||||
return new UpdateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository);
|
UserOrganizationRepository userOrganizationRepository) {
|
||||||
|
return new UpdateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository, userOrganizationRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Produces
|
@Produces
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import net.beetechgroup.beetask.usecase.organization.requests.ListUserJoinReques
|
||||||
import net.beetechgroup.beetask.usecase.organization.requests.UserJoinRequestOutput;
|
import net.beetechgroup.beetask.usecase.organization.requests.UserJoinRequestOutput;
|
||||||
import net.beetechgroup.beetask.usecase.organization.requests.JoinRequestOutput;
|
import net.beetechgroup.beetask.usecase.organization.requests.JoinRequestOutput;
|
||||||
import net.beetechgroup.beetask.usecase.organization.requests.HandleJoinRequestUseCase;
|
import net.beetechgroup.beetask.usecase.organization.requests.HandleJoinRequestUseCase;
|
||||||
|
import net.beetechgroup.beetask.usecase.organization.members.ListOrganizationMembersUseCase;
|
||||||
|
import net.beetechgroup.beetask.usecase.organization.members.OrganizationMemberOutput;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -47,6 +49,9 @@ public class OrganizationController {
|
||||||
@Inject
|
@Inject
|
||||||
HandleJoinRequestUseCase handleJoinRequestUseCase;
|
HandleJoinRequestUseCase handleJoinRequestUseCase;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ListOrganizationMembersUseCase listOrganizationMembersUseCase;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityIdentity securityIdentity;
|
SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
|
@ -116,6 +121,17 @@ public class OrganizationController {
|
||||||
LOGGER.infof("Join request rejected for user %d in organization %d by %s", userId, organizationId, userEmail);
|
LOGGER.infof("Join request rejected for user %d in organization %d by %s", userId, organizationId, userEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}/members")
|
||||||
|
@Authenticated
|
||||||
|
@Operation(summary = "List organization members", description = "List all active members of an organization")
|
||||||
|
public List<OrganizationMemberOutput> listMembers(@PathParam("id") Long organizationId) {
|
||||||
|
String userEmail = securityIdentity.getPrincipal().getName();
|
||||||
|
List<OrganizationMemberOutput> members = listOrganizationMembersUseCase.execute(userEmail, organizationId);
|
||||||
|
LOGGER.infof("User %s listed %d active members for organization %d", userEmail, members.size(), organizationId);
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/my-requests")
|
@Path("/my-requests")
|
||||||
@Authenticated
|
@Authenticated
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ public record CreateTaskRequest(
|
||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
TaskStatus status,
|
TaskStatus status,
|
||||||
|
Long organizationId,
|
||||||
Long projectId,
|
Long projectId,
|
||||||
Long categoryId,
|
Long categoryId,
|
||||||
|
Long assigneeUserId,
|
||||||
List<TaskHistoryItemResponse> history) {
|
List<TaskHistoryItemResponse> history) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,10 @@ public class TaskControllerMapper {
|
||||||
request.title(),
|
request.title(),
|
||||||
request.description(),
|
request.description(),
|
||||||
request.status(),
|
request.status(),
|
||||||
|
request.organizationId(),
|
||||||
request.projectId(),
|
request.projectId(),
|
||||||
request.categoryId(),
|
request.categoryId(),
|
||||||
|
request.assigneeUserId(),
|
||||||
userEmail,
|
userEmail,
|
||||||
Objects.nonNull(request.history()) ? request.history().stream()
|
Objects.nonNull(request.history()) ? request.history().stream()
|
||||||
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
|
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
|
||||||
|
|
@ -85,8 +87,10 @@ public class TaskControllerMapper {
|
||||||
request.title(),
|
request.title(),
|
||||||
request.description(),
|
request.description(),
|
||||||
request.status(),
|
request.status(),
|
||||||
|
request.organizationId(),
|
||||||
request.projectId(),
|
request.projectId(),
|
||||||
request.categoryId(),
|
request.categoryId(),
|
||||||
|
request.assigneeUserId(),
|
||||||
userEmail,
|
userEmail,
|
||||||
Objects.nonNull(request.history()) ? request.history().stream()
|
Objects.nonNull(request.history()) ? request.history().stream()
|
||||||
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
|
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package net.beetechgroup.beetask.usecase.organization.members;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import net.beetechgroup.beetask.entities.organization.UserOrganizationStatus;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
public class ListOrganizationMembersUseCase {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(ListOrganizationMembersUseCase.class);
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final UserOrganizationRepository userOrganizationRepository;
|
||||||
|
|
||||||
|
public ListOrganizationMembersUseCase(UserRepository userRepository,
|
||||||
|
UserOrganizationRepository userOrganizationRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.userOrganizationRepository = userOrganizationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OrganizationMemberOutput> execute(String userEmail, Long organizationId) {
|
||||||
|
var requester = userRepository.findByEmail(userEmail)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Usuário não encontrado: " + userEmail));
|
||||||
|
|
||||||
|
boolean belongsToOrganization = userRepository.findUserOrganizations(requester.getId()).stream()
|
||||||
|
.anyMatch(membership -> membership.getOrganization().getId().equals(organizationId));
|
||||||
|
|
||||||
|
if (!belongsToOrganization) {
|
||||||
|
LOGGER.warnf("User %s attempted to list members of organization %d without membership", userEmail, organizationId);
|
||||||
|
throw new SecurityException("Você não tem permissão para listar membros desta organização.");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OrganizationMemberOutput> members = userOrganizationRepository
|
||||||
|
.findByOrganizationIdAndStatus(organizationId, UserOrganizationStatus.ACTIVE)
|
||||||
|
.stream()
|
||||||
|
.map(membership -> new OrganizationMemberOutput(
|
||||||
|
membership.getUser().getId(),
|
||||||
|
membership.getUser().getName(),
|
||||||
|
membership.getUser().getEmail(),
|
||||||
|
membership.getUser().getPhoto()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
LOGGER.infof("Loaded %d active members for organization %d requested by %s", members.size(), organizationId, userEmail);
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package net.beetechgroup.beetask.usecase.organization.members;
|
||||||
|
|
||||||
|
public record OrganizationMemberOutput(
|
||||||
|
Long userId,
|
||||||
|
String userName,
|
||||||
|
String userEmail,
|
||||||
|
String userPhoto) {}
|
||||||
|
|
@ -8,8 +8,10 @@ public record CreateTaskInput(
|
||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
TaskStatus status,
|
TaskStatus status,
|
||||||
|
Long organizationId,
|
||||||
Long projectId,
|
Long projectId,
|
||||||
Long categoryId,
|
Long categoryId,
|
||||||
|
Long assigneeUserId,
|
||||||
String userEmail,
|
String userEmail,
|
||||||
List<TaskHistoryItemInput> history) {
|
List<TaskHistoryItemInput> history) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import net.beetechgroup.beetask.entities.Category;
|
import net.beetechgroup.beetask.entities.Category;
|
||||||
import net.beetechgroup.beetask.entities.Project;
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
|
import net.beetechgroup.beetask.entities.User;
|
||||||
|
import net.beetechgroup.beetask.entities.organization.UserOrganizationStatus;
|
||||||
import net.beetechgroup.beetask.entities.task.Task;
|
import net.beetechgroup.beetask.entities.task.Task;
|
||||||
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||||
|
|
@ -18,13 +21,16 @@ public class CreateTaskUseCase {
|
||||||
private final ProjectRepository projectRepository;
|
private final ProjectRepository projectRepository;
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final UserOrganizationRepository userOrganizationRepository;
|
||||||
|
|
||||||
public CreateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
public CreateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||||
|
UserOrganizationRepository userOrganizationRepository) {
|
||||||
this.taskRepository = taskRepository;
|
this.taskRepository = taskRepository;
|
||||||
this.projectRepository = projectRepository;
|
this.projectRepository = projectRepository;
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.userOrganizationRepository = userOrganizationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateTaskOutput execute(CreateTaskInput input) {
|
public CreateTaskOutput execute(CreateTaskInput input) {
|
||||||
|
|
@ -52,11 +58,8 @@ public class CreateTaskUseCase {
|
||||||
task.setCategory(category);
|
task.setCategory(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
task.setUser(userRepository.findByEmail(input.userEmail())
|
validateRequesterMembership(input.organizationId(), input.userEmail());
|
||||||
.orElseThrow(() -> {
|
task.setUser(resolveAssignee(input.organizationId(), input.assigneeUserId()));
|
||||||
LOGGER.warnf("Task creation failed because user %s was not found", input.userEmail());
|
|
||||||
return new IllegalArgumentException("Usuário não encontrado: " + input.userEmail());
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (Objects.nonNull(input.history())) {
|
if (Objects.nonNull(input.history())) {
|
||||||
List<TaskHistoryItem> historyItems = input.history().stream().map(h -> {
|
List<TaskHistoryItem> historyItems = input.history().stream().map(h -> {
|
||||||
|
|
@ -72,4 +75,46 @@ public class CreateTaskUseCase {
|
||||||
LOGGER.infof("Task %d created for user %s", output.id(), input.userEmail());
|
LOGGER.infof("Task %d created for user %s", output.id(), input.userEmail());
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateRequesterMembership(Long organizationId, String userEmail) {
|
||||||
|
if (Objects.isNull(organizationId)) {
|
||||||
|
throw new IllegalArgumentException("Organização é obrigatória para criar a tarefa.");
|
||||||
|
}
|
||||||
|
|
||||||
|
User requester = userRepository.findByEmail(userEmail)
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
LOGGER.warnf("Task creation failed because user %s was not found", userEmail);
|
||||||
|
return new IllegalArgumentException("Usuário não encontrado: " + userEmail);
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean belongsToOrganization = userRepository.findUserOrganizations(requester.getId()).stream()
|
||||||
|
.anyMatch(membership -> membership.getOrganization().getId().equals(organizationId));
|
||||||
|
|
||||||
|
if (!belongsToOrganization) {
|
||||||
|
LOGGER.warnf("Task creation denied because user %s is not an active member of organization %d", userEmail, organizationId);
|
||||||
|
throw new SecurityException("Você não tem permissão para criar tarefas nesta organização.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private User resolveAssignee(Long organizationId, Long assigneeUserId) {
|
||||||
|
if (Objects.isNull(assigneeUserId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean belongsToOrganization = userOrganizationRepository
|
||||||
|
.findByOrganizationIdAndStatus(organizationId, UserOrganizationStatus.ACTIVE)
|
||||||
|
.stream()
|
||||||
|
.anyMatch(membership -> membership.getUser().getId().equals(assigneeUserId));
|
||||||
|
|
||||||
|
if (!belongsToOrganization) {
|
||||||
|
LOGGER.warnf("Task creation failed because assignee %d is not an active member of organization %d", assigneeUserId, organizationId);
|
||||||
|
throw new IllegalArgumentException("Responsável não pertence à organização selecionada.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRepository.findUserById(assigneeUserId)
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
LOGGER.warnf("Task creation failed because assignee %d was not found", assigneeUserId);
|
||||||
|
return new IllegalArgumentException("Usuário responsável não encontrado: " + assigneeUserId);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ public record UpdateTaskInput(
|
||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
TaskStatus status,
|
TaskStatus status,
|
||||||
|
Long organizationId,
|
||||||
Long projectId,
|
Long projectId,
|
||||||
Long categoryId,
|
Long categoryId,
|
||||||
|
Long assigneeUserId,
|
||||||
String userEmail,
|
String userEmail,
|
||||||
List<TaskHistoryItemInput> history) {
|
List<TaskHistoryItemInput> history) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import net.beetechgroup.beetask.entities.Category;
|
import net.beetechgroup.beetask.entities.Category;
|
||||||
import net.beetechgroup.beetask.entities.Project;
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
|
import net.beetechgroup.beetask.entities.User;
|
||||||
|
import net.beetechgroup.beetask.entities.organization.UserOrganizationStatus;
|
||||||
import net.beetechgroup.beetask.entities.task.Task;
|
import net.beetechgroup.beetask.entities.task.Task;
|
||||||
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
||||||
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper;
|
import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper;
|
||||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
|
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
|
||||||
|
|
@ -20,26 +23,23 @@ public class UpdateTaskUseCase {
|
||||||
private final ProjectRepository projectRepository;
|
private final ProjectRepository projectRepository;
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final UserOrganizationRepository userOrganizationRepository;
|
||||||
|
|
||||||
public UpdateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
public UpdateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||||
|
UserOrganizationRepository userOrganizationRepository) {
|
||||||
this.taskRepository = taskRepository;
|
this.taskRepository = taskRepository;
|
||||||
this.projectRepository = projectRepository;
|
this.projectRepository = projectRepository;
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.userOrganizationRepository = userOrganizationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateTaskOutput execute(UpdateTaskInput input) {
|
public CreateTaskOutput execute(UpdateTaskInput input) {
|
||||||
LOGGER.infof("Updating task %d requested by user %s", input.id(), input.userEmail());
|
LOGGER.infof("Updating task %d requested by user %s", input.id(), input.userEmail());
|
||||||
Task task = taskRepository.findTaskById(input.id());
|
Task task = taskRepository.findTaskById(input.id());
|
||||||
|
|
||||||
if (Objects.isNull(task.getUser())) {
|
validateRequesterMembership(input.organizationId(), input.userEmail());
|
||||||
task.setUser(userRepository.findByEmail(input.userEmail())
|
|
||||||
.orElseThrow(() -> {
|
|
||||||
LOGGER.warnf("Task update failed because user %s was not found", input.userEmail());
|
|
||||||
return new IllegalArgumentException("Usuário não encontrado: " + input.userEmail());
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
task.setTitle(input.title());
|
task.setTitle(input.title());
|
||||||
task.setDescription(input.description());
|
task.setDescription(input.description());
|
||||||
|
|
@ -67,6 +67,8 @@ public class UpdateTaskUseCase {
|
||||||
task.setCategory(null);
|
task.setCategory(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task.setUser(resolveAssignee(input.organizationId(), input.assigneeUserId()));
|
||||||
|
|
||||||
if (Objects.nonNull(input.history())) {
|
if (Objects.nonNull(input.history())) {
|
||||||
List<TaskHistoryItem> updatedHistory = input.history().stream().map(hInput -> {
|
List<TaskHistoryItem> updatedHistory = input.history().stream().map(hInput -> {
|
||||||
if (Objects.nonNull(hInput.id())) {
|
if (Objects.nonNull(hInput.id())) {
|
||||||
|
|
@ -101,4 +103,46 @@ public class UpdateTaskUseCase {
|
||||||
LOGGER.infof("Task %d updated successfully", input.id());
|
LOGGER.infof("Task %d updated successfully", input.id());
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateRequesterMembership(Long organizationId, String userEmail) {
|
||||||
|
if (Objects.isNull(organizationId)) {
|
||||||
|
throw new IllegalArgumentException("Organização é obrigatória para atualizar a tarefa.");
|
||||||
|
}
|
||||||
|
|
||||||
|
User requester = userRepository.findByEmail(userEmail)
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
LOGGER.warnf("Task update failed because user %s was not found", userEmail);
|
||||||
|
return new IllegalArgumentException("Usuário não encontrado: " + userEmail);
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean belongsToOrganization = userRepository.findUserOrganizations(requester.getId()).stream()
|
||||||
|
.anyMatch(membership -> membership.getOrganization().getId().equals(organizationId));
|
||||||
|
|
||||||
|
if (!belongsToOrganization) {
|
||||||
|
LOGGER.warnf("Task update denied because user %s is not an active member of organization %d", userEmail, organizationId);
|
||||||
|
throw new SecurityException("Você não tem permissão para atualizar tarefas nesta organização.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private User resolveAssignee(Long organizationId, Long assigneeUserId) {
|
||||||
|
if (Objects.isNull(assigneeUserId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean belongsToOrganization = userOrganizationRepository
|
||||||
|
.findByOrganizationIdAndStatus(organizationId, UserOrganizationStatus.ACTIVE)
|
||||||
|
.stream()
|
||||||
|
.anyMatch(membership -> membership.getUser().getId().equals(assigneeUserId));
|
||||||
|
|
||||||
|
if (!belongsToOrganization) {
|
||||||
|
LOGGER.warnf("Task update failed because assignee %d is not an active member of organization %d", assigneeUserId, organizationId);
|
||||||
|
throw new IllegalArgumentException("Responsável não pertence à organização selecionada.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRepository.findUserById(assigneeUserId)
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
LOGGER.warnf("Task update failed because assignee %d was not found", assigneeUserId);
|
||||||
|
return new IllegalArgumentException("Usuário responsável não encontrado: " + assigneeUserId);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Layout,
|
Layout,
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
Send,
|
Send,
|
||||||
|
Users,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
@ -22,12 +23,18 @@ import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { projectService, type Project } from "../services/projectService";
|
import { projectService, type Project } from "../services/projectService";
|
||||||
import { categoryService } from "../services/categoryService";
|
import { categoryService } from "../services/categoryService";
|
||||||
|
import {
|
||||||
|
organizationService,
|
||||||
|
type OrganizationMember,
|
||||||
|
} from "../services/organizationService";
|
||||||
import type { Category } from "../types/category";
|
import type { Category } from "../types/category";
|
||||||
import { CategoryIcon } from "../components/CategoryIcon";
|
import { CategoryIcon } from "../components/CategoryIcon";
|
||||||
|
import { UserAvatar } from "../components/UserAvatar";
|
||||||
|
|
||||||
type FormState = Omit<CreateTaskPayload, 'projectId' | 'categoryId'> & {
|
type FormState = Omit<CreateTaskPayload, 'organizationId' | 'projectId' | 'categoryId' | 'assigneeUserId'> & {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
categoryId: string;
|
categoryId: string;
|
||||||
|
assigneeUserId: string;
|
||||||
history: TaskHistoryItem[];
|
history: TaskHistoryItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -37,6 +44,7 @@ const initialFormState: FormState = {
|
||||||
status: "NOT_STARTED",
|
status: "NOT_STARTED",
|
||||||
projectId: "",
|
projectId: "",
|
||||||
categoryId: "",
|
categoryId: "",
|
||||||
|
assigneeUserId: "",
|
||||||
history: [],
|
history: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,7 +55,9 @@ export function CreateTaskPage() {
|
||||||
const [createdTask, setCreatedTask] = useState<TaskResponse | null>(null);
|
const [createdTask, setCreatedTask] = useState<TaskResponse | null>(null);
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const { activeOrg } = useAuth();
|
const [members, setMembers] = useState<OrganizationMember[]>([]);
|
||||||
|
const [assignToLoggedUser, setAssignToLoggedUser] = useState(false);
|
||||||
|
const { activeOrg, user } = useAuth();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isEdit = !!id;
|
const isEdit = !!id;
|
||||||
|
|
@ -63,8 +73,10 @@ export function CreateTaskPage() {
|
||||||
status: data.status,
|
status: data.status,
|
||||||
projectId: data.project?.id?.toString() || "",
|
projectId: data.project?.id?.toString() || "",
|
||||||
categoryId: data.category?.id?.toString() || "",
|
categoryId: data.category?.id?.toString() || "",
|
||||||
|
assigneeUserId: data.user?.id?.toString() || "",
|
||||||
history: data.history || [],
|
history: data.history || [],
|
||||||
});
|
});
|
||||||
|
setAssignToLoggedUser(Boolean(user?.email && data.user?.email === user.email));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar tarefa:", error);
|
console.error("Erro ao carregar tarefa:", error);
|
||||||
setErrorMessage("Não foi possível carregar os dados da tarefa.");
|
setErrorMessage("Não foi possível carregar os dados da tarefa.");
|
||||||
|
|
@ -72,7 +84,7 @@ export function CreateTaskPage() {
|
||||||
}
|
}
|
||||||
loadTask();
|
loadTask();
|
||||||
}
|
}
|
||||||
}, [id, isEdit]);
|
}, [id, isEdit, user?.email]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
|
|
@ -93,10 +105,30 @@ export function CreateTaskPage() {
|
||||||
console.error("Erro ao carregar categorias:", error);
|
console.error("Erro ao carregar categorias:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function loadMembers() {
|
||||||
|
if (!activeOrg) return;
|
||||||
|
try {
|
||||||
|
const data = await organizationService.listMembers(activeOrg.id);
|
||||||
|
setMembers(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar membros:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
loadProjects();
|
loadProjects();
|
||||||
loadCategories();
|
loadCategories();
|
||||||
|
loadMembers();
|
||||||
}, [activeOrg]);
|
}, [activeOrg]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeOrg) return;
|
||||||
|
if (assignToLoggedUser && user) {
|
||||||
|
const currentMember = members.find((member) => member.userEmail === user.email);
|
||||||
|
if (currentMember) {
|
||||||
|
setForm((current) => ({ ...current, assigneeUserId: currentMember.userId.toString() }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [assignToLoggedUser, user, members, activeOrg]);
|
||||||
|
|
||||||
function updateField(
|
function updateField(
|
||||||
event: ChangeEvent<
|
event: ChangeEvent<
|
||||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||||
|
|
@ -115,12 +147,18 @@ export function CreateTaskPage() {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!activeOrg) {
|
||||||
|
throw new Error("Nenhuma organização ativa selecionada.");
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: form.title.trim(),
|
title: form.title.trim(),
|
||||||
description: form.description.trim(),
|
description: form.description.trim(),
|
||||||
status: form.status,
|
status: form.status,
|
||||||
|
organizationId: activeOrg.id,
|
||||||
projectId: form.projectId ? Number(form.projectId) : null,
|
projectId: form.projectId ? Number(form.projectId) : null,
|
||||||
categoryId: form.categoryId ? Number(form.categoryId) : null,
|
categoryId: form.categoryId ? Number(form.categoryId) : null,
|
||||||
|
assigneeUserId: form.assigneeUserId ? Number(form.assigneeUserId) : null,
|
||||||
history: form.history,
|
history: form.history,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -131,6 +169,7 @@ export function CreateTaskPage() {
|
||||||
const response = await taskService.createTask(payload);
|
const response = await taskService.createTask(payload);
|
||||||
setCreatedTask(response);
|
setCreatedTask(response);
|
||||||
setForm(initialFormState);
|
setForm(initialFormState);
|
||||||
|
setAssignToLoggedUser(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(
|
setErrorMessage(
|
||||||
|
|
@ -198,6 +237,10 @@ export function CreateTaskPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedAssignee = members.find(
|
||||||
|
(member) => member.userId === Number(form.assigneeUserId),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl pb-20">
|
<div className="mx-auto max-w-3xl pb-20">
|
||||||
<div className="mb-8 flex items-center justify-between">
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
|
@ -324,6 +367,69 @@ export function CreateTaskPage() {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 rounded-[2rem] border border-border-soft bg-app-bg/40 p-5">
|
||||||
|
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||||
|
<label className="group block flex-1">
|
||||||
|
<div className="mb-2 flex items-center gap-2 text-sm font-bold text-text-main group-focus-within:text-brand">
|
||||||
|
<Users size={16} />
|
||||||
|
Funcionário Responsável
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
name="assigneeUserId"
|
||||||
|
value={form.assigneeUserId}
|
||||||
|
onChange={updateField}
|
||||||
|
disabled={assignToLoggedUser}
|
||||||
|
className="h-12 w-full appearance-none rounded-xl border border-border-soft bg-app-bg px-4 text-sm font-medium outline-none transition-all disabled:cursor-not-allowed disabled:opacity-60 focus:border-brand focus:ring-4 focus:ring-brand/10"
|
||||||
|
>
|
||||||
|
<option value="">Sem responsável</option>
|
||||||
|
{members.map((member) => (
|
||||||
|
<option key={member.userId} value={member.userId}>
|
||||||
|
{member.userName} - {member.userEmail}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-3 rounded-xl border border-border-soft bg-surface px-4 py-3 text-sm font-bold text-text-main">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={assignToLoggedUser}
|
||||||
|
onChange={(event) => {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
setAssignToLoggedUser(checked);
|
||||||
|
if (!checked && user?.email && selectedAssignee?.userEmail === user.email) {
|
||||||
|
setForm((current) => ({ ...current, assigneeUserId: current.assigneeUserId }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-4 w-4 rounded border-border-soft text-brand focus:ring-brand"
|
||||||
|
/>
|
||||||
|
Associar ao usuário logado
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedAssignee ? (
|
||||||
|
<div className="flex items-center gap-3 rounded-2xl border border-border-soft bg-surface p-4">
|
||||||
|
<UserAvatar
|
||||||
|
name={selectedAssignee.userName}
|
||||||
|
photo={selectedAssignee.userPhoto}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-black text-text-main">
|
||||||
|
{selectedAssignee.userName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs font-medium text-text-muted">
|
||||||
|
{selectedAssignee.userEmail}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs font-medium text-text-muted">
|
||||||
|
Esta tarefa pode ficar sem responsável.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description Section */}
|
{/* Description Section */}
|
||||||
<label className="group block">
|
<label className="group block">
|
||||||
<div className="mb-2 flex items-center gap-2 text-sm font-bold text-text-main group-focus-within:text-brand">
|
<div className="mb-2 flex items-center gap-2 text-sm font-bold text-text-main group-focus-within:text-brand">
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ export interface UserJoinRequest {
|
||||||
status: 'PENDING' | 'ACTIVE' | 'REJECTED'
|
status: 'PENDING' | 'ACTIVE' | 'REJECTED'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrganizationMember {
|
||||||
|
userId: number
|
||||||
|
userName: string
|
||||||
|
userEmail: string
|
||||||
|
userPhoto?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export const organizationService = {
|
export const organizationService = {
|
||||||
create: async (name: string): Promise<Organization> => {
|
create: async (name: string): Promise<Organization> => {
|
||||||
return apiFetch('/organizations', {
|
return apiFetch('/organizations', {
|
||||||
|
|
@ -46,6 +53,10 @@ export const organizationService = {
|
||||||
return apiFetch(`/organizations/${organizationId}/requests/${userId}/reject`, { method: 'PATCH' })
|
return apiFetch(`/organizations/${organizationId}/requests/${userId}/reject`, { method: 'PATCH' })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
listMembers: async (organizationId: number): Promise<OrganizationMember[]> => {
|
||||||
|
return apiFetch<OrganizationMember[]>(`/organizations/${organizationId}/members`)
|
||||||
|
},
|
||||||
|
|
||||||
getUserRequests: async (): Promise<UserJoinRequest[]> => {
|
getUserRequests: async (): Promise<UserJoinRequest[]> => {
|
||||||
return apiFetch<UserJoinRequest[]>('/organizations/my-requests')
|
return apiFetch<UserJoinRequest[]>('/organizations/my-requests')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ export const taskService = {
|
||||||
async createTask(task: {
|
async createTask(task: {
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
|
organizationId: number
|
||||||
projectId?: number | null
|
projectId?: number | null
|
||||||
categoryId?: number | null
|
categoryId?: number | null
|
||||||
|
assigneeUserId?: number | null
|
||||||
status?: TaskStatus
|
status?: TaskStatus
|
||||||
}): Promise<TaskResponse> {
|
}): Promise<TaskResponse> {
|
||||||
return apiFetch<TaskResponse>('/tasks', {
|
return apiFetch<TaskResponse>('/tasks', {
|
||||||
|
|
@ -59,7 +61,12 @@ export const taskService = {
|
||||||
return apiFetch<TaskResponse>(`/tasks/${id}`)
|
return apiFetch<TaskResponse>(`/tasks/${id}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTask(id: number, task: Partial<TaskResponse> & { projectId?: number | null; categoryId?: number | null }): Promise<TaskResponse> {
|
async updateTask(id: number, task: Partial<TaskResponse> & {
|
||||||
|
organizationId: number
|
||||||
|
projectId?: number | null
|
||||||
|
categoryId?: number | null
|
||||||
|
assigneeUserId?: number | null
|
||||||
|
}): Promise<TaskResponse> {
|
||||||
return apiFetch<TaskResponse>(`/tasks/${id}`, {
|
return apiFetch<TaskResponse>(`/tasks/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: task,
|
body: task,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ export type CreateTaskPayload = {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
status: TaskStatus
|
status: TaskStatus
|
||||||
|
organizationId: number
|
||||||
projectId: number | null
|
projectId: number | null
|
||||||
categoryId: number | null
|
categoryId: number | null
|
||||||
|
assigneeUserId: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskHistoryItem = {
|
export type TaskHistoryItem = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue