Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions apps/admin-frontend/.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Senior React & TypeScript Developer

You are a Staff-level React and TypeScript developer specializing in **clean, type-safe React** for the Agenza admin-frontend SaaS application. You write code that adheres to strict architectural principles, Clean Architecture layering, and modern React best practices.

## Scope

This agent handles:

- **React/TypeScript code generation and review** for features within `apps/admin-frontend/`
- **Architecture compliance** — ensuring feature isolation (ADR 009), dependency inversion, and layer boundaries
- **Type safety** — strict TypeScript config enforcement, no `any`, proper discriminated unions
- **Testing strategy** — TDD workflows, fake repositories, MSW mocking, React Testing Library patterns

When the user asks about features, infrastructure, or cross-cutting concerns, propose solutions grounded in the project's ADRs (`docs/adr/`).

## Non-Scope

- Backend API implementation (refer to `backend/AGENTS.md`)
- DevOps, CI/CD, deployment tooling
- Package/dependency upgrades (ask the user first)

## Principles

### Type Safety First

- Never use `any`. Use strict TypeScript types, discriminated unions for state, and narrow types properly.
- Leverage `unknown` for dynamic data; validate at runtime (especially for externally-supplied arrays and numeric IDs).
- When a domain entity's input comes from the generated API, validate at runtime in the domain's `create()` factory, not just at the type level.

### Modern React (v18/v19)

- Functional components and hooks only; no class components or `React.FC`.
- Props are explicit interfaces/types.
- Prefer `useCallback` and `useMemo` judiciously — only when avoiding re-renders solves a real problem (expensive computation, reference stability for deps).
- Never use array index as list key when items can reorder or be deleted.

### Clean Architecture & Separation of Concerns

- **Layering:** Each feature's `domain/` → `application/` → `infrastructure/`/`presentation/` layers are hermetic. Dependencies point inward only.
- **Presentation layer** never imports infrastructure directly; all errors are `AppError` before leaving infrastructure.
- **Hooks/Controllers** are thin wrappers around use cases; complex multi-workflow logic is split into focused hooks, not monolithic controllers.
- **Composition root** is `app/main.tsx`; only place that constructs `AppContainer`.

### Componentization

- A page component is a shell that wires a controller hook's view models into UI components — nothing else.
- Extract a component/hook on first distinct concern; promote to `shared/` only on _second_ identical use.
- Use `src/components/ui/*` (shadcn/ui) as-is; extend only when a real need surfaces.
- Reference implementations: `TagsPage` (behavior & design), `TagForm` (form structure), `AdminLayout` (page shell).

### Testing

- Use case tests → hand-written fake repositories
- Infrastructure tests → MSW handlers (real `HttpClient` code path)
- Presentation tests → fake `AppContainer` from `src/test/fixtures/createFakeAppContainer.ts`
- Every HTTP call needs a registered MSW handler; `onUnhandledRequest: 'error'`
- Add `jest-axe` accessibility checks to new/changed forms and routed pages

### Comments — Minimal by Default

- No comment unless a senior reviewer would get it wrong without it.
- Security/tenant-isolation defaults, concurrency guards, React/Radix/RHF/Zod quirks, or lint suppression only.
- Architectural rationale belongs in `docs/adr/`; reference it in one clause (`see docs/adr/0006`), never restate.

### Configuration Compliance

- `erasableSyntaxOnly: true` — no constructor parameter property shorthand; explicit field + `this.x = x`.
- `exactOptionalPropertyTypes: true` — always guard optional fields; never assign `maybeUndefined` directly.
- `noUncheckedIndexedAccess: true` — index access returns `T | undefined`; always guard.
- `strict: true` — no `any`.
- ESLint rules for layer/feature isolation must not be disabled (`no-restricted-imports`).

## Workflow

1. **Read context first:** If the user mentions a feature, file, or domain, understand the existing code & ADRs before proposing changes.
2. **Type-safe design:** Propose types upfront, validate at runtime where necessary.
3. **Incremental changes:** Prefer small, testable changes; explain architectural decisions with ADR references when non-obvious.
4. **Test coverage:** After code changes, confirm tests pass (`npm run test:coverage --workspace=apps/admin-frontend`).
5. **No speculative code:** Don't add variants, props, or styling "just in case." Add only what's needed now.

## Key Files to Reference

- `.AGENTS.md` — repo-wide rules and non-negotiable constraints
- `docs/adr/` — architectural decisions and their rationale
- `docs/STATUS.md` — current feature state and blockers
- `docs/DOMAIN.md` — domain entity definitions
- `agent-skills/agenza-frontend-feature` — design language, component inventory, mobile checklist
- `.skills/admin-tdd-conventions/SKILL.md` — testing patterns & MSW usage
- `src/test/fixtures/createFakeAppContainer.ts` — fake container for presentation tests

## Communication Style

- **Professional, concise, direct:** Address the immediate request; omit unrelated details.
- **Educate via reference:** When a decision seems surprising, reference the relevant ADR or doc; don't re-explain.
- **Show, don't tell:** Provide working code/tests; minimal preamble.
- **Language:** All user-facing text (labels, messages, aria-labels) is Brazilian Portuguese (pt-BR).

## Invocation Triggers

Use this agent when:

- Building a new feature vertical or page component
- Refactoring existing React/TypeScript code for type safety or architectural compliance
- Writing or debugging tests (especially MSW mocking, fake containers, presentation/use-case layers)
- Reviewing code for adherence to Clean Architecture, layer isolation, or TypeScript strictness
- Questions about why a constraint or pattern exists — refer to ADRs

**Example prompts:**

- "Add a new `ClientsPage` following the Tags reference pattern"
- "Review this controller hook for over-complexity; should I split it?"
- "Why does the component forward a `ref` to the DOM? When is that required?"
- "Add MSW handlers for these endpoints and wire them into the test"
9 changes: 6 additions & 3 deletions apps/admin-frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ run the repo-wide governance checks from
- React Router 8
- oidc-client-ts (Auth Code + PKCE)
- Vitest + React Testing Library + MSW
- Husky + lint-staged

## Design language

Expand All @@ -254,8 +253,12 @@ page). The short version:
Tailwind palette classes. Tokens are what make dark mode work; raw
classes silently break it.
- A list of records is a `Table` (`src/components/ui/table.tsx`), not
stacked `Card`s. A create/edit form always opens in a `Dialog`
modal, never inline or as its own route.
stacked `Card`s. Create/edit forms open in a `Dialog` by default.
Categories maps the nested routes `/categories/new` and
`/categories/:id/edit` to the same editor `Dialog` over the still-mounted
`/categories` list. The dialog reuses one form and one controller hook for
creation and editing. Its table uses compact, record-labelled icon actions
on smartphones and text actions from `sm` upward (docs/adr/012).
- Build pages from `src/components/ui/` (shadcn/ui) and the shared
composites in `shared/presentation/components/` (`PageHeader`,
`StatusMessage`, `TextField`/`TextAreaField`, `CenteredScreen`,
Expand Down
19 changes: 10 additions & 9 deletions apps/admin-frontend/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,16 @@ claim. Routes are versioned (`Asp.Versioning.Mvc`, docs/adr/0005) —
omitting the segment falls back to v1, but the frontend always sends it
explicitly.

| Method | Path | Success |
| -------- | ------------------------- | ----------------------------- |
| `GET` | `/api/v1/categories` | `200` — `CategoryDto[]` |
| `POST` | `/api/v1/categories` | `201` — created `CategoryDto` |
| `PUT` | `/api/v1/categories/{id}` | `200` — updated `CategoryDto` |
| `DELETE` | `/api/v1/categories/{id}` | `204` — no body |

`GET` accepts an optional `search` query param (case-insensitive name
match), e.g. `GET /api/v1/categories?search=massa`.
| Method | Path | Success |
| -------- | ------------------------- | ----------------------------------------------------------------------- |
| `GET` | `/api/v1/categories` | `200` — `CategoryDto[]` |
| `GET` | `/api/v1/categories/{id}` | `200` — `CategoryDto`, `404` if not found (tenant-scoped, docs/adr/013) |
| `POST` | `/api/v1/categories` | `201` — created `CategoryDto` |
| `PUT` | `/api/v1/categories/{id}` | `200` — updated `CategoryDto` |
| `DELETE` | `/api/v1/categories/{id}` | `204` — no body |

`GET` (collection) accepts an optional `search` query param
(case-insensitive name match), e.g. `GET /api/v1/categories?search=massa`.

`DELETE` fails with `409` (`Category.InUse`) if the category is still
referenced by one or more Services.
Expand Down
31 changes: 18 additions & 13 deletions apps/admin-frontend/docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,19 +241,24 @@ each.
a wide table usable at 375px — don't add a second scroll wrapper around
it.

### Form pattern: always a `Dialog` modal

**Decision:** A create/edit form always opens in a `Dialog`
(`src/components/ui/dialog.tsx`) over the list. Never inline in the
page, never its own route.
**Reason:** The project owner chose this explicitly over a dedicated
page/route, for one consistent pattern across every feature vertical —
simpler to build and to maintain than deciding per-vertical.
**Impact:** The list stays mounted and visible behind the dialog (no
navigation, no lost scroll position). `TagsPage` is the reference: a
single `Dialog` instance whose content switches between create/edit
based on which record (if any) triggered it, rather than one dialog per
row.
### Form pattern: one URL-driven `Dialog` editor for Categories

**Decision:** A create/edit form opens in a `Dialog`
(`src/components/ui/dialog.tsx`) over the list by default. Categories maps
the nested routes `/categories/new` and `/categories/:id/edit` to the same
`CategoryEditorDialog`, which renders the same `CategoryForm` for both
operations.
**Reason:** Both workflows edit the same single-field shape. Separate route
components, pages, and hooks duplicated lifecycle and error-handling logic
without adding a distinct interaction. The URLs still provide direct
navigation and predictable browser-history behavior while the list remains
mounted.
**Impact:** `TagsPage` remains the reference for the modal CRUD pattern.
Categories follows docs/adr/012 for the routed-modal shape; per
docs/adr/013, `useCategoryEditor` resolves an edit id by calling
`GET /api/v1/categories/{id}` directly (tenant-scoped) instead of reading
the list's state through outlet context, and the list refetches when
navigation returns to `/categories`.

### Destructive-action confirmation: `AlertDialog`, not `window.confirm`

Expand Down
26 changes: 13 additions & 13 deletions apps/admin-frontend/docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ what's blocked, and what order to build things in.

## Infrastructure

| Piece | Status | Notes |
| -------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| TypeScript strict config | `done` | |
| ESLint + Prettier | `done` | |
| Vitest + RTL + MSW | `done` | |
| Husky + lint-staged | `done` | |
| `HttpClient` interface + `AuthenticatedHttpClient` | `done` | Single per-request session read (token + tenant id together); converts every failure to `AppError` |
| MSW handlers (auth) | `stub` | Auth uses OIDC not REST — no handlers needed |
| MSW handlers (Tags/Categories/Services) | `done` | `tagHandlers.ts`/`categoryHandlers.ts`/`serviceHandlers.ts` |
| MSW handlers (remaining REST features) | `stub` | Add per-feature as specs arrive (Clients, Appointments, Inbox, Settings) |
| shadcn/ui design system (`src/components/ui/`) | `done` | Radix-based, stock "Nova"/neutral theme, unmodified; see ADR 005 |
| `ThemeProvider` / `useTheme` / `ThemeToggle` | `done` | Light/dark, defaults to OS preference, persists an override |
| Piece | Status | Notes |
| -------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------- |
| TypeScript strict config | `done` | |
| ESLint + Prettier | `done` | |
| Vitest + RTL + MSW | `done` | |
| Local Git hooks | `removed` | Quality gates run explicitly during development and in required CI checks |
| `HttpClient` interface + `AuthenticatedHttpClient` | `done` | Single per-request session read (token + tenant id together); converts every failure to `AppError` |
| MSW handlers (auth) | `stub` | Auth uses OIDC not REST — no handlers needed |
| MSW handlers (Tags/Categories/Services) | `done` | `tagHandlers.ts`/`categoryHandlers.ts`/`serviceHandlers.ts` |
| MSW handlers (remaining REST features) | `stub` | Add per-feature as specs arrive (Clients, Appointments, Inbox, Settings) |
| shadcn/ui design system (`src/components/ui/`) | `done` | Radix-based, stock "Nova"/neutral theme, unmodified; see ADR 005 |
| `ThemeProvider` / `useTheme` / `ThemeToggle` | `done` | Light/dark, defaults to OS preference, persists an override |

---

Expand Down Expand Up @@ -106,7 +106,7 @@ create/edit form's pickers, both already built.
| Use cases (List, Create, Update, Delete) | `done` | |
| `ApiCategoryRepository` + `categoryMapper` | `done` | |
| `useCategories` hook | `done` | |
| `CategoriesPage` + nav entry | `done` | Table list, dialog create/edit form, delete with confirm |
| Categories responsive list/editor + nav entry | `done` | Mobile-ready table; shared URL-driven create/edit modal; delete with confirm |
| Backend (services-service `/api/v1/categories`) | `done` | Search/filter added; see docs/adr/0012 for the latest validation/handler shape |

**Dependency:** none. Referenced by Services (optional `categoryId`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependency rule: `domain` ← `application` ← `infrastructure` +

ESLint `no-restricted-imports` rules prevent `domain/` and `application/`
from importing React, react-router, or any outer-layer module. Violations
fail the pre-commit hook and CI.
fail the explicit lint command and CI.

## Consequences

Expand Down
66 changes: 66 additions & 0 deletions apps/admin-frontend/docs/adr/012-routed-category-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ADR 012 — Categories uses one URL-driven modal editor

**Status:** Accepted. The outlet-context sharing described below is
superseded by docs/adr/013 - the routed modal editor itself (one Dialog,
two nested routes) is still accurate.

## Decision

Categories separates its collection, creation, and editing workflows into
three routes:

- `/categories` renders the searchable table and delete confirmation;
- `/categories/new` is a nested route that opens the editor in create mode;
- `/categories/:id/edit` is a nested route that opens the same editor in
edit mode.

`CategoriesListPage` is the route component and composes its controller
hook, responsive table, outlet, and delete confirmation.
Both child routes render `CategoryEditorDialog`. It composes
`useCategoryEditor` with the same `CategoryForm` for creation and editing.
The route parameter selects the operation, title, submit label, initial
values, and mutation.

The list remains a `Table`. On smartphones, record actions use labelled
icons with larger touch targets and the category name can wrap; from the
`sm` breakpoint upward, the action text is also visible.

## Rationale

Creation and editing operate on the same single-field form. Keeping both in
one modal preserves list/search context and avoids parallel components and
hooks for nearly identical workflows. Their nested URLs still provide
direct navigation and browser back/forward behavior without unmounting the
list page.

The parent passes its single `useCategories` source through outlet context.
The editor therefore creates, resolves, and updates against the visible
tenant-scoped source without an independent collection request.

The backend exposes collection listing but no `GET /categories/{id}`.
The editor therefore resolves the requested id from the authenticated
tenant's category collection. A missing id renders a curated not-found
state; it never falls back to data from another tenant.

## Consequences

- `CategoriesListPage` is a composition shell; state machines and use-case
access remain in focused hooks.
- `useCategoriesListPage` instantiates `useCategories` once and provides it
to the nested editor through outlet context.
- `useCategoryEditor` owns the shared submit, structured-error,
loading/not-found, and return-navigation state.
- Closing or submitting either editor mode navigates to `/categories`
while keeping the current list/search state. Browser back closes it too.
- There are no operation-specific creation route or editing page
components.
- There is no redundant list route wrapper.
- Route-level regression tests cover shared modal creation/editing,
direct `/categories/new` access, browser history, preserved list state,
not-found/loading errors, deletion refresh, responsive action semantics,
structured field errors, security, and accessibility.

No architecture guard is added because choosing a routed editor or modal
is a product interaction decision, not a generalizable import or
filesystem invariant. The regression tests run through the existing
frontend coverage command in CI.
Loading
Loading