Add optional assignee to task form

This commit is contained in:
Gabriel Santana de Menezes 2026-05-17 21:18:07 -04:00
parent 7c3ba32b3f
commit ae0d536ff7
15 changed files with 328 additions and 23 deletions

View file

@ -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);
}
}

View file

@ -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

View file

@ -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

View file

@ -8,7 +8,9 @@ public record CreateTaskRequest(
String title,
String description,
TaskStatus status,
Long organizationId,
Long projectId,
Long categoryId,
Long assigneeUserId,
List<TaskHistoryItemResponse> history) {
}

View file

@ -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()))

View file

@ -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;
}
}

View file

@ -0,0 +1,7 @@
package net.beetechgroup.beetask.usecase.organization.members;
public record OrganizationMemberOutput(
Long userId,
String userName,
String userEmail,
String userPhoto) {}

View file

@ -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) {
}

View file

@ -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);
});
}
}

View file

@ -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) {
}

View file

@ -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);
});
}
}

View file

@ -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">

View file

@ -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')
}

View file

@ -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,

View file

@ -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 = {