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:
parent
6800688284
commit
1868d069c2
26 changed files with 363 additions and 75 deletions
92
.claude/skills/new-task.md
Normal file
92
.claude/skills/new-task.md
Normal 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
38
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
145
CLAUDE.md
Normal 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
|
||||
```
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package net.beetechgroup.beetask.entities;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import net.beetechgroup.beetask.entities.organization.Organization;
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ public class Project {
|
|||
return "Project{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
", organization=" + (organization != null ? organization.getName() : "null") +
|
||||
", organization=" + (Objects.nonNull(organization) ? organization.getName() : "null") +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package net.beetechgroup.beetask.entities.task;
|
|||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import net.beetechgroup.beetask.entities.Project;
|
||||
import net.beetechgroup.beetask.entities.User;
|
||||
|
|
@ -48,9 +49,6 @@ public class Task {
|
|||
public void setStatus(TaskStatus status) {
|
||||
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) {
|
||||
this.finishedAt = LocalDateTime.now();
|
||||
} else {
|
||||
|
|
@ -99,14 +97,14 @@ public class Task {
|
|||
private TaskHistoryItem getCurrentRunningItem() {
|
||||
return history.stream()
|
||||
.filter(TaskHistoryItem::isRunning)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
TaskHistoryItem current = getCurrentRunningItem();
|
||||
|
||||
if (current != null) {
|
||||
if (Objects.nonNull(current)) {
|
||||
throw new IllegalStateException("Tarefa já está em execução");
|
||||
}
|
||||
|
||||
|
|
@ -117,14 +115,12 @@ public class Task {
|
|||
}
|
||||
|
||||
public void stop() {
|
||||
TaskHistoryItem current = getCurrentRunningItem();
|
||||
TaskHistoryItem current = getCurrentRunningItem();
|
||||
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("Task não está em execução");
|
||||
if (Objects.isNull(current)) {
|
||||
throw new IllegalStateException("Task não está em execução");
|
||||
}
|
||||
|
||||
current.setEndAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
current.setEndAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package net.beetechgroup.beetask.entities.task;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
public class TaskHistoryItem {
|
||||
private Long id;
|
||||
|
|
@ -32,6 +33,6 @@ public class TaskHistoryItem {
|
|||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return endAt == null;
|
||||
return Objects.isNull(endAt);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.mapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.organization.Organization;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.OrganizationEntity;
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ public class OrganizationEntityMapper {
|
|||
}
|
||||
|
||||
public static Organization toDomain(OrganizationEntity entity) {
|
||||
if (entity == null) return null;
|
||||
if (Objects.isNull(entity)) return null;
|
||||
|
||||
Organization organization = new Organization();
|
||||
organization.setId(entity.getId());
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.mapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.Project;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.ProjectEntity;
|
||||
|
||||
public class ProjectEntityMapper {
|
||||
|
||||
public static ProjectEntity toEntity(Project project) {
|
||||
if (project == null) return null;
|
||||
if (Objects.isNull(project)) return null;
|
||||
ProjectEntity entity = new ProjectEntity();
|
||||
entity.setId(project.getId());
|
||||
entity.setName(project.getName());
|
||||
|
|
@ -15,7 +16,7 @@ public class ProjectEntityMapper {
|
|||
}
|
||||
|
||||
public static Project toDomain(ProjectEntity entity) {
|
||||
if (entity == null) return null;
|
||||
if (Objects.isNull(entity)) return null;
|
||||
|
||||
Project project = new Project();
|
||||
project.setId(entity.getId());
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.mapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
import net.beetechgroup.beetask.entities.task.Task;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity;
|
||||
|
|
@ -26,7 +27,7 @@ public class TaskEntityMapper {
|
|||
}
|
||||
|
||||
public static Task toDomain(TaskEntity entity) {
|
||||
if (entity == null) return null;
|
||||
if (Objects.isNull(entity)) return null;
|
||||
|
||||
Task task = new Task();
|
||||
task.setId(entity.getId());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.mapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.TaskHistoryItemEntity;
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ public class TaskHistoryItemMapper {
|
|||
}
|
||||
|
||||
public static TaskHistoryItem toDomain(TaskHistoryItemEntity entity) {
|
||||
if (entity == null) return null;
|
||||
if (Objects.isNull(entity)) return null;
|
||||
|
||||
TaskHistoryItem item = new TaskHistoryItem();
|
||||
item.setId(entity.getId());
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.mapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.User;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity;
|
||||
|
||||
public class UserEntityMapper {
|
||||
|
||||
public static UserEntity toEntity(User user) {
|
||||
if (user == null) return null;
|
||||
if (Objects.isNull(user)) return null;
|
||||
UserEntity entity = new UserEntity();
|
||||
entity.setId(user.getId());
|
||||
entity.setName(user.getName());
|
||||
|
|
@ -17,7 +18,7 @@ public class UserEntityMapper {
|
|||
}
|
||||
|
||||
public static User toDomain(UserEntity entity) {
|
||||
if (entity == null) return null;
|
||||
if (Objects.isNull(entity)) return null;
|
||||
User user = new User();
|
||||
user.setId(entity.getId());
|
||||
user.setName(entity.getName());
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.mapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.organization.UserOrganization;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.UserOrganizationEntity;
|
||||
|
||||
public class UserOrganizationEntityMapper {
|
||||
|
||||
public static UserOrganization toDomain(UserOrganizationEntity entity) {
|
||||
if (entity == null) return null;
|
||||
if (Objects.isNull(entity)) return null;
|
||||
UserOrganization domain = new UserOrganization();
|
||||
domain.setUser(UserEntityMapper.toDomain(entity.getUser()));
|
||||
domain.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization()));
|
||||
|
|
@ -16,7 +17,7 @@ public class UserOrganizationEntityMapper {
|
|||
}
|
||||
|
||||
public static UserOrganizationEntity toEntity(UserOrganization domain) {
|
||||
if (domain == null) return null;
|
||||
if (Objects.isNull(domain)) return null;
|
||||
UserOrganizationEntity entity = new UserOrganizationEntity();
|
||||
entity.setUser(UserEntityMapper.toEntity(domain.getUser()));
|
||||
entity.setOrganization(OrganizationEntityMapper.toEntity(domain.getOrganization()));
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public class OrganizationRepositoryImpl implements OrganizationRepository, Panac
|
|||
@Transactional
|
||||
public Organization saveOrganization(Organization organization) {
|
||||
OrganizationEntity entity = OrganizationEntityMapper.toEntity(organization);
|
||||
if (entity.getId() == null) {
|
||||
if (Objects.isNull(entity.getId())) {
|
||||
persist(entity);
|
||||
} else {
|
||||
entity = getEntityManager().merge(entity);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import net.beetechgroup.beetask.frameworks.persistence.mapper.ProjectEntityMappe
|
|||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@ApplicationScoped
|
||||
|
|
@ -18,7 +19,7 @@ public class ProjectRepositoryImpl implements ProjectRepository, PanacheReposito
|
|||
@Transactional
|
||||
public Project saveProject(Project project) {
|
||||
ProjectEntity entity = ProjectEntityMapper.toEntity(project);
|
||||
if (entity.getId() == null) {
|
||||
if (Objects.isNull(entity.getId())) {
|
||||
persist(entity);
|
||||
} else {
|
||||
entity = getEntityManager().merge(entity);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
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.mapper.TaskEntityMapper;
|
||||
import net.beetechgroup.beetask.usecase.exceptions.TaskNotFoundException;
|
||||
|
|
@ -19,7 +21,7 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository<Tas
|
|||
@Transactional
|
||||
public Task saveTask(Task task) {
|
||||
TaskEntity entity = TaskEntityMapper.toEntity(task);
|
||||
if (entity.getId() == null) {
|
||||
if (Objects.isNull(entity.getId())) {
|
||||
persist(entity);
|
||||
} else {
|
||||
entity = getEntityManager().merge(entity);
|
||||
|
|
@ -41,17 +43,20 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository<Tas
|
|||
|
||||
@Override
|
||||
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 " +
|
||||
"and h.startAt <= ?3 and (h.endAt is null or h.endAt >= ?2)",
|
||||
email, start, end).list().stream().map(TaskEntityMapper::toDomain).toList();
|
||||
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(TaskEntityMapper::toDomain)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end) {
|
||||
return find("select distinct t from TaskEntity t where t.user.email = ?1 " +
|
||||
"and t.status = net.beetechgroup.beetask.entities.task.TaskStatus.COMPLETED " +
|
||||
"and t.finishedAt >= ?2 and t.finishedAt <= ?3",
|
||||
email, start, end).list().stream().map(TaskEntityMapper::toDomain).toList();
|
||||
return find("user.email = ?1 and status = ?2 and finishedAt >= ?3 and finishedAt <= ?4",
|
||||
email, TaskStatus.COMPLETED, start, end)
|
||||
.list().stream().map(TaskEntityMapper::toDomain).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import net.beetechgroup.beetask.frameworks.persistence.mapper.UserOrganizationEn
|
|||
import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@ApplicationScoped
|
||||
|
|
@ -47,8 +48,7 @@ public class UserOrganizationRepositoryImpl implements UserOrganizationRepositor
|
|||
@Transactional
|
||||
public void save(UserOrganization userOrganization) {
|
||||
UserOrganizationEntity entity = UserOrganizationEntityMapper.toEntity(userOrganization);
|
||||
if (entity.getUser().getId() != null && entity.getOrganization().getId() != null) {
|
||||
// Find existing by user and org to update instead of creating new if it exists
|
||||
if (Objects.nonNull(entity.getUser().getId()) && Objects.nonNull(entity.getOrganization().getId())) {
|
||||
Optional<UserOrganizationEntity> existing = find("user.id = ?1 and organization.id = ?2",
|
||||
entity.getUser().getId(), entity.getOrganization().getId()).firstResultOptional();
|
||||
if (existing.isPresent()) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
|||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.User;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.UserEntity;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.mapper.UserEntityMapper;
|
||||
|
|
@ -25,7 +26,7 @@ public class UserRepositoryImpl implements UserRepository, PanacheRepository<Use
|
|||
@Transactional
|
||||
public User save(User user) {
|
||||
UserEntity entity = UserEntityMapper.toEntity(user);
|
||||
if (entity.getId() == null) {
|
||||
if (Objects.isNull(entity.getId())) {
|
||||
persist(entity);
|
||||
} else {
|
||||
entity = getEntityManager().merge(entity);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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.CreateTaskOutput;
|
||||
import net.beetechgroup.beetask.usecase.task.start.StartTaskInput;
|
||||
|
|
@ -47,7 +48,7 @@ public class TaskControllerMapper {
|
|||
request.status(),
|
||||
request.projectId(),
|
||||
userEmail,
|
||||
request.history() != null ? request.history().stream()
|
||||
Objects.nonNull(request.history()) ? request.history().stream()
|
||||
.map(h -> new TaskHistoryItemInput(h.id(), h.startAt(), h.endAt()))
|
||||
.toList() : null
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
|||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -69,7 +70,7 @@ public class LoginUseCase {
|
|||
return new LoginOutput(
|
||||
user.getName(),
|
||||
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,
|
||||
refreshToken,
|
||||
3600L,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import org.eclipse.microprofile.jwt.JsonWebToken;
|
|||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -33,7 +34,7 @@ public class RefreshTokenUseCase {
|
|||
JsonWebToken token = jwtParser.parse(input.refreshToken());
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ public class RefreshTokenUseCase {
|
|||
return new LoginOutput(
|
||||
user.getName(),
|
||||
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,
|
||||
newRefreshToken,
|
||||
3600L,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import net.beetechgroup.beetask.entities.task.Task;
|
||||
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
||||
|
|
@ -22,24 +23,20 @@ public class DashboardUseCase {
|
|||
}
|
||||
|
||||
public DashboardOutput execute(DashboardInput input) {
|
||||
// 1. Fetch tasks worked by user in period
|
||||
List<Task> workedTasks = taskRepository.findTasksWorkedByUserInPeriod(
|
||||
input.userEmail(), input.startDate(), input.endDate()
|
||||
);
|
||||
|
||||
// 2. Fetch tasks finished by user in period
|
||||
List<Task> finishedTasks = taskRepository.findTasksFinishedByUserInPeriod(
|
||||
input.userEmail(), input.startDate(), input.endDate()
|
||||
);
|
||||
|
||||
// 3. Yesterday's tasks
|
||||
LocalDateTime startOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MIN);
|
||||
LocalDateTime endOfYesterday = LocalDateTime.now().minusDays(1).with(LocalTime.MAX);
|
||||
List<Task> yesterdayTasks = taskRepository.findTasksWorkedByUserInPeriod(
|
||||
input.userEmail(), startOfYesterday, endOfYesterday
|
||||
);
|
||||
|
||||
// Calculate total minutes and project stats
|
||||
long totalMinutes = 0;
|
||||
Map<Long, DashboardProjectStats> projectStatsMap = new HashMap<>();
|
||||
|
||||
|
|
@ -51,7 +48,7 @@ public class DashboardUseCase {
|
|||
|
||||
totalMinutes += taskMinutesInPeriod;
|
||||
|
||||
if (task.getProject() != null) {
|
||||
if (Objects.nonNull(task.getProject())) {
|
||||
Long projectId = task.getProject().getId();
|
||||
DashboardProjectStats stats = projectStatsMap.getOrDefault(projectId,
|
||||
new DashboardProjectStats(projectId, task.getProject().getName(), 0L));
|
||||
|
|
@ -73,16 +70,15 @@ public class DashboardUseCase {
|
|||
}
|
||||
|
||||
private boolean isWithinPeriod(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) {
|
||||
if (item.getStartAt() == null) return false;
|
||||
// If it started after end or ended before start, it's out
|
||||
if (Objects.isNull(item.getStartAt())) 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;
|
||||
}
|
||||
|
||||
private long calculateMinutesInRange(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) {
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package net.beetechgroup.beetask.usecase.task.create;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.task.Task;
|
||||
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ public class CreateTaskMapper {
|
|||
task.getTitle(),
|
||||
task.getDescription(),
|
||||
task.getStatus(),
|
||||
task.getProject() != null ? task.getProject().getName() : null,
|
||||
Objects.nonNull(task.getProject()) ? task.getProject().getName() : null,
|
||||
task.getFinishedAt(),
|
||||
task.getHistory().stream().map(CreateTaskMapper::toTaskHistoryItemOutput).toList());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package net.beetechgroup.beetask.usecase.task.create;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.Project;
|
||||
import net.beetechgroup.beetask.entities.task.Task;
|
||||
import net.beetechgroup.beetask.usecase.repository.ProjectRepository;
|
||||
|
|
@ -23,7 +24,7 @@ public class CreateTaskUseCase {
|
|||
task.setDescription(input.description());
|
||||
task.setStatus(input.status());
|
||||
|
||||
if (input.projectId() != null) {
|
||||
if (Objects.nonNull(input.projectId())) {
|
||||
Project project = projectRepository.findProjectById(input.projectId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Projeto não encontrado com ID: " + input.projectId()));
|
||||
task.setProject(project);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
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.usecase.repository.TaskRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskMapper;
|
||||
import net.beetechgroup.beetask.usecase.task.create.CreateTaskOutput;
|
||||
import net.beetechgroup.beetask.usecase.exceptions.UserNotFoundException;
|
||||
|
||||
public class StartTaskUseCase {
|
||||
|
||||
|
|
@ -21,7 +20,7 @@ public class StartTaskUseCase {
|
|||
public CreateTaskOutput execute(StartTaskInput input) {
|
||||
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.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package net.beetechgroup.beetask.usecase.task.update;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import net.beetechgroup.beetask.entities.Project;
|
||||
import net.beetechgroup.beetask.entities.task.Task;
|
||||
import net.beetechgroup.beetask.entities.task.TaskHistoryItem;
|
||||
|
|
@ -24,7 +25,7 @@ public class UpdateTaskUseCase {
|
|||
public CreateTaskOutput execute(UpdateTaskInput input) {
|
||||
Task task = taskRepository.findTaskById(input.id());
|
||||
|
||||
if (task.getUser() == null) {
|
||||
if (Objects.isNull(task.getUser())) {
|
||||
task.setUser(userRepository.findByEmail(input.userEmail())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Usuário não encontrado: " + input.userEmail())));
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ public class UpdateTaskUseCase {
|
|||
task.setDescription(input.description());
|
||||
task.setStatus(input.status());
|
||||
|
||||
if (input.projectId() != null) {
|
||||
if (Objects.nonNull(input.projectId())) {
|
||||
Project project = projectRepository.findProjectById(input.projectId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Projeto não encontrado com ID: " + input.projectId()));
|
||||
task.setProject(project);
|
||||
|
|
@ -41,9 +42,9 @@ public class UpdateTaskUseCase {
|
|||
task.setProject(null);
|
||||
}
|
||||
|
||||
if (input.history() != null) {
|
||||
if (Objects.nonNull(input.history())) {
|
||||
List<TaskHistoryItem> updatedHistory = input.history().stream().map(hInput -> {
|
||||
if (hInput.id() != null) {
|
||||
if (Objects.nonNull(hInput.id())) {
|
||||
return task.getHistory().stream()
|
||||
.filter(existing -> existing.getId().equals(hInput.id()))
|
||||
.findFirst()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import net.beetechgroup.beetask.entities.organization.UserOrganization;
|
|||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ public class GetUserProfileUseCase {
|
|||
return new UserProfileOutput(
|
||||
user.getName(),
|
||||
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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue