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.HandleJoinRequestUseCase;
|
||||
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.UserOrganizationRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||
|
|
@ -58,4 +59,10 @@ public class OrganizationUseCaseConfig {
|
|||
UserRepository 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.ProjectRepository;
|
||||
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.task.create.CreateTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.listall.ListAllTasksUseCase;
|
||||
|
|
@ -20,8 +21,9 @@ public class TaskUseCaseConfig {
|
|||
|
||||
@Produces
|
||||
public CreateTaskUseCase createTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
||||
return new CreateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository);
|
||||
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||
UserOrganizationRepository userOrganizationRepository) {
|
||||
return new CreateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository, userOrganizationRepository);
|
||||
}
|
||||
|
||||
@Produces
|
||||
|
|
@ -51,8 +53,9 @@ public class TaskUseCaseConfig {
|
|||
|
||||
@Produces
|
||||
public UpdateTaskUseCase updateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
||||
return new UpdateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository);
|
||||
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||
UserOrganizationRepository userOrganizationRepository) {
|
||||
return new UpdateTaskUseCase(taskRepository, projectRepository, categoryRepository, userRepository, userOrganizationRepository);
|
||||
}
|
||||
|
||||
@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.JoinRequestOutput;
|
||||
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 java.util.List;
|
||||
|
|
@ -47,6 +49,9 @@ public class OrganizationController {
|
|||
@Inject
|
||||
HandleJoinRequestUseCase handleJoinRequestUseCase;
|
||||
|
||||
@Inject
|
||||
ListOrganizationMembersUseCase listOrganizationMembersUseCase;
|
||||
|
||||
@Inject
|
||||
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);
|
||||
}
|
||||
|
||||
@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
|
||||
@Path("/my-requests")
|
||||
@Authenticated
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ public record CreateTaskRequest(
|
|||
String title,
|
||||
String description,
|
||||
TaskStatus status,
|
||||
Long organizationId,
|
||||
Long projectId,
|
||||
Long categoryId,
|
||||
Long assigneeUserId,
|
||||
List<TaskHistoryItemResponse> history) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ public class TaskControllerMapper {
|
|||
request.title(),
|
||||
request.description(),
|
||||
request.status(),
|
||||
request.organizationId(),
|
||||
request.projectId(),
|
||||
request.categoryId(),
|
||||
request.assigneeUserId(),
|
||||
userEmail,
|
||||
Objects.nonNull(request.history()) ? request.history().stream()
|
||||
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
|
||||
|
|
@ -85,8 +87,10 @@ public class TaskControllerMapper {
|
|||
request.title(),
|
||||
request.description(),
|
||||
request.status(),
|
||||
request.organizationId(),
|
||||
request.projectId(),
|
||||
request.categoryId(),
|
||||
request.assigneeUserId(),
|
||||
userEmail,
|
||||
Objects.nonNull(request.history()) ? request.history().stream()
|
||||
.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 description,
|
||||
TaskStatus status,
|
||||
Long organizationId,
|
||||
Long projectId,
|
||||
Long categoryId,
|
||||
Long assigneeUserId,
|
||||
String userEmail,
|
||||
List<TaskHistoryItemInput> history) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.Category;
|
||||
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.TaskHistoryItem;
|
||||
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||
|
|
@ -18,13 +21,16 @@ public class CreateTaskUseCase {
|
|||
private final ProjectRepository projectRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final UserOrganizationRepository userOrganizationRepository;
|
||||
|
||||
public CreateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
||||
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||
UserOrganizationRepository userOrganizationRepository) {
|
||||
this.taskRepository = taskRepository;
|
||||
this.projectRepository = projectRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userOrganizationRepository = userOrganizationRepository;
|
||||
}
|
||||
|
||||
public CreateTaskOutput execute(CreateTaskInput input) {
|
||||
|
|
@ -52,11 +58,8 @@ public class CreateTaskUseCase {
|
|||
task.setCategory(category);
|
||||
}
|
||||
|
||||
task.setUser(userRepository.findByEmail(input.userEmail())
|
||||
.orElseThrow(() -> {
|
||||
LOGGER.warnf("Task creation failed because user %s was not found", input.userEmail());
|
||||
return new IllegalArgumentException("Usuário não encontrado: " + input.userEmail());
|
||||
}));
|
||||
validateRequesterMembership(input.organizationId(), input.userEmail());
|
||||
task.setUser(resolveAssignee(input.organizationId(), input.assigneeUserId()));
|
||||
|
||||
if (Objects.nonNull(input.history())) {
|
||||
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());
|
||||
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 description,
|
||||
TaskStatus status,
|
||||
Long organizationId,
|
||||
Long projectId,
|
||||
Long categoryId,
|
||||
Long assigneeUserId,
|
||||
String userEmail,
|
||||
List<TaskHistoryItemInput> history) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.Category;
|
||||
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.TaskHistoryItem;
|
||||
import net.beetechgroup.beetask.usecase.repository.CategoryRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||
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.task.create.CreateTaskMapper;
|
||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
|
||||
|
|
@ -20,26 +23,23 @@ public class UpdateTaskUseCase {
|
|||
private final ProjectRepository projectRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final UserOrganizationRepository userOrganizationRepository;
|
||||
|
||||
public UpdateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository,
|
||||
CategoryRepository categoryRepository, UserRepository userRepository) {
|
||||
CategoryRepository categoryRepository, UserRepository userRepository,
|
||||
UserOrganizationRepository userOrganizationRepository) {
|
||||
this.taskRepository = taskRepository;
|
||||
this.projectRepository = projectRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userOrganizationRepository = userOrganizationRepository;
|
||||
}
|
||||
|
||||
public CreateTaskOutput execute(UpdateTaskInput input) {
|
||||
LOGGER.infof("Updating task %d requested by user %s", input.id(), input.userEmail());
|
||||
Task task = taskRepository.findTaskById(input.id());
|
||||
|
||||
if (Objects.isNull(task.getUser())) {
|
||||
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());
|
||||
}));
|
||||
}
|
||||
validateRequesterMembership(input.organizationId(), input.userEmail());
|
||||
|
||||
task.setTitle(input.title());
|
||||
task.setDescription(input.description());
|
||||
|
|
@ -67,6 +67,8 @@ public class UpdateTaskUseCase {
|
|||
task.setCategory(null);
|
||||
}
|
||||
|
||||
task.setUser(resolveAssignee(input.organizationId(), input.assigneeUserId()));
|
||||
|
||||
if (Objects.nonNull(input.history())) {
|
||||
List<TaskHistoryItem> updatedHistory = input.history().stream().map(hInput -> {
|
||||
if (Objects.nonNull(hInput.id())) {
|
||||
|
|
@ -101,4 +103,46 @@ public class UpdateTaskUseCase {
|
|||
LOGGER.infof("Task %d updated successfully", input.id());
|
||||
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,
|
||||
PlusCircle,
|
||||
Send,
|
||||
Users,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
|
|
@ -22,12 +23,18 @@ import { useAuth } from "../contexts/AuthContext";
|
|||
import { useEffect } from "react";
|
||||
import { projectService, type Project } from "../services/projectService";
|
||||
import { categoryService } from "../services/categoryService";
|
||||
import {
|
||||
organizationService,
|
||||
type OrganizationMember,
|
||||
} from "../services/organizationService";
|
||||
import type { Category } from "../types/category";
|
||||
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;
|
||||
categoryId: string;
|
||||
assigneeUserId: string;
|
||||
history: TaskHistoryItem[];
|
||||
};
|
||||
|
||||
|
|
@ -37,6 +44,7 @@ const initialFormState: FormState = {
|
|||
status: "NOT_STARTED",
|
||||
projectId: "",
|
||||
categoryId: "",
|
||||
assigneeUserId: "",
|
||||
history: [],
|
||||
};
|
||||
|
||||
|
|
@ -47,7 +55,9 @@ export function CreateTaskPage() {
|
|||
const [createdTask, setCreatedTask] = useState<TaskResponse | null>(null);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
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 navigate = useNavigate();
|
||||
const isEdit = !!id;
|
||||
|
|
@ -63,8 +73,10 @@ export function CreateTaskPage() {
|
|||
status: data.status,
|
||||
projectId: data.project?.id?.toString() || "",
|
||||
categoryId: data.category?.id?.toString() || "",
|
||||
assigneeUserId: data.user?.id?.toString() || "",
|
||||
history: data.history || [],
|
||||
});
|
||||
setAssignToLoggedUser(Boolean(user?.email && data.user?.email === user.email));
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar tarefa:", error);
|
||||
setErrorMessage("Não foi possível carregar os dados da tarefa.");
|
||||
|
|
@ -72,7 +84,7 @@ export function CreateTaskPage() {
|
|||
}
|
||||
loadTask();
|
||||
}
|
||||
}, [id, isEdit]);
|
||||
}, [id, isEdit, user?.email]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadProjects() {
|
||||
|
|
@ -93,10 +105,30 @@ export function CreateTaskPage() {
|
|||
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();
|
||||
loadCategories();
|
||||
loadMembers();
|
||||
}, [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(
|
||||
event: ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
|
|
@ -115,12 +147,18 @@ export function CreateTaskPage() {
|
|||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
if (!activeOrg) {
|
||||
throw new Error("Nenhuma organização ativa selecionada.");
|
||||
}
|
||||
|
||||
const payload = {
|
||||
title: form.title.trim(),
|
||||
description: form.description.trim(),
|
||||
status: form.status,
|
||||
organizationId: activeOrg.id,
|
||||
projectId: form.projectId ? Number(form.projectId) : null,
|
||||
categoryId: form.categoryId ? Number(form.categoryId) : null,
|
||||
assigneeUserId: form.assigneeUserId ? Number(form.assigneeUserId) : null,
|
||||
history: form.history,
|
||||
};
|
||||
|
||||
|
|
@ -131,6 +169,7 @@ export function CreateTaskPage() {
|
|||
const response = await taskService.createTask(payload);
|
||||
setCreatedTask(response);
|
||||
setForm(initialFormState);
|
||||
setAssignToLoggedUser(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(
|
||||
|
|
@ -198,6 +237,10 @@ export function CreateTaskPage() {
|
|||
);
|
||||
}
|
||||
|
||||
const selectedAssignee = members.find(
|
||||
(member) => member.userId === Number(form.assigneeUserId),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl pb-20">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
|
|
@ -324,6 +367,69 @@ export function CreateTaskPage() {
|
|||
</label>
|
||||
</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 */}
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ export interface UserJoinRequest {
|
|||
status: 'PENDING' | 'ACTIVE' | 'REJECTED'
|
||||
}
|
||||
|
||||
export interface OrganizationMember {
|
||||
userId: number
|
||||
userName: string
|
||||
userEmail: string
|
||||
userPhoto?: string | null
|
||||
}
|
||||
|
||||
export const organizationService = {
|
||||
create: async (name: string): Promise<Organization> => {
|
||||
return apiFetch('/organizations', {
|
||||
|
|
@ -46,6 +53,10 @@ export const organizationService = {
|
|||
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[]> => {
|
||||
return apiFetch<UserJoinRequest[]>('/organizations/my-requests')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ export const taskService = {
|
|||
async createTask(task: {
|
||||
title: string
|
||||
description?: string
|
||||
organizationId: number
|
||||
projectId?: number | null
|
||||
categoryId?: number | null
|
||||
assigneeUserId?: number | null
|
||||
status?: TaskStatus
|
||||
}): Promise<TaskResponse> {
|
||||
return apiFetch<TaskResponse>('/tasks', {
|
||||
|
|
@ -59,7 +61,12 @@ export const taskService = {
|
|||
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}`, {
|
||||
method: 'PUT',
|
||||
body: task,
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ export type CreateTaskPayload = {
|
|||
title: string
|
||||
description: string
|
||||
status: TaskStatus
|
||||
organizationId: number
|
||||
projectId: number | null
|
||||
categoryId: number | null
|
||||
assigneeUserId: number | null
|
||||
}
|
||||
|
||||
export type TaskHistoryItem = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue