From 064a29cc215d42f44344a3da524581e11e76e0bc Mon Sep 17 00:00:00 2001 From: Gabriel Santana de Menezes Date: Sat, 9 May 2026 15:36:04 -0400 Subject: [PATCH] fix: separar response do controller do output do use case Cria XxxResponse records e OrgDashboardControllerMapper na camada do controller, removendo o vazamento do OrgDashboardOutput para a camada HTTP. Documenta a regra no CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 194 ++++++++---------- .../orgdashboard/OrgDashboardController.java | 7 +- .../OrgDashboardControllerMapper.java | 22 ++ .../orgdashboard/OrgDashboardResponse.java | 9 + .../orgdashboard/OrgMemberStatsResponse.java | 9 + .../orgdashboard/OrgProjectStatsResponse.java | 3 + .../orgdashboard/OrgTopTaskResponse.java | 3 + 7 files changed, 132 insertions(+), 115 deletions(-) create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardControllerMapper.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardResponse.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgMemberStatsResponse.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgProjectStatsResponse.java create mode 100644 backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgTopTaskResponse.java diff --git a/CLAUDE.md b/CLAUDE.md index 0202e82..6f5d0d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,145 +1,115 @@ -# Bee Task — Guia de Desenvolvimento +# CLAUDE.md -## Stack +Guidance for Claude Code when working in this repository. Keep edits aligned with the conventions below. -- **Backend**: Quarkus + Hibernate ORM + Panache (Java 17+) -- **Frontend**: React + TypeScript + Tailwind CSS -- **Banco de dados**: PostgreSQL (via Docker Compose) +## Project -## Arquitetura +**bee-task-v3 / TimeTrack** — task & time-tracking SaaS. Monorepo with React (Vite) frontend and Quarkus backend, PostgreSQL + Keycloak via Docker. Backlog and user stories live in `backlog.md`. -O backend segue **Clean Architecture** adaptada: +## Layout ``` -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 +backend/ Quarkus 3.34, Java 17, Hibernate ORM + Panache, Flyway, JWT (smallrye) +frontend/ Vite + React 19 + TypeScript + Tailwind v4 +docker/ Postgres init scripts +docker-compose.yml Postgres + Keycloak +specification/ Domain specs +backlog.md User stories (US-001…) ``` -**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` +Java root package: `net.beetechgroup.beetask`. -## Padrões de Código Java +## Commands -### Repositories — Sem SQL nativo e sem JPQL explícito +Infra: `docker-compose up` (Postgres :5432, Keycloak :8081). -Nas implementações de repository (`frameworks/persistence/repository/`), use **sempre** o estilo shorthand do Panache: +Backend (`cd backend`): +- Dev: `./mvnw quarkus:dev` (port 8080, uses H2 in dev profile, swagger at `/swagger`, OpenAPI at `/openapi`) +- Test: `./mvnw test` +- Build: `./mvnw package` -```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); +Frontend (`cd frontend`): +- Dev: `npm run dev` (port 5173, proxies `/api/*` → `http://localhost:8080`) +- Build: `npm run build` (runs `tsc -b` then `vite build`) +- Lint: `npm run lint` -// 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 ...", ...); +## Backend Architecture (Clean Architecture, adapted) -// ERRADO — SQL nativo -getEntityManager().createNativeQuery("SELECT * FROM tasks WHERE ..."); -``` +Four layers, dependencies point inward. **Never** import outward. -Para consultas complexas que envolvem JOINs com coleções, prefira buscar via shorthand e filtrar em Java: +| Package | Role | May depend on | +|---|---|---| +| `entities/` | Pure domain POJOs and enums (`Task`, `Project`, `User`, `Organization`, `TaskStatus`, …). Plain Java, no annotations. | nothing | +| `usecase///` | Business logic. Plain classes with constructor DI. Records for `XxxInput`/`XxxOutput`. Static `XxxMapper` for domain→Output. | `entities`, `usecase.repository` | +| `usecase/repository/` | Repository **ports** (interfaces) returning domain types. | `entities` | +| `interfaceadapters/controllers//` | JAX-RS controllers, `XxxRequest`/`XxxResponse` records, `XxxControllerMapper`. Translates HTTP ↔ use case I/O. | `usecase`, `entities` | +| `frameworks/persistence/` | Panache `XxxEntity`, `XxxRepositoryImpl` (implements port + `PanacheRepository`), `XxxEntityMapper` (entity ↔ domain). | everything | +| `frameworks/config/` | `@ApplicationScoped` classes with `@Produces` methods that build use cases from injected repository ports. | everything | -```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(); -``` +### Use case anatomy -### Verificações de nulo — `Objects.isNull` e `Objects.nonNull` +A new use case `usecase///` contains exactly: +- `XxxInput.java` — `record` with primitives/IDs/email (no domain entities in the signature unless intentional) +- `XxxOutput.java` — `record` with the response shape +- `XxxUseCase.java` — plain class, constructor-injected repository ports, single `execute(XxxInput)` method +- `XxxMapper.java` — static `toXxxOutput(domain)` only -Use sempre `Objects.isNull(x)` e `Objects.nonNull(x)` ao invés dos operadores `== null` e `!= null`: +Then **wire it** via `@Produces` in the matching `frameworks/config/UseCaseConfig.java`. Use cases are NOT annotated with CDI scopes themselves — the producer is what makes them injectable. -```java -// CORRETO -if (Objects.isNull(entity.getId())) { - persist(entity); -} +### Controller anatomy -if (Objects.nonNull(input.projectId())) { - task.setProject(projectRepository.findProjectById(input.projectId()).orElseThrow()); -} +`interfaceadapters/controllers//`: +- `XxxController.java` — `@Path`, JAX-RS verbs, `@Inject` use cases + `SecurityIdentity`, OpenAPI annotations (`@Tag`, `@Operation`, `@APIResponse`) +- `XxxRequest.java` / `XxxResponse.java` — records mirroring the use case Input/Output (don't reuse use case records in HTTP layer) +- `XxxControllerMapper.java` — static `toXxxInput(request, email)` / `toXxxResponse(output)` -// ERRADO -if (entity.getId() == null) { ... } -if (input.projectId() != null) { ... } -``` +The authenticated email comes from `securityIdentity.getPrincipal().getName()` and is passed into the Input. -Isso se aplica a: repositories, mappers, use cases e entities. -Exceção tolerada: ternários simples inline onde a legibilidade é melhor com `!=`. +### Repository pattern -### Mappers de persistência +- Port: `usecase/repository/XxxRepository.java`, methods return **domain** types. +- Impl: `frameworks/persistence/repository/XxxRepositoryImpl.java`: + - `@ApplicationScoped` + - `implements XxxRepository, PanacheRepository` + - `@Transactional` on writes + - Uses `XxxEntityMapper.toEntity` / `toDomain` at the boundary -Sempre verifique nulo na entrada de métodos de mapeamento: +### DB / migrations -```java -public static MyEntity toEntity(MyDomain domain) { - if (Objects.isNull(domain)) return null; - // ... -} -``` +- Postgres in prod, **H2 in dev** (`%dev` profile, file at `backend/data/beetask`). +- `quarkus.hibernate-orm.database.generation=validate` (prod) / `update` (dev). +- Flyway migrations live at `backend/src/main/resources/db/migration/V{N}__{snake_name}.sql`. +- `quarkus.flyway.migrate-at-start=false` — migrations are not auto-applied; run them deliberately. -### Padrão de save nos repositories +### Auth -```java -if (Objects.isNull(entity.getId())) { - persist(entity); -} else { - entity = getEntityManager().merge(entity); -} -``` +- JWT signed with `privateKey.pem`, verified with `publicKey.pem`. Issuer `https://beetech.net`. +- Keycloak is provisioned in docker-compose but **not yet integrated** into the app — JWTs are minted by `usecase/auth/login`. -## Workflow de Tarefas +## Frontend Conventions -Use o skill `/new-task` para seguir o guia completo de início ao PR. +- **React 19**, **Tailwind v4** (`@tailwindcss/vite`, no Tailwind config file required), **react-router-dom v7**. +- Folder layout: `pages/`, `components/`, `services/`, `contexts/`, `types/`, `lib/`. +- HTTP through `apiFetch` in `src/lib/api.ts` — it auto-injects the JWT from `localStorage.user.jwt` and prefixes `VITE_API_URL` (defaults to `/api`, proxied by Vite to the backend). +- Each backend feature gets a service in `services/Service.ts` exporting an object of methods. +- Class merging uses `cn(...inputs)` from `src/lib/utils.ts` (clsx + tailwind-merge). +- Icons: `lucide-react`. Animations: `framer-motion`. DnD: `@hello-pangea/dnd`. +- Auth state: `AuthContext` (`src/contexts/AuthContext.tsx`) — `user`, `activeOrg`, `login`, `logout`, `refreshUser`. Persisted in `localStorage` (`user`, `activeOrgId`). +- Authenticated routes nest under `` in `src/router.tsx`. Role-restricted routes wrap with ``. +- UI strings are in pt-BR. Keep that consistent. -### Branches +## Conventions & Constraints -| Tipo | Prefixo | -|------|---------| -| Funcionalidade | `feat/` | -| Bug | `fix/` | -| Manutenção | `chore/` | -| Testes | `test/` | +- Don't put business logic in controllers — they only translate HTTP ↔ use case I/O. +- **Never return a use case Output record directly from a controller.** Controllers must define their own `XxxResponse` records and use `XxxControllerMapper.toResponse(output)` to convert. Use case I/O types must not leak into the HTTP layer. +- Don't import `jakarta.*`, `io.quarkus.*`, or Panache from `entities/` or `usecase/` (except `usecase.repository` ports which are pure interfaces). +- Don't reach for a UI library on the frontend — Tailwind only. +- Don't create files outside the conventional folders. +- Vertical slices first; refactor when a pattern repeats. The backlog is in `backlog.md`. -### Commits +## Skills -Formato: `: ` - -``` -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 #` - -## 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 -``` +Project skills live in `.claude/skills/`: +- `scaffold-usecase` — generate a backend use case + controller wiring. +- `scaffold-feature` — generate a frontend page + service + types + route. +- `architecture-review` — audit Clean Architecture violations. diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardController.java index 345ea42..5b37fdb 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardController.java @@ -10,7 +10,6 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import net.beetechgroup.beetask.usecase.orgdashboard.OrgDashboardInput; -import net.beetechgroup.beetask.usecase.orgdashboard.OrgDashboardOutput; import net.beetechgroup.beetask.usecase.orgdashboard.OrgDashboardUseCase; import java.time.LocalDateTime; @@ -28,7 +27,7 @@ public class OrgDashboardController { SecurityIdentity securityIdentity; @GET - public OrgDashboardOutput getDashboard( + public OrgDashboardResponse getDashboard( @PathParam("id") Long organizationId, @QueryParam("startDate") String startDateStr, @QueryParam("endDate") String endDateStr) { @@ -47,6 +46,8 @@ public class OrgDashboardController { end = end.with(java.time.LocalTime.MAX); } - return orgDashboardUseCase.execute(new OrgDashboardInput(email, organizationId, start, end)); + return OrgDashboardControllerMapper.toResponse( + orgDashboardUseCase.execute(new OrgDashboardInput(email, organizationId, start, end)) + ); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardControllerMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardControllerMapper.java new file mode 100644 index 0000000..d392346 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardControllerMapper.java @@ -0,0 +1,22 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.orgdashboard; + +import net.beetechgroup.beetask.usecase.orgdashboard.OrgDashboardOutput; + +public class OrgDashboardControllerMapper { + + private OrgDashboardControllerMapper() {} + + public static OrgDashboardResponse toResponse(OrgDashboardOutput output) { + return new OrgDashboardResponse( + output.projectStats().stream() + .map(p -> new OrgProjectStatsResponse(p.projectId(), p.projectName(), p.totalMinutes())) + .toList(), + output.topTasks().stream() + .map(t -> new OrgTopTaskResponse(t.taskId(), t.title(), t.projectName(), t.totalMinutes())) + .toList(), + output.memberStats().stream() + .map(m -> new OrgMemberStatsResponse(m.userId(), m.userName(), m.userPhoto(), m.finishedTasksCount(), m.totalMinutesWorked())) + .toList() + ); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardResponse.java new file mode 100644 index 0000000..b80c05d --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgDashboardResponse.java @@ -0,0 +1,9 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.orgdashboard; + +import java.util.List; + +public record OrgDashboardResponse( + List projectStats, + List topTasks, + List memberStats +) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgMemberStatsResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgMemberStatsResponse.java new file mode 100644 index 0000000..dbcc4db --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgMemberStatsResponse.java @@ -0,0 +1,9 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.orgdashboard; + +public record OrgMemberStatsResponse( + Long userId, + String userName, + String userPhoto, + long finishedTasksCount, + long totalMinutesWorked +) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgProjectStatsResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgProjectStatsResponse.java new file mode 100644 index 0000000..c15e436 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgProjectStatsResponse.java @@ -0,0 +1,3 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.orgdashboard; + +public record OrgProjectStatsResponse(Long projectId, String projectName, long totalMinutes) {} diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgTopTaskResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgTopTaskResponse.java new file mode 100644 index 0000000..a2c2471 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/OrgTopTaskResponse.java @@ -0,0 +1,3 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.orgdashboard; + +public record OrgTopTaskResponse(Long taskId, String title, String projectName, long totalMinutes) {}