editar task
This commit is contained in:
parent
880d72d5d6
commit
4582663fc8
20 changed files with 324 additions and 20 deletions
|
|
@ -4,10 +4,19 @@ import java.time.LocalDateTime;
|
|||
import net.beetechgroup.beetask.entities.User;
|
||||
|
||||
public class TaskHistoryItem {
|
||||
private Long id;
|
||||
private LocalDateTime startAt;
|
||||
private LocalDateTime endAt;
|
||||
private User user;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setEndAt(LocalDateTime endAt) {
|
||||
this.endAt = endAt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import net.beetechgroup.beetask.usecase.task.listall.ListAllTasksUseCase;
|
|||
import net.beetechgroup.beetask.usecase.task.start.StartTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.stop.StopTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.update.UpdateTaskStatusUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.update.UpdateTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.get.GetTaskUseCase;
|
||||
|
||||
@ApplicationScoped
|
||||
public class TaskUseCaseConfig {
|
||||
|
|
@ -38,4 +40,14 @@ public class TaskUseCaseConfig {
|
|||
public UpdateTaskStatusUseCase updateTaskStatusUseCase(TaskRepository taskRepository) {
|
||||
return new UpdateTaskStatusUseCase(taskRepository);
|
||||
}
|
||||
|
||||
@Produces
|
||||
public UpdateTaskUseCase updateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository) {
|
||||
return new UpdateTaskUseCase(taskRepository, projectRepository);
|
||||
}
|
||||
|
||||
@Produces
|
||||
public GetTaskUseCase getTaskUseCase(TaskRepository taskRepository) {
|
||||
return new GetTaskUseCase(taskRepository);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import net.beetechgroup.beetask.frameworks.persistence.entities.TaskHistoryItemE
|
|||
public class TaskHistoryItemMapper {
|
||||
public static TaskHistoryItemEntity toEntity(TaskHistoryItem item) {
|
||||
TaskHistoryItemEntity entity = new TaskHistoryItemEntity();
|
||||
entity.setId(item.getId());
|
||||
entity.setStartAt(item.getStartAt());
|
||||
entity.setEndAt(item.getEndAt());
|
||||
if (item.getUser() != null) {
|
||||
|
|
@ -18,6 +19,7 @@ public class TaskHistoryItemMapper {
|
|||
if (entity == null) return null;
|
||||
|
||||
TaskHistoryItem item = new TaskHistoryItem();
|
||||
item.setId(entity.getId());
|
||||
item.setStartAt(entity.getStartAt());
|
||||
item.setEndAt(entity.getEndAt());
|
||||
if (entity.getUser() != null) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package net.beetechgroup.beetask.interfaceadapters.controllers.task;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.beetechgroup.beetask.entities.task.TaskStatus;
|
||||
|
||||
public record CreateTaskRequest(
|
||||
String title,
|
||||
String description,
|
||||
TaskStatus status,
|
||||
Long projectId) {
|
||||
Long projectId,
|
||||
List<TaskHistoryItemResponse> history) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public record CreateTaskResponse(
|
|||
}
|
||||
|
||||
record TaskHistoryItemResponse(
|
||||
Long id,
|
||||
LocalDateTime startAt,
|
||||
LocalDateTime endAt) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import net.beetechgroup.beetask.usecase.task.listall.ListAllTasksUseCase;
|
|||
import net.beetechgroup.beetask.usecase.task.start.StartTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.stop.StopTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.update.UpdateTaskStatusUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.update.UpdateTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.task.get.GetTaskUseCase;
|
||||
|
||||
@Path("/tasks")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
@ -49,6 +51,12 @@ public class TaskController {
|
|||
@Inject
|
||||
UpdateTaskStatusUseCase updateTaskStatusUseCase;
|
||||
|
||||
@Inject
|
||||
UpdateTaskUseCase updateTaskUseCase;
|
||||
|
||||
@Inject
|
||||
GetTaskUseCase getTaskUseCase;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
|
|
@ -61,6 +69,23 @@ public class TaskController {
|
|||
return TaskControllerMapper.toCreateTaskResponse(output);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Operation(summary = "Update a task", description = "Updates an existing task in the system")
|
||||
@APIResponse(responseCode = "200", description = "Task updated successfully")
|
||||
public CreateTaskResponse updateTask(@PathParam("id") Long id, CreateTaskRequest request) {
|
||||
CreateTaskOutput output = updateTaskUseCase.execute(TaskControllerMapper.toUpdateTaskInput(id, request));
|
||||
return TaskControllerMapper.toCreateTaskResponse(output);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Operation(summary = "Get a task", description = "Gets a specific task by ID")
|
||||
@APIResponse(responseCode = "200", description = "Task retrieved successfully")
|
||||
public CreateTaskResponse getTask(@PathParam("id") Long id) {
|
||||
return TaskControllerMapper.toCreateTaskResponse(getTaskUseCase.execute(id));
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{id}/start")
|
||||
@Operation(summary = "Start a task", description = "Starts a task in the system")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import net.beetechgroup.beetask.usecase.task.create.CreateTaskInput;
|
|||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
|
||||
import net.beetechgroup.beetask.usecase.task.start.StartTaskInput;
|
||||
import net.beetechgroup.beetask.usecase.task.stop.StopTaskInput;
|
||||
import net.beetechgroup.beetask.usecase.task.update.TaskHistoryItemInput;
|
||||
import net.beetechgroup.beetask.usecase.task.update.UpdateTaskInput;
|
||||
|
||||
public class TaskControllerMapper {
|
||||
|
||||
|
|
@ -24,7 +26,7 @@ public class TaskControllerMapper {
|
|||
output.status(),
|
||||
output.projectName(),
|
||||
output.finishedAt(),
|
||||
output.history().stream().map(taskHistoryItemOutput -> new TaskHistoryItemResponse(taskHistoryItemOutput.startAt(), taskHistoryItemOutput.endAt())).toList()
|
||||
output.history().stream().map(taskHistoryItemOutput -> new TaskHistoryItemResponse(taskHistoryItemOutput.id(), taskHistoryItemOutput.startAt(), taskHistoryItemOutput.endAt())).toList()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -35,4 +37,17 @@ public class TaskControllerMapper {
|
|||
public static StopTaskInput toStopTaskInput(Long id) {
|
||||
return new StopTaskInput(id);
|
||||
}
|
||||
|
||||
public static UpdateTaskInput toUpdateTaskInput(Long id, CreateTaskRequest request) {
|
||||
return new UpdateTaskInput(
|
||||
id,
|
||||
request.title(),
|
||||
request.description(),
|
||||
request.status(),
|
||||
request.projectId(),
|
||||
request.history() != null ? request.history().stream()
|
||||
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
|
||||
.toList() : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public class CreateTaskMapper {
|
|||
|
||||
private static TaskHistoryItemOutput toTaskHistoryItemOutput(TaskHistoryItem taskHistoryItem) {
|
||||
return new TaskHistoryItemOutput(
|
||||
taskHistoryItem.getId(),
|
||||
taskHistoryItem.getStartAt(),
|
||||
taskHistoryItem.getEndAt());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package net.beetechgroup.beetask.usecase.task.create;
|
|||
import java.time.LocalDateTime;
|
||||
|
||||
public record TaskHistoryItemOutput(
|
||||
Long id,
|
||||
LocalDateTime startAt,
|
||||
LocalDateTime endAt) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package net.beetechgroup.beetask.usecase.task.get;
|
||||
|
||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper;
|
||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
|
||||
|
||||
public class GetTaskUseCase {
|
||||
private final TaskRepository taskRepository;
|
||||
|
||||
public GetTaskUseCase(TaskRepository taskRepository) {
|
||||
this.taskRepository = taskRepository;
|
||||
}
|
||||
|
||||
public CreateTaskOutput execute(Long id) {
|
||||
//TODO deve validar se o usuario logado é o dono da task
|
||||
return CreateTaskMapper.toCreateTaskOutput(taskRepository.findTaskById(id));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package net.beetechgroup.beetask.usecase.task.update;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record TaskHistoryItemInput(
|
||||
Long id,
|
||||
LocalDateTime startAt,
|
||||
LocalDateTime endAt) {
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package net.beetechgroup.beetask.usecase.task.update;
|
||||
|
||||
import java.util.List;
|
||||
import net.beetechgroup.beetask.entities.task.TaskStatus;
|
||||
|
||||
public record UpdateTaskInput(
|
||||
Long id,
|
||||
String title,
|
||||
String description,
|
||||
TaskStatus status,
|
||||
Long projectId,
|
||||
List<TaskHistoryItemInput> history) {
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package net.beetechgroup.beetask.usecase.task.update;
|
||||
|
||||
import java.util.List;
|
||||
import net.beetechgroup.beetask.entities.Project;
|
||||
import net.beetechgroup.beetask.entities.task.Task;
|
||||
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
||||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper;
|
||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
|
||||
|
||||
public class UpdateTaskUseCase {
|
||||
private final TaskRepository taskRepository;
|
||||
private final ProjectRepository projectRepository;
|
||||
|
||||
public UpdateTaskUseCase(TaskRepository taskRepository, ProjectRepository projectRepository) {
|
||||
this.taskRepository = taskRepository;
|
||||
this.projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
public CreateTaskOutput execute(UpdateTaskInput input) {
|
||||
//TODO deve validar se o usuario logado é o dono da task
|
||||
Task task = taskRepository.findTaskById(input.id());
|
||||
|
||||
task.setTitle(input.title());
|
||||
task.setDescription(input.description());
|
||||
task.setStatus(input.status());
|
||||
|
||||
if (input.projectId() != null) {
|
||||
Project project = projectRepository.findProjectById(input.projectId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Projeto não encontrado com ID: " + input.projectId()));
|
||||
task.setProject(project);
|
||||
} else {
|
||||
task.setProject(null);
|
||||
}
|
||||
|
||||
if (input.history() != null) {
|
||||
List<TaskHistoryItem> updatedHistory = input.history().stream().map(hInput -> {
|
||||
if (hInput.id() != null) {
|
||||
return task.getHistory().stream()
|
||||
.filter(existing -> existing.getId().equals(hInput.id()))
|
||||
.findFirst()
|
||||
.map(existing -> {
|
||||
existing.setStartAt(hInput.startAt());
|
||||
existing.setEndAt(hInput.endAt());
|
||||
return existing;
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
TaskHistoryItem newItem = new TaskHistoryItem();
|
||||
newItem.setStartAt(hInput.startAt());
|
||||
newItem.setEndAt(hInput.endAt());
|
||||
return newItem;
|
||||
});
|
||||
} else {
|
||||
TaskHistoryItem newItem = new TaskHistoryItem();
|
||||
newItem.setStartAt(hInput.startAt());
|
||||
newItem.setEndAt(hInput.endAt());
|
||||
return newItem;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
task.getHistory().clear();
|
||||
task.getHistory().addAll(updatedHistory);
|
||||
}
|
||||
|
||||
return CreateTaskMapper.toCreateTaskOutput(taskRepository.saveTask(task));
|
||||
}
|
||||
}
|
||||
|
|
@ -71,7 +71,8 @@ export function TaskTimer({ task, onUpdate }: TaskTimerProps) {
|
|||
}
|
||||
}, [runningItem, accumulatedSeconds])
|
||||
|
||||
async function handleStart() {
|
||||
async function handleStart(e: React.MouseEvent) {
|
||||
e.stopPropagation()
|
||||
setIsActionLoading(true)
|
||||
try {
|
||||
await taskService.startTask(task.id)
|
||||
|
|
@ -83,7 +84,8 @@ export function TaskTimer({ task, onUpdate }: TaskTimerProps) {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleStop() {
|
||||
async function handleStop(e: React.MouseEvent) {
|
||||
e.stopPropagation()
|
||||
setIsActionLoading(true)
|
||||
try {
|
||||
await taskService.stopTask(task.id)
|
||||
|
|
|
|||
|
|
@ -2,30 +2,36 @@ import { motion } from "framer-motion";
|
|||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
FileText,
|
||||
Layout,
|
||||
PlusCircle,
|
||||
Send,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ChangeEvent, FormEvent } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { taskService } from "../services/taskService";
|
||||
import { cn } from "../lib/utils";
|
||||
import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS } from "../types/task";
|
||||
import type { CreateTaskPayload, TaskResponse } from "../types/task";
|
||||
import type { CreateTaskPayload, TaskResponse, TaskHistoryItem } from "../types/task";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { useEffect } from "react";
|
||||
import { projectService, type Project } from "../services/projectService";
|
||||
|
||||
type FormState = Omit<CreateTaskPayload, 'projectId'> & { projectId: string };
|
||||
type FormState = Omit<CreateTaskPayload, 'projectId'> & {
|
||||
projectId: string;
|
||||
history: TaskHistoryItem[];
|
||||
};
|
||||
|
||||
const initialFormState: FormState = {
|
||||
title: "",
|
||||
description: "",
|
||||
status: "NOT_STARTED",
|
||||
projectId: "",
|
||||
history: [],
|
||||
};
|
||||
|
||||
export function CreateTaskPage() {
|
||||
|
|
@ -35,6 +41,30 @@ export function CreateTaskPage() {
|
|||
const [createdTask, setCreatedTask] = useState<TaskResponse | null>(null);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const { activeOrg } = useAuth();
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const isEdit = !!id;
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
async function loadTask() {
|
||||
try {
|
||||
const data = await taskService.getTask(Number(id));
|
||||
setForm({
|
||||
title: data.title,
|
||||
description: data.description || "",
|
||||
status: data.status,
|
||||
projectId: data.projectId?.toString() || "",
|
||||
history: data.history || [],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar tarefa:", error);
|
||||
setErrorMessage("Não foi possível carregar os dados da tarefa.");
|
||||
}
|
||||
}
|
||||
loadTask();
|
||||
}
|
||||
}, [id, isEdit]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadProjects() {
|
||||
|
|
@ -67,17 +97,22 @@ export function CreateTaskPage() {
|
|||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const payload: CreateTaskPayload = {
|
||||
const payload = {
|
||||
title: form.title.trim(),
|
||||
description: form.description.trim(),
|
||||
status: form.status,
|
||||
projectId: form.projectId ? Number(form.projectId) : null,
|
||||
history: form.history,
|
||||
};
|
||||
|
||||
const response = await taskService.createTask(payload);
|
||||
|
||||
setCreatedTask(response);
|
||||
setForm(initialFormState);
|
||||
if (isEdit) {
|
||||
await taskService.updateTask(Number(id), payload);
|
||||
navigate("/");
|
||||
} else {
|
||||
const response = await taskService.createTask(payload);
|
||||
setCreatedTask(response);
|
||||
setForm(initialFormState);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(
|
||||
error instanceof Error
|
||||
|
|
@ -168,10 +203,10 @@ export function CreateTaskPage() {
|
|||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black tracking-tight">
|
||||
Nova Tarefa
|
||||
{isEdit ? "Editar Tarefa" : "Nova Tarefa"}
|
||||
</h1>
|
||||
<p className="text-sm font-medium text-white/80">
|
||||
Preencha os detalhes abaixo para começar.
|
||||
{isEdit ? `Editando tarefa #${id}` : "Preencha os detalhes abaixo para começar."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -254,6 +289,76 @@ export function CreateTaskPage() {
|
|||
className="w-full rounded-2xl border border-border-soft bg-app-bg px-5 py-4 text-sm font-medium outline-none transition-all focus:border-brand focus:ring-4 focus:ring-brand/10"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* History Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm font-bold text-text-main">
|
||||
<Clock size={16} />
|
||||
Histórico de Execução
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
history: [...prev.history, { startAt: new Date().toISOString(), endAt: null }]
|
||||
}))
|
||||
}}
|
||||
className="text-xs font-bold text-brand hover:underline"
|
||||
>
|
||||
+ Adicionar Registro Manual
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{form.history.map((item, index) => (
|
||||
<div key={index} className="flex flex-wrap items-end gap-4 rounded-xl border border-border-soft bg-app-bg/50 p-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-[10px] font-black uppercase text-text-muted">Início</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={item.startAt ? item.startAt.slice(0, 16) : ""}
|
||||
onChange={(e) => {
|
||||
const newHistory = [...form.history];
|
||||
newHistory[index] = { ...item, startAt: new Date(e.target.value).toISOString() };
|
||||
setForm(prev => ({ ...prev, history: newHistory }));
|
||||
}}
|
||||
className="w-full rounded-lg border border-border-soft bg-surface px-3 py-2 text-xs outline-none focus:border-brand"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-[10px] font-black uppercase text-text-muted">Fim</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={item.endAt ? item.endAt.slice(0, 16) : ""}
|
||||
onChange={(e) => {
|
||||
const newHistory = [...form.history];
|
||||
newHistory[index] = { ...item, endAt: e.target.value ? new Date(e.target.value).toISOString() : null };
|
||||
setForm(prev => ({ ...prev, history: newHistory }));
|
||||
}}
|
||||
className="w-full rounded-lg border border-border-soft bg-surface px-3 py-2 text-xs outline-none focus:border-brand"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
history: prev.history.filter((_, i) => i !== index)
|
||||
}))
|
||||
}}
|
||||
className="mb-1 rounded-lg p-2 text-danger hover:bg-danger-soft"
|
||||
>
|
||||
<XCircle size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{form.history.length === 0 && (
|
||||
<p className="py-4 text-center text-xs font-medium text-text-muted">Nenhum registro de execução encontrado.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
|
|
@ -282,11 +387,11 @@ export function CreateTaskPage() {
|
|||
)}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
"Criando..."
|
||||
isEdit ? "Salvando..." : "Criando..."
|
||||
) : (
|
||||
<>
|
||||
<Send size={18} />
|
||||
Criar Tarefa
|
||||
{isEdit ? "Salvar Alterações" : "Criar Tarefa"}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { taskService } from '../services/taskService'
|
||||
import { cn } from '../lib/utils'
|
||||
|
|
@ -50,6 +50,7 @@ import { TaskTimer } from '../components/TaskTimer'
|
|||
export function TaskBoardPage() {
|
||||
const [tasks, setTasks] = useState<TaskResponse[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const loadTasks = async (silent = false) => {
|
||||
if (!silent) setIsLoading(true)
|
||||
|
|
@ -169,8 +170,9 @@ export function TaskBoardPage() {
|
|||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
onClick={() => navigate(`/edit/${task.id}`)}
|
||||
className={cn(
|
||||
'group rounded-2xl border border-border-soft bg-surface p-4 shadow-sm transition-all',
|
||||
'group cursor-pointer rounded-2xl border border-border-soft bg-surface p-4 shadow-sm transition-all',
|
||||
snapshot.isDragging ? 'shadow-2xl ring-2 ring-brand/20' : 'hover:border-brand/30',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { taskService } from '../services/taskService'
|
||||
import { cn } from '../lib/utils'
|
||||
|
|
@ -55,6 +55,7 @@ export function TaskListPage() {
|
|||
const [tasks, setTasks] = useState<TaskResponse[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const navigate = useNavigate()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string | 'ALL'>('ALL')
|
||||
|
||||
|
|
@ -178,7 +179,8 @@ export function TaskListPage() {
|
|||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2, delay: i * 0.05 }}
|
||||
key={task.id}
|
||||
className="group relative flex flex-col rounded-[2rem] border border-border-soft bg-surface p-6 shadow-sm transition-all hover:shadow-xl hover:shadow-brand/5"
|
||||
onClick={() => navigate(`/edit/${task.id}`)}
|
||||
className="group relative flex cursor-pointer flex-col rounded-[2rem] border border-border-soft bg-surface p-6 shadow-sm transition-all hover:shadow-xl hover:shadow-brand/5"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ export const router = createBrowserRouter([
|
|||
path: "new",
|
||||
element: <CreateTaskPage />,
|
||||
},
|
||||
{
|
||||
path: "edit/:id",
|
||||
element: <CreateTaskPage />,
|
||||
},
|
||||
{
|
||||
path: "board",
|
||||
element: <TaskBoardPage />,
|
||||
|
|
|
|||
|
|
@ -31,4 +31,15 @@ export const taskService = {
|
|||
method: 'PUT',
|
||||
})
|
||||
},
|
||||
|
||||
async getTask(id: number): Promise<TaskResponse> {
|
||||
return apiFetch<TaskResponse>(`/tasks/${id}`)
|
||||
},
|
||||
|
||||
async updateTask(id: number, task: Partial<TaskResponse>): Promise<TaskResponse> {
|
||||
return apiFetch<TaskResponse>(`/tasks/${id}`, {
|
||||
method: 'PUT',
|
||||
body: task,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export type CreateTaskPayload = {
|
|||
}
|
||||
|
||||
export type TaskHistoryItem = {
|
||||
id?: number
|
||||
startAt: string
|
||||
endAt: string | null
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue