chore: enforce coding standards and add project guidelines

- Replace JPQL queries in TaskRepositoryImpl with Panache shorthand and Java filtering
- Replace == null / != null with Objects.isNull() / Objects.nonNull() across all layers
- Add CLAUDE.md with project conventions, architecture rules, and coding standards
- Add .github/PULL_REQUEST_TEMPLATE.md for standardized PR descriptions in Portuguese
- Add .claude/skills/new-task.md workflow guide (issue reading → branch → commit → PR)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gabriel Santana de Menezes 2026-05-09 15:57:40 -04:00
parent 6800688284
commit 1868d069c2
26 changed files with 363 additions and 75 deletions

View file

@ -0,0 +1,92 @@
---
trigger: manual
description: Workflow completo para iniciar e concluir uma nova tarefa a partir de uma issue do GitHub
---
# Workflow de Nova Tarefa
Siga estes passos em ordem para executar uma tarefa do início ao PR.
## Passo 1 — Ler a issue
```bash
gh issue view <número> --repo <owner/repo>
```
Leia o **título** e a **descrição** completa da issue antes de qualquer ação.
## Passo 2 — Criar a branch
Escolha o prefixo de acordo com o tipo de mudança:
| Tipo | Prefixo | Quando usar |
|------|---------|-------------|
| Nova funcionalidade | `feat/` | Algo novo que o usuário percebe |
| Correção de bug | `fix/` | Comportamento errado corrigido |
| Manutenção/chore | `chore/` | Refactor, config, padrões, docs |
| Teste | `test/` | Apenas adição/correção de testes |
Formato do nome: `<prefixo><descricao-em-kebab-case>`
```bash
git checkout main
git pull origin main
git checkout -b feat/nome-descritivo-da-tarefa
```
## Passo 3 — Executar a tarefa
Implemente as mudanças seguindo os padrões definidos no `CLAUDE.md` do projeto.
Lembre-se de:
- Não usar SQL nativo nem JPQL nas repositories
- Usar `Objects.isNull()` / `Objects.nonNull()` ao invés de `== null` / `!= null`
- Seguir a Clean Architecture (entities → use cases → interface adapters → frameworks)
- Não importar frameworks nas camadas de domínio
## Passo 4 — Commit
Formato obrigatório: `<tipo>: <descrição em inglês no imperativo>`
```bash
git add <arquivos específicos>
git commit -m "feat: add null check using Objects utility methods"
```
Tipos válidos: `feat`, `fix`, `chore`, `docs`, `test`, `refactor`
Exemplos:
- `feat: add task filtering by organization`
- `fix: prevent duplicate user organization entries`
- `chore: replace JPQL queries with Panache shorthand`
## Passo 5 — Criar o Pull Request
```bash
git push origin <nome-da-branch>
gh pr create \
--title "<tipo>: <descrição curta em inglês>" \
--body "$(cat <<'EOF'
## Descrição
...
## Tipo de mudança
- [x] Nova funcionalidade
## Issue relacionada
Fecha #<número>
## O que foi feito
- ...
## Como testar
1. ...
## Checklist
- [x] Código segue padrões do projeto
- [x] Build passa sem erros
EOF
)"
```
A descrição do PR deve ser **em português**, detalhada e seguir o template em `.github/PULL_REQUEST_TEMPLATE.md`.

38
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,38 @@
## Descrição
<!-- Descreva de forma clara e objetiva o que foi implementado, corrigido ou alterado neste PR. -->
## Tipo de mudança
- [ ] Nova funcionalidade (`feat`)
- [ ] Correção de bug (`fix`)
- [ ] Melhoria/refatoração (`chore`)
- [ ] Documentação (`docs`)
- [ ] Testes (`test`)
## Issue relacionada
Fecha #<!-- número da issue -->
## O que foi feito
<!-- Liste as principais mudanças realizadas: -->
-
-
-
## Como testar
<!-- Descreva o passo a passo para validar as mudanças: -->
1.
2.
3.
## Checklist
- [ ] O código segue os padrões do projeto (sem SQL nativo, sem JPQL direto, usando `Objects.isNull`/`nonNull`)
- [ ] Foram criados/atualizados testes para as mudanças
- [ ] O build passa sem erros (`./mvnw verify` ou `quarkus dev`)
- [ ] Foi revisado o impacto em outras partes do sistema

145
CLAUDE.md Normal file
View file

@ -0,0 +1,145 @@
# Bee Task — Guia de Desenvolvimento
## Stack
- **Backend**: Quarkus + Hibernate ORM + Panache (Java 17+)
- **Frontend**: React + TypeScript + Tailwind CSS
- **Banco de dados**: PostgreSQL (via Docker Compose)
## Arquitetura
O backend segue **Clean Architecture** adaptada:
```
entities/ → Domínio puro: entidades e regras de negócio
usecase/ → Casos de uso: orquestram entidades e repositórios (interfaces)
interfaceadapters/ → Controllers REST (JAX-RS), mappers de entrada/saída
frameworks/ → Implementações: Panache repositories, JPA entities, mappers de persistência
```
**Regras de dependência (nunca viole):**
- `entities` não importa nada externo (sem frameworks, sem Jakarta, sem Quarkus)
- `usecase` só importa `entities` e interfaces próprias
- `interfaceadapters` importa `usecase` e `entities`, nunca `frameworks`
- `frameworks` implementa as interfaces de `usecase`
## Padrões de Código Java
### Repositories — Sem SQL nativo e sem JPQL explícito
Nas implementações de repository (`frameworks/persistence/repository/`), use **sempre** o estilo shorthand do Panache:
```java
// CORRETO — shorthand Panache (propriedades da entidade)
find("user.email = ?1 and status = ?2", email, status).list();
find("organization.id", organizationId).stream().toList();
findByIdOptional(id).map(mapper::toDomain);
// ERRADO — JPQL explícito
find("SELECT t FROM TaskEntity t WHERE t.user.email = ?1", email);
find("select distinct t from TaskEntity t join t.history h where ...", ...);
// ERRADO — SQL nativo
getEntityManager().createNativeQuery("SELECT * FROM tasks WHERE ...");
```
Para consultas complexas que envolvem JOINs com coleções, prefira buscar via shorthand e filtrar em Java:
```java
// Busca por propriedade simples + filtragem em Java
return find("user.email = ?1", email).list().stream()
.filter(entity -> entity.getHistory().stream().anyMatch(h ->
Objects.nonNull(h.getStartAt()) &&
!h.getStartAt().isAfter(end) &&
(Objects.isNull(h.getEndAt()) || !h.getEndAt().isBefore(start))))
.map(mapper::toDomain)
.toList();
```
### Verificações de nulo — `Objects.isNull` e `Objects.nonNull`
Use sempre `Objects.isNull(x)` e `Objects.nonNull(x)` ao invés dos operadores `== null` e `!= null`:
```java
// CORRETO
if (Objects.isNull(entity.getId())) {
persist(entity);
}
if (Objects.nonNull(input.projectId())) {
task.setProject(projectRepository.findProjectById(input.projectId()).orElseThrow());
}
// ERRADO
if (entity.getId() == null) { ... }
if (input.projectId() != null) { ... }
```
Isso se aplica a: repositories, mappers, use cases e entities.
Exceção tolerada: ternários simples inline onde a legibilidade é melhor com `!=`.
### Mappers de persistência
Sempre verifique nulo na entrada de métodos de mapeamento:
```java
public static MyEntity toEntity(MyDomain domain) {
if (Objects.isNull(domain)) return null;
// ...
}
```
### Padrão de save nos repositories
```java
if (Objects.isNull(entity.getId())) {
persist(entity);
} else {
entity = getEntityManager().merge(entity);
}
```
## Workflow de Tarefas
Use o skill `/new-task` para seguir o guia completo de início ao PR.
### Branches
| Tipo | Prefixo |
|------|---------|
| Funcionalidade | `feat/` |
| Bug | `fix/` |
| Manutenção | `chore/` |
| Testes | `test/` |
### Commits
Formato: `<tipo>: <descrição em inglês no imperativo>`
```
feat: add task filtering by user organization
fix: prevent null pointer in task history mapper
chore: replace JPQL with Panache shorthand queries
```
### Pull Requests
- Título em inglês, curto (< 70 chars)
- Descrição em **português**, seguindo `.github/PULL_REQUEST_TEMPLATE.md`
- Vincular a issue com `Fecha #<número>`
## Rodando o projeto
```bash
# Backend
cd backend
./mvnw quarkus:dev
# Frontend
cd frontend
npm install
npm run dev
# Banco de dados
docker-compose up -d
```

View file

@ -1,6 +1,7 @@
package net.beetechgroup.beetask.entities; package net.beetechgroup.beetask.entities;
import java.util.List; import java.util.List;
import java.util.Objects;
import net.beetechgroup.beetask.entities.organization.Organization; import net.beetechgroup.beetask.entities.organization.Organization;
@ -47,7 +48,7 @@ public class Project {
return "Project{" + return "Project{" +
"id=" + id + "id=" + id +
", name='" + name + '\'' + ", name='" + name + '\'' +
", organization=" + (organization != null ? organization.getName() : "null") + ", organization=" + (Objects.nonNull(organization) ? organization.getName() : "null") +
'}'; '}';
} }
} }

View file

@ -3,6 +3,7 @@ package net.beetechgroup.beetask.entities.task;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import net.beetechgroup.beetask.entities.Project; import net.beetechgroup.beetask.entities.Project;
import net.beetechgroup.beetask.entities.User; import net.beetechgroup.beetask.entities.User;
@ -48,9 +49,6 @@ public class Task {
public void setStatus(TaskStatus status) { public void setStatus(TaskStatus status) {
if (this.status == status) return; if (this.status == status) return;
// Note: Automatic start/stop removed from setStatus to avoid
// compilation errors and ensure user attribution via StartTaskUseCase.
if (status == TaskStatus.COMPLETED) { if (status == TaskStatus.COMPLETED) {
this.finishedAt = LocalDateTime.now(); this.finishedAt = LocalDateTime.now();
} else { } else {
@ -106,7 +104,7 @@ public class Task {
public void start() { public void start() {
TaskHistoryItem current = getCurrentRunningItem(); TaskHistoryItem current = getCurrentRunningItem();
if (current != null) { if (Objects.nonNull(current)) {
throw new IllegalStateException("Tarefa já está em execução"); throw new IllegalStateException("Tarefa já está em execução");
} }
@ -119,12 +117,10 @@ public class Task {
public void stop() { public void stop() {
TaskHistoryItem current = getCurrentRunningItem(); TaskHistoryItem current = getCurrentRunningItem();
if (current == null) { if (Objects.isNull(current)) {
throw new IllegalStateException("Task não está em execução"); throw new IllegalStateException("Task não está em execução");
} }
current.setEndAt(LocalDateTime.now()); current.setEndAt(LocalDateTime.now());
} }
} }

View file

@ -1,6 +1,7 @@
package net.beetechgroup.beetask.entities.task; package net.beetechgroup.beetask.entities.task;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects;
public class TaskHistoryItem { public class TaskHistoryItem {
private Long id; private Long id;
@ -32,6 +33,6 @@ public class TaskHistoryItem {
} }
public boolean isRunning() { public boolean isRunning() {
return endAt == null; return Objects.isNull(endAt);
} }
} }

View file

@ -1,5 +1,6 @@
package net.beetechgroup.beetask.frameworks.persistence.mapper; package net.beetechgroup.beetask.frameworks.persistence.mapper;
import java.util.Objects;
import net.beetechgroup.beetask.entities.organization.Organization; import net.beetechgroup.beetask.entities.organization.Organization;
import net.beetechgroup.beetask.frameworks.persistence.entities.OrganizationEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.OrganizationEntity;
@ -12,7 +13,7 @@ public class OrganizationEntityMapper {
} }
public static Organization toDomain(OrganizationEntity entity) { public static Organization toDomain(OrganizationEntity entity) {
if (entity == null) return null; if (Objects.isNull(entity)) return null;
Organization organization = new Organization(); Organization organization = new Organization();
organization.setId(entity.getId()); organization.setId(entity.getId());

View file

@ -1,12 +1,13 @@
package net.beetechgroup.beetask.frameworks.persistence.mapper; package net.beetechgroup.beetask.frameworks.persistence.mapper;
import java.util.Objects;
import net.beetechgroup.beetask.entities.Project; import net.beetechgroup.beetask.entities.Project;
import net.beetechgroup.beetask.frameworks.persistence.entities.ProjectEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.ProjectEntity;
public class ProjectEntityMapper { public class ProjectEntityMapper {
public static ProjectEntity toEntity(Project project) { public static ProjectEntity toEntity(Project project) {
if (project == null) return null; if (Objects.isNull(project)) return null;
ProjectEntity entity = new ProjectEntity(); ProjectEntity entity = new ProjectEntity();
entity.setId(project.getId()); entity.setId(project.getId());
entity.setName(project.getName()); entity.setName(project.getName());
@ -15,7 +16,7 @@ public class ProjectEntityMapper {
} }
public static Project toDomain(ProjectEntity entity) { public static Project toDomain(ProjectEntity entity) {
if (entity == null) return null; if (Objects.isNull(entity)) return null;
Project project = new Project(); Project project = new Project();
project.setId(entity.getId()); project.setId(entity.getId());

View file

@ -1,6 +1,7 @@
package net.beetechgroup.beetask.frameworks.persistence.mapper; package net.beetechgroup.beetask.frameworks.persistence.mapper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects;
import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.Task;
import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity;
@ -26,7 +27,7 @@ public class TaskEntityMapper {
} }
public static Task toDomain(TaskEntity entity) { public static Task toDomain(TaskEntity entity) {
if (entity == null) return null; if (Objects.isNull(entity)) return null;
Task task = new Task(); Task task = new Task();
task.setId(entity.getId()); task.setId(entity.getId());

View file

@ -1,5 +1,6 @@
package net.beetechgroup.beetask.frameworks.persistence.mapper; package net.beetechgroup.beetask.frameworks.persistence.mapper;
import java.util.Objects;
import net.beetechgroup.beetask.entities.task.TaskHistoryItem; import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
import net.beetechgroup.beetask.frameworks.persistence.entities.TaskHistoryItemEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.TaskHistoryItemEntity;
@ -13,7 +14,7 @@ public class TaskHistoryItemMapper {
} }
public static TaskHistoryItem toDomain(TaskHistoryItemEntity entity) { public static TaskHistoryItem toDomain(TaskHistoryItemEntity entity) {
if (entity == null) return null; if (Objects.isNull(entity)) return null;
TaskHistoryItem item = new TaskHistoryItem(); TaskHistoryItem item = new TaskHistoryItem();
item.setId(entity.getId()); item.setId(entity.getId());

View file

@ -1,12 +1,13 @@
package net.beetechgroup.beetask.frameworks.persistence.mapper; package net.beetechgroup.beetask.frameworks.persistence.mapper;
import java.util.Objects;
import net.beetechgroup.beetask.entities.User; import net.beetechgroup.beetask.entities.User;
import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity;
public class UserEntityMapper { public class UserEntityMapper {
public static UserEntity toEntity(User user) { public static UserEntity toEntity(User user) {
if (user == null) return null; if (Objects.isNull(user)) return null;
UserEntity entity = new UserEntity(); UserEntity entity = new UserEntity();
entity.setId(user.getId()); entity.setId(user.getId());
entity.setName(user.getName()); entity.setName(user.getName());
@ -17,7 +18,7 @@ public class UserEntityMapper {
} }
public static User toDomain(UserEntity entity) { public static User toDomain(UserEntity entity) {
if (entity == null) return null; if (Objects.isNull(entity)) return null;
User user = new User(); User user = new User();
user.setId(entity.getId()); user.setId(entity.getId());
user.setName(entity.getName()); user.setName(entity.getName());

View file

@ -1,12 +1,13 @@
package net.beetechgroup.beetask.frameworks.persistence.mapper; package net.beetechgroup.beetask.frameworks.persistence.mapper;
import java.util.Objects;
import net.beetechgroup.beetask.entities.organization.UserOrganization; import net.beetechgroup.beetask.entities.organization.UserOrganization;
import net.beetechgroup.beetask.frameworks.persistence.entities.UserOrganizationEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.UserOrganizationEntity;
public class UserOrganizationEntityMapper { public class UserOrganizationEntityMapper {
public static UserOrganization toDomain(UserOrganizationEntity entity) { public static UserOrganization toDomain(UserOrganizationEntity entity) {
if (entity == null) return null; if (Objects.isNull(entity)) return null;
UserOrganization domain = new UserOrganization(); UserOrganization domain = new UserOrganization();
domain.setUser(UserEntityMapper.toDomain(entity.getUser())); domain.setUser(UserEntityMapper.toDomain(entity.getUser()));
domain.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization())); domain.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization()));
@ -16,7 +17,7 @@ public class UserOrganizationEntityMapper {
} }
public static UserOrganizationEntity toEntity(UserOrganization domain) { public static UserOrganizationEntity toEntity(UserOrganization domain) {
if (domain == null) return null; if (Objects.isNull(domain)) return null;
UserOrganizationEntity entity = new UserOrganizationEntity(); UserOrganizationEntity entity = new UserOrganizationEntity();
entity.setUser(UserEntityMapper.toEntity(domain.getUser())); entity.setUser(UserEntityMapper.toEntity(domain.getUser()));
entity.setOrganization(OrganizationEntityMapper.toEntity(domain.getOrganization())); entity.setOrganization(OrganizationEntityMapper.toEntity(domain.getOrganization()));

View file

@ -19,7 +19,7 @@ public class OrganizationRepositoryImpl implements OrganizationRepository, Panac
@Transactional @Transactional
public Organization saveOrganization(Organization organization) { public Organization saveOrganization(Organization organization) {
OrganizationEntity entity = OrganizationEntityMapper.toEntity(organization); OrganizationEntity entity = OrganizationEntityMapper.toEntity(organization);
if (entity.getId() == null) { if (Objects.isNull(entity.getId())) {
persist(entity); persist(entity);
} else { } else {
entity = getEntityManager().merge(entity); entity = getEntityManager().merge(entity);

View file

@ -9,6 +9,7 @@ import net.beetechgroup.beetask.frameworks.persistence.mapper.ProjectEntityMappe
import net.beetechgroup.beetask.usecase.repository.ProjectRepository; import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ApplicationScoped @ApplicationScoped
@ -18,7 +19,7 @@ public class ProjectRepositoryImpl implements ProjectRepository, PanacheReposito
@Transactional @Transactional
public Project saveProject(Project project) { public Project saveProject(Project project) {
ProjectEntity entity = ProjectEntityMapper.toEntity(project); ProjectEntity entity = ProjectEntityMapper.toEntity(project);
if (entity.getId() == null) { if (Objects.isNull(entity.getId())) {
persist(entity); persist(entity);
} else { } else {
entity = getEntityManager().merge(entity); entity = getEntityManager().merge(entity);

View file

@ -1,12 +1,14 @@
package net.beetechgroup.beetask.frameworks.persistence.repository; package net.beetechgroup.beetask.frameworks.persistence.repository;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import io.quarkus.hibernate.orm.panache.PanacheRepository; import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.Task;
import net.beetechgroup.beetask.entities.task.TaskStatus;
import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity;
import net.beetechgroup.beetask.frameworks.persistence.mapper.TaskEntityMapper; import net.beetechgroup.beetask.frameworks.persistence.mapper.TaskEntityMapper;
import net.beetechgroup.beetask.usecase.exceptions.TaskNotFoundException; import net.beetechgroup.beetask.usecase.exceptions.TaskNotFoundException;
@ -19,7 +21,7 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository<Tas
@Transactional @Transactional
public Task saveTask(Task task) { public Task saveTask(Task task) {
TaskEntity entity = TaskEntityMapper.toEntity(task); TaskEntity entity = TaskEntityMapper.toEntity(task);
if (entity.getId() == null) { if (Objects.isNull(entity.getId())) {
persist(entity); persist(entity);
} else { } else {
entity = getEntityManager().merge(entity); entity = getEntityManager().merge(entity);
@ -41,17 +43,20 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository<Tas
@Override @Override
public List<Task> findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) { public List<Task> findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) {
return find("select distinct t from TaskEntity t join t.history h where t.user.email = ?1 " + return find("user.email = ?1", email).list().stream()
"and h.startAt <= ?3 and (h.endAt is null or h.endAt >= ?2)", .filter(entity -> entity.getHistory().stream().anyMatch(h ->
email, start, end).list().stream().map(TaskEntityMapper::toDomain).toList(); Objects.nonNull(h.getStartAt()) &&
!h.getStartAt().isAfter(end) &&
(Objects.isNull(h.getEndAt()) || !h.getEndAt().isBefore(start))))
.map(TaskEntityMapper::toDomain)
.toList();
} }
@Override @Override
public List<Task> findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) { public List<Task> findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) {
return find("select distinct t from TaskEntity t where t.user.email = ?1 " + return find("user.email = ?1 and status = ?2 and finishedAt >= ?3 and finishedAt <= ?4",
"and t.status = net.beetechgroup.beetask.entities.task.TaskStatus.COMPLETED " + email, TaskStatus.COMPLETED, start, end)
"and t.finishedAt >= ?2 and t.finishedAt <= ?3", .list().stream().map(TaskEntityMapper::toDomain).toList();
email, start, end).list().stream().map(TaskEntityMapper::toDomain).toList();
} }
@Override @Override

View file

@ -10,6 +10,7 @@ import net.beetechgroup.beetask.frameworks.persistence.mapper.UserOrganizationEn
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ApplicationScoped @ApplicationScoped
@ -47,8 +48,7 @@ public class UserOrganizationRepositoryImpl implements UserOrganizationRepositor
@Transactional @Transactional
public void save(UserOrganization userOrganization) { public void save(UserOrganization userOrganization) {
UserOrganizationEntity entity = UserOrganizationEntityMapper.toEntity(userOrganization); UserOrganizationEntity entity = UserOrganizationEntityMapper.toEntity(userOrganization);
if (entity.getUser().getId() != null && entity.getOrganization().getId() != null) { if (Objects.nonNull(entity.getUser().getId()) && Objects.nonNull(entity.getOrganization().getId())) {
// Find existing by user and org to update instead of creating new if it exists
Optional<UserOrganizationEntity> existing = find("user.id = ?1 and organization.id = ?2", Optional<UserOrganizationEntity> existing = find("user.id = ?1 and organization.id = ?2",
entity.getUser().getId(), entity.getOrganization().getId()).firstResultOptional(); entity.getUser().getId(), entity.getOrganization().getId()).firstResultOptional();
if (existing.isPresent()) { if (existing.isPresent()) {

View file

@ -4,6 +4,7 @@ import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.util.Objects;
import net.beetechgroup.beetask.entities.User; import net.beetechgroup.beetask.entities.User;
import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity; import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity;
import net.beetechgroup.beetask.frameworks.persistence.mapper.UserEntityMapper; import net.beetechgroup.beetask.frameworks.persistence.mapper.UserEntityMapper;
@ -25,7 +26,7 @@ public class UserRepositoryImpl implements UserRepository, PanacheRepository<Use
@Transactional @Transactional
public User save(User user) { public User save(User user) {
UserEntity entity = UserEntityMapper.toEntity(user); UserEntity entity = UserEntityMapper.toEntity(user);
if (entity.getId() == null) { if (Objects.isNull(entity.getId())) {
persist(entity); persist(entity);
} else { } else {
entity = getEntityManager().merge(entity); entity = getEntityManager().merge(entity);

View file

@ -1,5 +1,6 @@
package net.beetechgroup.beetask.interfaceadapters.controllers.task; package net.beetechgroup.beetask.interfaceadapters.controllers.task;
import java.util.Objects;
import net.beetechgroup.beetask.usecase.task.create.CreateTaskInput; import net.beetechgroup.beetask.usecase.task.create.CreateTaskInput;
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput; import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
import net.beetechgroup.beetask.usecase.task.start.StartTaskInput; import net.beetechgroup.beetask.usecase.task.start.StartTaskInput;
@ -47,7 +48,7 @@ public class TaskControllerMapper {
request.status(), request.status(),
request.projectId(), request.projectId(),
userEmail, userEmail,
request.history() != null ? request.history().stream() Objects.nonNull(request.history()) ? request.history().stream()
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt())) .map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
.toList() : null .toList() : null
); );

View file

@ -8,6 +8,7 @@ import net.beetechgroup.beetask.usecase.repository.UserRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -69,7 +70,7 @@ public class LoginUseCase {
return new LoginOutput( return new LoginOutput(
user.getName(), user.getName(),
user.getEmail(), user.getEmail(),
user.getPhoto() != null ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random", Objects.nonNull(user.getPhoto()) ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random",
token, token,
refreshToken, refreshToken,
3600L, 3600L,

View file

@ -12,6 +12,7 @@ import org.eclipse.microprofile.jwt.JsonWebToken;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -33,7 +34,7 @@ public class RefreshTokenUseCase {
JsonWebToken token = jwtParser.parse(input.refreshToken()); JsonWebToken token = jwtParser.parse(input.refreshToken());
String email = token.getSubject(); // Or token.getName() / token.getClaim(Claims.upn.name()) String email = token.getSubject(); // Or token.getName() / token.getClaim(Claims.upn.name())
if (email == null) { if (Objects.isNull(email)) {
throw new RuntimeException("Invalid refresh token: email not found"); throw new RuntimeException("Invalid refresh token: email not found");
} }
@ -77,7 +78,7 @@ public class RefreshTokenUseCase {
return new LoginOutput( return new LoginOutput(
user.getName(), user.getName(),
user.getEmail(), user.getEmail(),
user.getPhoto() != null ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random", Objects.nonNull(user.getPhoto()) ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random",
newToken, newToken,
newRefreshToken, newRefreshToken,
3600L, 3600L,

View file

@ -7,6 +7,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.Task;
import net.beetechgroup.beetask.entities.task.TaskHistoryItem; import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
@ -22,24 +23,20 @@ public class DashboardUseCase {
} }
public DashboardOutput execute(DashboardInput input) { public DashboardOutput execute(DashboardInput input) {
// 1. Fetch tasks worked by user in period
List<Task> workedTasks = taskRepository.findTasksWorkedByUserInPeriod( List<Task> workedTasks = taskRepository.findTasksWorkedByUserInPeriod(
input.userEmail(), input.startDate(), input.endDate() input.userEmail(), input.startDate(), input.endDate()
); );
// 2. Fetch tasks finished by user in period
List<Task> finishedTasks = taskRepository.findTasksFinishedByUserInPeriod( List<Task> finishedTasks = taskRepository.findTasksFinishedByUserInPeriod(
input.userEmail(), input.startDate(), input.endDate() input.userEmail(), input.startDate(), input.endDate()
); );
// 3. Yesterday's tasks
LocalDateTime startOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MIN); LocalDateTime startOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MIN);
LocalDateTime endOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MAX); LocalDateTime endOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MAX);
List<Task> yesterdayTasks = taskRepository.findTasksWorkedByUserInPeriod( List<Task> yesterdayTasks = taskRepository.findTasksWorkedByUserInPeriod(
input.userEmail(), startOfYesterday, endOfYesterday input.userEmail(), startOfYesterday, endOfYesterday
); );
// Calculate total minutes and project stats
long totalMinutes = 0; long totalMinutes = 0;
Map<Long, DashboardProjectStats> projectStatsMap = new HashMap<>(); Map<Long, DashboardProjectStats> projectStatsMap = new HashMap<>();
@ -51,7 +48,7 @@ public class DashboardUseCase {
totalMinutes += taskMinutesInPeriod; totalMinutes += taskMinutesInPeriod;
if (task.getProject() != null) { if (Objects.nonNull(task.getProject())) {
Long projectId = task.getProject().getId(); Long projectId = task.getProject().getId();
DashboardProjectStats stats = projectStatsMap.getOrDefault(projectId, DashboardProjectStats stats = projectStatsMap.getOrDefault(projectId,
new DashboardProjectStats(projectId, task.getProject().getName(), 0L)); new DashboardProjectStats(projectId, task.getProject().getName(), 0L));
@ -73,16 +70,15 @@ public class DashboardUseCase {
} }
private boolean isWithinPeriod(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) { private boolean isWithinPeriod(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) {
if (item.getStartAt() == null) return false; if (Objects.isNull(item.getStartAt())) return false;
// If it started after end or ended before start, it's out
if (item.getStartAt().isAfter(end)) return false; if (item.getStartAt().isAfter(end)) return false;
if (item.getEndAt() != null && item.getEndAt().isBefore(start)) return false; if (Objects.nonNull(item.getEndAt()) && item.getEndAt().isBefore(start)) return false;
return true; return true;
} }
private long calculateMinutesInRange(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) { private long calculateMinutesInRange(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) {
LocalDateTime effectiveStart = item.getStartAt().isBefore(start) ? start : item.getStartAt(); LocalDateTime effectiveStart = item.getStartAt().isBefore(start) ? start : item.getStartAt();
LocalDateTime effectiveEnd = item.getEndAt() == null || item.getEndAt().isAfter(end) ? end : item.getEndAt(); LocalDateTime effectiveEnd = Objects.isNull(item.getEndAt()) || item.getEndAt().isAfter(end) ? end : item.getEndAt();
if (effectiveStart.isAfter(effectiveEnd)) return 0; if (effectiveStart.isAfter(effectiveEnd)) return 0;

View file

@ -1,5 +1,6 @@
package net.beetechgroup.beetask.usecase.task.create; package net.beetechgroup.beetask.usecase.task.create;
import java.util.Objects;
import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.Task;
import net.beetechgroup.beetask.entities.task.TaskHistoryItem; import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
@ -10,7 +11,7 @@ public class CreateTaskMapper {
task.getTitle(), task.getTitle(),
task.getDescription(), task.getDescription(),
task.getStatus(), task.getStatus(),
task.getProject() != null ? task.getProject().getName() : null, Objects.nonNull(task.getProject()) ? task.getProject().getName() : null,
task.getFinishedAt(), task.getFinishedAt(),
task.getHistory().stream().map(CreateTaskMapper::toTaskHistoryItemOutput).toList()); task.getHistory().stream().map(CreateTaskMapper::toTaskHistoryItemOutput).toList());
} }

View file

@ -1,5 +1,6 @@
package net.beetechgroup.beetask.usecase.task.create; package net.beetechgroup.beetask.usecase.task.create;
import java.util.Objects;
import net.beetechgroup.beetask.entities.Project; import net.beetechgroup.beetask.entities.Project;
import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.Task;
import net.beetechgroup.beetask.usecase.repository.ProjectRepository; import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
@ -23,7 +24,7 @@ public class CreateTaskUseCase {
task.setDescription(input.description()); task.setDescription(input.description());
task.setStatus(input.status()); task.setStatus(input.status());
if (input.projectId() != null) { if (Objects.nonNull(input.projectId())) {
Project project = projectRepository.findProjectById(input.projectId()) Project project = projectRepository.findProjectById(input.projectId())
.orElseThrow(() -> new IllegalArgumentException("Projeto não encontrado com ID: " + input.projectId())); .orElseThrow(() -> new IllegalArgumentException("Projeto não encontrado com ID: " + input.projectId()));
task.setProject(project); task.setProject(project);

View file

@ -1,12 +1,11 @@
package net.beetechgroup.beetask.usecase.task.start; package net.beetechgroup.beetask.usecase.task.start;
import net.beetechgroup.beetask.entities.User; import java.util.Objects;
import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.Task;
import net.beetechgroup.beetask.usecase.repository.TaskRepository; import net.beetechgroup.beetask.usecase.repository.TaskRepository;
import net.beetechgroup.beetask.usecase.repository.UserRepository; import net.beetechgroup.beetask.usecase.repository.UserRepository;
import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper; import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper;
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput; import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
import net.beetechgroup.beetask.usecase.exceptions.UserNotFoundException;
public class StartTaskUseCase { public class StartTaskUseCase {
@ -21,7 +20,7 @@ public class StartTaskUseCase {
public CreateTaskOutput execute(StartTaskInput input) { public CreateTaskOutput execute(StartTaskInput input) {
Task task = taskRepository.findTaskById(input.id()); Task task = taskRepository.findTaskById(input.id());
if (task.getUser() != null && !task.getUser().getEmail().equals(input.userEmail())) { if (Objects.nonNull(task.getUser()) && !task.getUser().getEmail().equals(input.userEmail())) {
throw new SecurityException("Você não tem permissão para iniciar esta tarefa."); throw new SecurityException("Você não tem permissão para iniciar esta tarefa.");
} }

View file

@ -1,6 +1,7 @@
package net.beetechgroup.beetask.usecase.task.update; package net.beetechgroup.beetask.usecase.task.update;
import java.util.List; import java.util.List;
import java.util.Objects;
import net.beetechgroup.beetask.entities.Project; import net.beetechgroup.beetask.entities.Project;
import net.beetechgroup.beetask.entities.task.Task; import net.beetechgroup.beetask.entities.task.Task;
import net.beetechgroup.beetask.entities.task.TaskHistoryItem; import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
@ -24,7 +25,7 @@ public class UpdateTaskUseCase {
public CreateTaskOutput execute(UpdateTaskInput input) { public CreateTaskOutput execute(UpdateTaskInput input) {
Task task = taskRepository.findTaskById(input.id()); Task task = taskRepository.findTaskById(input.id());
if (task.getUser() == null) { if (Objects.isNull(task.getUser())) {
task.setUser(userRepository.findByEmail(input.userEmail()) task.setUser(userRepository.findByEmail(input.userEmail())
.orElseThrow(() -> new IllegalArgumentException("Usuário não encontrado: " + input.userEmail()))); .orElseThrow(() -> new IllegalArgumentException("Usuário não encontrado: " + input.userEmail())));
} }
@ -33,7 +34,7 @@ public class UpdateTaskUseCase {
task.setDescription(input.description()); task.setDescription(input.description());
task.setStatus(input.status()); task.setStatus(input.status());
if (input.projectId() != null) { if (Objects.nonNull(input.projectId())) {
Project project = projectRepository.findProjectById(input.projectId()) Project project = projectRepository.findProjectById(input.projectId())
.orElseThrow(() -> new IllegalArgumentException("Projeto não encontrado com ID: " + input.projectId())); .orElseThrow(() -> new IllegalArgumentException("Projeto não encontrado com ID: " + input.projectId()));
task.setProject(project); task.setProject(project);
@ -41,9 +42,9 @@ public class UpdateTaskUseCase {
task.setProject(null); task.setProject(null);
} }
if (input.history() != null) { if (Objects.nonNull(input.history())) {
List<TaskHistoryItem> updatedHistory = input.history().stream().map(hInput -> { List<TaskHistoryItem> updatedHistory = input.history().stream().map(hInput -> {
if (hInput.id() != null) { if (Objects.nonNull(hInput.id())) {
return task.getHistory().stream() return task.getHistory().stream()
.filter(existing -> existing.getId().equals(hInput.id())) .filter(existing -> existing.getId().equals(hInput.id()))
.findFirst() .findFirst()

View file

@ -5,6 +5,7 @@ import net.beetechgroup.beetask.entities.organization.UserOrganization;
import net.beetechgroup.beetask.usecase.repository.UserRepository; import net.beetechgroup.beetask.usecase.repository.UserRepository;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -42,7 +43,7 @@ public class GetUserProfileUseCase {
return new UserProfileOutput( return new UserProfileOutput(
user.getName(), user.getName(),
user.getEmail(), user.getEmail(),
user.getPhoto() != null ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random", Objects.nonNull(user.getPhoto()) ? user.getPhoto() : "https://ui-avatars.com/api/?name=" + user.getName().replace(" ", "+") + "&background=random",
orgs orgs
); );
} }