diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/Task.java b/backend/src/main/java/net/beetechgroup/beetask/entities/Task.java index 8fcbde5..25fde3c 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/Task.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/Task.java @@ -1,6 +1,5 @@ package net.beetechgroup.beetask.entities; -import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -95,10 +94,5 @@ public class Task { current.setEndAt(LocalDateTime.now()); } -public Duration getTotalTime() { - return history.stream() - .map(TaskHistoryItem::getDuration) - .reduce(Duration.ZERO, Duration::plus); - } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/TaskHistoryItem.java b/backend/src/main/java/net/beetechgroup/beetask/entities/TaskHistoryItem.java index cd63d2b..ce7385b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/TaskHistoryItem.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/TaskHistoryItem.java @@ -1,6 +1,5 @@ package net.beetechgroup.beetask.entities; -import java.time.Duration; import java.time.LocalDateTime; public class TaskHistoryItem { @@ -23,13 +22,6 @@ public class TaskHistoryItem { return endAt; } - public Duration getDuration() { - if (endAt == null) { - return Duration.between(startAt, LocalDateTime.now()); - } - return Duration.between(startAt, endAt); - } - public boolean isRunning() { return endAt == null; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java index 79aad3c..8cfa55b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/TaskUseCaseConfig.java @@ -4,8 +4,10 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import net.beetechgroup.beetask.usecase.repository.TaskRepository; import net.beetechgroup.beetask.usecase.task.create.CreateTaskUseCase; +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; @ApplicationScoped public class TaskUseCaseConfig { @@ -24,4 +26,14 @@ public class TaskUseCaseConfig { public StopTaskUseCase stopTaskUseCase(TaskRepository taskRepository) { return new StopTaskUseCase(taskRepository); } + + @Produces + public ListAllTasksUseCase listAllTasksUseCase(TaskRepository taskRepository) { + return new ListAllTasksUseCase(taskRepository); + } + + @Produces + public UpdateTaskStatusUseCase updateTaskStatusUseCase(TaskRepository taskRepository) { + return new UpdateTaskStatusUseCase(taskRepository); + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java index f2c9842..624f0cb 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java @@ -1,8 +1,9 @@ package net.beetechgroup.beetask.frameworks.persistence.repository; +import java.util.List; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import net.beetechgroup.beetask.entities.Task; import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity; @@ -11,29 +12,29 @@ import net.beetechgroup.beetask.usecase.exceptions.TaskNotFoundException; import net.beetechgroup.beetask.usecase.repository.TaskRepository; @ApplicationScoped -public class TaskRepositoryImpl implements TaskRepository { - - @Inject - EntityManager em; +public class TaskRepositoryImpl implements TaskRepository, PanacheRepository { @Override @Transactional - public Task save(Task task) { + public Task saveTask(Task task) { TaskEntity entity = TaskMapper.toEntity(task); if (entity.getId() == null) { - em.persist(entity); + persist(entity); } else { - entity = em.merge(entity); + entity = getEntityManager().merge(entity); } return TaskMapper.toDomain(entity); } @Override - public Task findById(Long id) { - TaskEntity entity = em.find(TaskEntity.class, id); - if (entity == null) { - throw new TaskNotFoundException("Task not found"); - } + public Task findTaskById(Long id) { + TaskEntity entity = find("id = ?1", id).firstResultOptional() + .orElseThrow(() -> new TaskNotFoundException(id)); return TaskMapper.toDomain(entity); } + + @Override + public List findAllTasks() { + return findAll().list().stream().map(TaskMapper::toDomain).toList(); + } } \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/TaskController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/TaskController.java index e9e6ed2..fed9093 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/TaskController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/TaskController.java @@ -2,22 +2,30 @@ package net.beetechgroup.beetask.interfaceadapters.controllers; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; + +import java.util.List; + import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import net.beetechgroup.beetask.entities.Task; +import net.beetechgroup.beetask.entities.TaskStatus; import net.beetechgroup.beetask.interfaceadapters.dtos.TaskDTOMapper; import net.beetechgroup.beetask.interfaceadapters.dtos.TaskRequestDTO; import net.beetechgroup.beetask.interfaceadapters.dtos.TaskResponseDTO; import net.beetechgroup.beetask.usecase.task.create.CreateTaskUseCase; +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; @Path("/tasks") @Produces(MediaType.APPLICATION_JSON) @@ -34,6 +42,12 @@ public class TaskController { @Inject StopTaskUseCase stopTaskUseCase; + @Inject + ListAllTasksUseCase listAllTasksUseCase; + + @Inject + UpdateTaskStatusUseCase updateTaskStatusUseCase; + @POST @Operation(summary = "Create a new task", description = "Creates a new task in the system") @APIResponse(responseCode = "201", description = "Task created successfully") @@ -58,6 +72,22 @@ public class TaskController { public TaskResponseDTO stopTask(@PathParam("id") Long id) { Task task = stopTaskUseCase.execute(TaskDTOMapper.toStopTaskInput(id)); return TaskDTOMapper.toTaskResponseDTO(task); - } + } + + @PATCH + @Path("/{id}/status") + @Operation(summary = "Update task status", description = "Updates the status of a task") + @APIResponse(responseCode = "200", description = "Task status updated successfully") + public TaskResponseDTO updateTaskStatus(@PathParam("id") Long id, TaskStatus status) { + Task task = updateTaskStatusUseCase.execute(id, status); + return TaskDTOMapper.toTaskResponseDTO(task); + } + + @GET + @Operation(summary = "List all tasks", description = "Lists all tasks in the system") + @APIResponse(responseCode = "200", description = "Tasks listed successfully") + public List listTasks() { + return listAllTasksUseCase.execute().stream().map(TaskDTOMapper::toTaskResponseDTO).toList(); + } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskDTOMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskDTOMapper.java index a7f2da3..8fc31ab 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskDTOMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskDTOMapper.java @@ -1,6 +1,7 @@ package net.beetechgroup.beetask.interfaceadapters.dtos; import net.beetechgroup.beetask.entities.Task; +import net.beetechgroup.beetask.entities.TaskHistoryItem; import net.beetechgroup.beetask.usecase.task.create.CreateTaskInput; import net.beetechgroup.beetask.usecase.task.start.StartTaskInput; import net.beetechgroup.beetask.usecase.task.stop.StopTaskInput; @@ -23,10 +24,14 @@ public class TaskDTOMapper { task.getDescription(), task.getStatus(), task.getProject(), - task.getHistory() + task.getHistory().stream().map(TaskDTOMapper::toTaskHistoryItemDTO).toList() ); } + private static TaskHistoryItemDTO toTaskHistoryItemDTO(TaskHistoryItem taskHistoryItem) { + return new TaskHistoryItemDTO(taskHistoryItem.getStartAt(), taskHistoryItem.getEndAt()); + } + public static StartTaskInput toStartTaskInput(Long id) { return new StartTaskInput(id); } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskHistoryItemDTO.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskHistoryItemDTO.java new file mode 100644 index 0000000..68b2d4c --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskHistoryItemDTO.java @@ -0,0 +1,7 @@ +package net.beetechgroup.beetask.interfaceadapters.dtos; + +import java.time.LocalDateTime; + +public record TaskHistoryItemDTO(LocalDateTime startAt, LocalDateTime endAt) { + +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskRequestDTO.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskRequestDTO.java index e181cd1..6deb0a5 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskRequestDTO.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskRequestDTO.java @@ -3,9 +3,8 @@ package net.beetechgroup.beetask.interfaceadapters.dtos; import net.beetechgroup.beetask.entities.TaskStatus; public record TaskRequestDTO( - Long id, - String title, - String description, - TaskStatus status, - String project) { + String title, + String description, + TaskStatus status, + String project) { } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskResponseDTO.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskResponseDTO.java index e691779..456d9be 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskResponseDTO.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/dtos/TaskResponseDTO.java @@ -2,7 +2,6 @@ package net.beetechgroup.beetask.interfaceadapters.dtos; import java.util.List; -import net.beetechgroup.beetask.entities.TaskHistoryItem; import net.beetechgroup.beetask.entities.TaskStatus; public record TaskResponseDTO( @@ -11,5 +10,5 @@ public record TaskResponseDTO( String description, TaskStatus status, String project, - List history) { + List history) { } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/TaskNotFoundException.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/TaskNotFoundException.java index 1fb07c9..51952eb 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/TaskNotFoundException.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/exceptions/TaskNotFoundException.java @@ -1,7 +1,7 @@ package net.beetechgroup.beetask.usecase.exceptions; public class TaskNotFoundException extends RuntimeException { - public TaskNotFoundException(String message) { - super(message); + public TaskNotFoundException(Long id) { + super("Task not found with id: " + id); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java index e8c7168..0cfc28e 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java @@ -1,8 +1,11 @@ package net.beetechgroup.beetask.usecase.repository; +import java.util.List; + import net.beetechgroup.beetask.entities.Task; public interface TaskRepository { - Task save(Task task); - Task findById(Long id); + Task saveTask(Task task); + Task findTaskById(Long id); + List findAllTasks(); } \ No newline at end of file diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskUseCase.java index 93153bb..cf99390 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/create/CreateTaskUseCase.java @@ -16,6 +16,6 @@ public class CreateTaskUseCase { task.setDescription(input.description()); task.setStatus(input.status()); task.setProject(input.project()); - return taskRepository.save(task); + return taskRepository.saveTask(task); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListAllTasksUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListAllTasksUseCase.java new file mode 100644 index 0000000..21c048b --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListAllTasksUseCase.java @@ -0,0 +1,19 @@ +package net.beetechgroup.beetask.usecase.task.listall; + +import net.beetechgroup.beetask.entities.Task; +import net.beetechgroup.beetask.usecase.repository.TaskRepository; + +import java.util.List; + +public class ListAllTasksUseCase { + + private final TaskRepository taskRepository; + + public ListAllTasksUseCase(TaskRepository taskRepository) { + this.taskRepository = taskRepository; + } + + public List execute() { + return taskRepository.findAllTasks(); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java index 3d2ffae..cc8d032 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/start/StartTaskUseCase.java @@ -12,8 +12,8 @@ public class StartTaskUseCase { } public Task execute(StartTaskInput input) { - Task task = taskRepository.findById(input.id()); + Task task = taskRepository.findTaskById(input.id()); task.start(); - return taskRepository.save(task); + return taskRepository.saveTask(task); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/stop/StopTaskUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/stop/StopTaskUseCase.java index 6bc2b8d..fab9167 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/stop/StopTaskUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/stop/StopTaskUseCase.java @@ -12,8 +12,8 @@ public class StopTaskUseCase { } public Task execute(StopTaskInput input) { - Task task = taskRepository.findById(input.id()); + Task task = taskRepository.findTaskById(input.id()); task.stop(); - return taskRepository.save(task); + return taskRepository.saveTask(task); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/update/UpdateTaskStatusUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/update/UpdateTaskStatusUseCase.java new file mode 100644 index 0000000..0cb4140 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/update/UpdateTaskStatusUseCase.java @@ -0,0 +1,24 @@ +package net.beetechgroup.beetask.usecase.task.update; + +import net.beetechgroup.beetask.entities.Task; +import net.beetechgroup.beetask.entities.TaskStatus; +import net.beetechgroup.beetask.usecase.repository.TaskRepository; + +public class UpdateTaskStatusUseCase { + + private final TaskRepository taskRepository; + + public UpdateTaskStatusUseCase(TaskRepository taskRepository) { + this.taskRepository = taskRepository; + } + + public Task execute(Long id, TaskStatus newStatus) { + Task task = taskRepository.findTaskById(id); + + // Simple status update + // In a more complex scenario, we might want to trigger start() or stop() logic here + task.setStatus(newStatus); + + return taskRepository.saveTask(task); + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aee72ed..71cdd31 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,10 +8,16 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@hello-pangea/dnd": "^18.0.1", "@tailwindcss/vite": "^4.2.4", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "framer-motion": "^12.38.0", + "lucide-react": "^1.14.0", "react": "^19.2.5", "react-dom": "^19.2.5", - "react-router-dom": "^7.9.4" + "react-router-dom": "^7.9.4", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -62,7 +68,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -224,6 +229,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -272,6 +286,27 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -410,6 +445,23 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1091,7 +1143,6 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1100,9 +1151,8 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1117,6 +1167,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", @@ -1162,7 +1218,6 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -1393,7 +1448,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1521,7 +1575,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1557,6 +1610,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1592,13 +1654,32 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1682,7 +1763,6 @@ "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1962,6 +2042,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2447,6 +2554,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2472,6 +2588,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2592,7 +2723,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2619,7 +2749,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2656,12 +2785,17 @@ "node": ">=6" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, "node_modules/react": { "version": "19.2.5", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2671,7 +2805,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2679,6 +2812,29 @@ "react": "^19.2.5" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.14.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", @@ -2717,6 +2873,12 @@ "react-dom": ">=18" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", @@ -2810,6 +2972,16 @@ "node": ">=0.10.0" } }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", @@ -2829,6 +3001,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -2862,8 +3040,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -2884,7 +3061,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2965,12 +3141,20 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "8.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3095,7 +3279,6 @@ "integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index 4d077fb..704dd40 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,16 @@ "preview": "vite preview" }, "dependencies": { + "@hello-pangea/dnd": "^18.0.1", "@tailwindcss/vite": "^4.2.4", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "framer-motion": "^12.38.0", + "lucide-react": "^1.14.0", "react": "^19.2.5", "react-dom": "^19.2.5", - "react-router-dom": "^7.9.4" + "react-router-dom": "^7.9.4", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx new file mode 100644 index 0000000..dd810b5 --- /dev/null +++ b/frontend/src/components/AppShell.tsx @@ -0,0 +1,91 @@ +import { Layout, ListTodo, PlusCircle, Columns } from 'lucide-react' +import { NavLink, Outlet } from 'react-router-dom' + +import { cn } from '../lib/utils' + +const navLinkClassName = ({ isActive }: { isActive: boolean }) => + cn( + 'inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-semibold transition-all duration-200', + isActive + ? 'bg-brand text-white shadow-md shadow-brand/20' + : 'text-text-muted hover:bg-surface-muted hover:text-text-main', + ) + +export function AppShell() { + return ( +
+ {/* Top Navigation */} +
+
+
+ +
+ +
+
+ + BeeTask + + + Workspace + +
+
+ + +
+ +
+
+
+
+ GM +
+ + Gabriel Menezes + +
+
+
+
+ + {/* Main Content */} +
+
+ +
+
+ + {/* Footer / Mobile Nav */} +
+ +
+
+ ) +} diff --git a/frontend/src/components/TaskTimer.tsx b/frontend/src/components/TaskTimer.tsx new file mode 100644 index 0000000..e0b264f --- /dev/null +++ b/frontend/src/components/TaskTimer.tsx @@ -0,0 +1,135 @@ +import { differenceInSeconds, parseISO } from 'date-fns' +import { Play, Square } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' + +import { taskService } from '../services/taskService' +import { cn } from '../lib/utils' +import type { TaskResponse } from '../types/task' + +interface TaskTimerProps { + task: TaskResponse + onUpdate: () => void +} + +function formatDuration(seconds: number) { + const hrs = Math.floor(seconds / 3600) + const mins = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + + return [ + hrs > 0 ? String(hrs).padStart(2, '0') : '00', + String(mins).padStart(2, '0'), + String(secs).padStart(2, '0'), + ].join(':') +} + +export function TaskTimer({ task, onUpdate }: TaskTimerProps) { + const [now, setNow] = useState(new Date()) + const [isActionLoading, setIsActionLoading] = useState(false) + + const history = task.history ?? [] + const runningItem = useMemo( + () => history.find((item) => !item.endAt), + [history], + ) + + const accumulatedSeconds = useMemo(() => { + return history.reduce((acc, item) => { + if (item.endAt) { + return acc + differenceInSeconds(parseISO(item.endAt), parseISO(item.startAt)) + } + return acc + }, 0) + }, [history]) + + const [displaySeconds, setDisplaySeconds] = useState(0) + + useEffect(() => { + let interval: NodeJS.Timeout | null = null + + if (runningItem) { + interval = setInterval(() => { + const runningSeconds = differenceInSeconds(new Date(), parseISO(runningItem.startAt)) + setDisplaySeconds(accumulatedSeconds + runningSeconds) + }, 1000) + } else { + setDisplaySeconds(accumulatedSeconds) + } + + return () => { + if (interval) clearInterval(interval) + } + }, [runningItem, accumulatedSeconds]) + + // Initial calculation + useEffect(() => { + if (runningItem) { + const runningSeconds = differenceInSeconds(new Date(), parseISO(runningItem.startAt)) + setDisplaySeconds(accumulatedSeconds + runningSeconds) + } else { + setDisplaySeconds(accumulatedSeconds) + } + }, [runningItem, accumulatedSeconds]) + + async function handleStart() { + setIsActionLoading(true) + try { + await taskService.startTask(task.id) + onUpdate() + } catch (error) { + console.error('Failed to start task', error) + } finally { + setIsActionLoading(false) + } + } + + async function handleStop() { + setIsActionLoading(true) + try { + await taskService.stopTask(task.id) + onUpdate() + } catch (error) { + console.error('Failed to stop task', error) + } finally { + setIsActionLoading(false) + } + } + + return ( +
+
+ + Tempo Gasto + + + {formatDuration(displaySeconds)} + +
+ +
+ {runningItem ? ( + + ) : ( + + )} +
+
+ ) +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..fed2fe9 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/frontend/src/pages/CreateTaskPage.tsx b/frontend/src/pages/CreateTaskPage.tsx index 0620999..35197ce 100644 --- a/frontend/src/pages/CreateTaskPage.tsx +++ b/frontend/src/pages/CreateTaskPage.tsx @@ -1,13 +1,20 @@ +import { motion } from "framer-motion"; +import { + ArrowLeft, + CheckCircle2, + FileText, + Layout, + PlusCircle, + Send, +} from "lucide-react"; import { useState } from "react"; import type { ChangeEvent, FormEvent } from "react"; +import { Link } from "react-router-dom"; -import { apiFetch } from "../lib/api"; -import { TASK_STATUS_OPTIONS } from "../types/task"; -import type { - CreateTaskPayload, - TaskResponse, - TaskStatus, -} from "../types/task"; +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"; type FormState = CreateTaskPayload; @@ -18,13 +25,6 @@ const initialFormState: FormState = { project: "", }; -const statusLabels: Record = { - NOT_STARTED: "Nao iniciada", - IN_PROGRESS: "Em andamento", - COMPLETED: "Concluida", - CANCELED: "Cancelada", -}; - export function CreateTaskPage() { const [form, setForm] = useState(initialFormState); const [isSubmitting, setIsSubmitting] = useState(false); @@ -56,10 +56,7 @@ export function CreateTaskPage() { project: form.project.trim(), }; - const response = await apiFetch("/tasks", { - method: "POST", - body: payload, - }); + const response = await taskService.createTask(payload); setCreatedTask(response); setForm(initialFormState); @@ -67,106 +64,211 @@ export function CreateTaskPage() { setErrorMessage( error instanceof Error ? error.message - : "Nao foi possivel criar a task.", + : "Não foi possível criar a task.", ); } finally { setIsSubmitting(false); } } + if (createdTask) { + return ( +
+ +
+ +
+

+ Tarefa Criada! +

+

+ Sua nova tarefa foi registrada com sucesso e já está disponível na + lista. +

+ +
+

+ #{createdTask.id} +

+

+ {createdTask.title} +

+
+ + {TASK_STATUS_LABELS[createdTask.status]} + + + {createdTask.project || "Geral"} + +
+
+ +
+ + Ver Minhas Tarefas + + +
+
+
+ ); + } + return ( -
-
-
-
- - Criar task +
+
+ + + Voltar para a lista + +
+ + +
+
+
+ +
+
+

+ Nova Tarefa +

+

+ Preencha os detalhes abaixo para começar. +

+
-
-
- + +
+ {/* Title Section */} +
+ +
-
+ + {/* Description Section */} +