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
4 changes: 2 additions & 2 deletions apps/admin-frontend/.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ When the user asks about features, infrastructure, or cross-cutting concerns, pr
- 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).
- Reference implementations: `CategoriesListPage`/`CategoryEditorDialog` (behavior, design & form structure), `AdminLayout` (page shell).

### Testing

Expand Down Expand Up @@ -107,7 +107,7 @@ Use this agent when:

**Example prompts:**

- "Add a new `ClientsPage` following the Tags reference pattern"
- "Add a new `ClientsPage` following the Categories 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"
2 changes: 1 addition & 1 deletion apps/admin-frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# `dotnet run --project backend/AppHost`.
#
# services-api must stay in scope: it's the audience services-service's
# AddIdentityServiceAuthentication checks, so REST calls (e.g. /api/tags)
# AddIdentityServiceAuthentication checks, so REST calls (e.g. /api/categories)
# 401 without it.

VITE_OIDC_AUTHORITY=http://localhost:5081
Expand Down
34 changes: 21 additions & 13 deletions apps/admin-frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ covers what's specific to `apps/admin-frontend/`.
A multi-tenant SaaS admin panel for small healthcare/wellness businesses.
Built with Clean Architecture, TDD, and strict TypeScript, organized by
feature (ADR 009: `app/`, `features/{auth,catalog}/`, `shared/`). The Auth,
Tags, Categories, and Services verticals are complete end-to-end (frontend +
backend). The remaining feature verticals (Appointments, Clients, Inbox,
Categories, and Services verticals are complete end-to-end (frontend +
backend). Tags was removed from the frontend (docs/adr/016) — the backend
`Tag` domain/`/api/v1/tags` endpoints are intentionally retained. The
remaining feature verticals (Appointments, Clients, Inbox,
Dashboard, Settings) are stubs awaiting implementation, under `app/pages/`
until each graduates into its own feature.

Expand Down Expand Up @@ -67,7 +69,7 @@ infrastructure/presentation`. ESLint (`no-restricted-imports`) and
(MSW fixtures need a feature's internal DTOs) and `app/routes/router.tsx`
lazy-loading catalog's pages by their own path (code-splitting — see
docs/adr/009's "Execution" section for why).
- Tags, Categories, and Services share one `features/catalog/` feature
- Categories and Services share one `features/catalog/` feature
(not one each) — they collaborate in the same business context and
cross-reference each other (a Service has a `categoryId` and `tags`).
- `app/composition/container.ts` is the ONLY place allowed to construct
Expand Down Expand Up @@ -147,11 +149,16 @@ default zero" — the same bar, applied here too.
feature-local. Only _promote_ something to `shared/` on its _second_,
genuinely-identical use across features — the "wait for the second use"
rule gates promotion to `shared/`, not the initial extraction.
- `TagsPage` is the reference for _behavior and design_ (search → table →
dialog create/edit → `AlertDialog` delete-confirm, loading/error/empty
states) — not for _anatomy_. A feature with more workflows (Services:
- `CategoriesListPage`/`CategoryEditorDialog` (`features/catalog/presentation/categories/`)
is the reference for _behavior and design_ (search → table → dialog
create/edit → `AlertDialog` delete-confirm, loading/error/empty states) —
not for _anatomy_. Categories' create/edit dialog is routed
(`/categories/new`, `/categories/:id/edit`, docs/adr/012) rather than
toggled by local state; that routing detail is Categories-specific, not a
requirement for every feature. A feature with more workflows (Services:
filters + pagination + dirty-tracking + inline-create) needs more files
than Tags does; that's a correctly-sized decomposition, not a deviation.
than Categories does; that's a correctly-sized decomposition, not a
deviation.
- Decomposition triggers: multiple independent workflows in one
hook/component, several dialogs, distinct state clusters, a prop list a
reader can't hold in their head, a type cycle between a controller and
Expand All @@ -160,7 +167,7 @@ default zero" — the same bar, applied here too.
a trigger, and splitting a genuinely cohesive 150-line component to hit
a number is not the goal.
- `GenericCrudPage` (or any generic entity-agnostic CRUD abstraction) is
prohibited. Tags/Categories/Services each keep their own page, form, and
prohibited. Categories/Services each keep their own page, form, and
table — share only behavior that's proven identical (`useDialogTarget`,
`useDeleteConfirmation`, `DeleteConfirmationDialog`,
`CollectionFeedback`, all in `shared/`), never a config-driven generic
Expand All @@ -181,7 +188,7 @@ default zero" — the same bar, applied here too.
is available for accessibility assertions — the matcher is registered
globally in `src/test/setup.ts`. Add it to any new or changed form/page
that a screen-reader or keyboard-only user would rely on; see
`TagForm.test.tsx` for the pattern.
`CategoriesRoutes.test.tsx` for the pattern.
- A form field wired through `Controller` (not `register()`) needs its
rendered component to forward a `ref` to a real, focusable DOM node
(`CreatableSingleSelect`/`CreatableMultiSelect` both do) - otherwise
Expand Down Expand Up @@ -264,9 +271,9 @@ page). The short version:
`StatusMessage`, `TextField`/`TextAreaField`, `CenteredScreen`,
`FullScreenSpinner`, `CollectionFeedback`, `DeleteConfirmationDialog`)
— don't hand-roll markup shadcn or an existing composite already covers.
- `TagsPage`/`TagForm` (`features/catalog/presentation/tags/`) is the
reference implementation for a CRUD list+form page (table + dialog)
see "Componentization" above for what "reference" means here.
- `CategoriesListPage`/`CategoryEditorDialog` (`features/catalog/presentation/categories/`)
is the reference implementation for a CRUD list+form page (table + dialog)
see "Componentization" above for what "reference" means here.
`AdminLayout` (`app/layouts/`) is the reference for the page shell,
including its off-canvas mobile sidebar — new pages don't need their
own mobile nav handling.
Expand All @@ -289,6 +296,7 @@ Copy `.env.example` to `.env.local`. Never commit `.env.local`.
- ✅ Tooling, Auth vertical, composition root, presentation shell
- ✅ `HttpClient` (`AuthenticatedHttpClient`) — REST features are unblocked
- ✅ shadcn/ui design system, dark mode, mobile-responsive `AdminLayout`
- ✅ Tags, Categories, Services (frontend + backend, search/filtering, pagination)
- ✅ Categories, Services (frontend + backend, search/filtering, pagination)
- Tags removed from the frontend (docs/adr/016); backend `Tag`/`/api/v1/tags` retained
- ✅ Feature-based physical layout (`app/`, `features/{auth,catalog}/`, `shared/` — ADR 009)
- 🔲 Clients → Appointments → Inbox → Dashboard → Settings
63 changes: 12 additions & 51 deletions apps/admin-frontend/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ Problem Details, always carrying a machine-readable `code`
```json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Já existe uma etiqueta chamada 'VIP'.",
"title": "Já existe uma categoria chamada 'Massagens'.",
"status": 409,
"code": "Tag.DuplicateName"
"code": "Category.DuplicateName"
}
```

Expand Down Expand Up @@ -119,7 +119,7 @@ into application or presentation (docs/adr/007).
`shared/presentation/forms/serverFormError.ts`'s `mapApiErrorToForm` turns
a caught `AppError` into field-level messages a form applies via
react-hook-form's `setError`, reading `AppError.rawFieldErrors`/
`backendCode` — each form (`ServiceForm`/`CategoryForm`/`TagForm`) exports
`backendCode` — each form (`ServiceForm`/`CategoryForm`) exports
its own backend-property → field-name map (e.g. `DurationMinutes` →
`durationMinutes`) and a conflict-`code` → field map (e.g.
`Service.DuplicateName` → `name`).
Expand All @@ -140,53 +140,11 @@ real spec says otherwise — don't invent a different shape.

### Tags

Served by **services-service** (`VITE_API_BASE_URL`). Tenant scope comes
from the `X-Tenant-Id` header, verified against the JWT's `tenant_id`
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/tags` | `200` — `TagDto[]`, ordered by name (asc) |
| `POST` | `/api/v1/tags` | `201` — created `TagDto`, `Location` header |
| `PUT` | `/api/v1/tags/{id}` | `200` — updated `TagDto` |
| `DELETE` | `/api/v1/tags/{id}` | `204` — no body |

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

`DELETE` fails with `409` (`Tag.InUse`) if the tag is still referenced by
one or more Services.

`TagDto`:

```json
{
"id": "0b6e5b3c-8f4e-4a52-9d0e-1c2a3b4c5d6e",
"name": "VIP",
"color": "#0d9488",
"description": "High-value returning client"
}
```

`description` is `null` when unset. Request body for `POST`/`PUT` is the
same shape minus `id` (`description` optional).

Validation rules (server-enforced, mirror them client-side):

- `name`: required, trimmed, 1–40 chars, **unique per tenant**
(case-insensitive) → violations: `400` (shape) / `409` (duplicate)
- `color`: required, must be one of the fixed palette below → `400`
- `description`: optional, trimmed, max 200 chars → `400`
- Unknown `{id}` within the tenant → `404`

Fixed color palette (the only accepted `color` values):

```
#0d9488 (teal) #0ea5e9 (sky) #8b5cf6 (violet) #ec4899 (pink)
#ef4444 (red) #f59e0b (amber) #22c55e (green) #64748b (slate)
```
Removed from the frontend — see `docs/adr/016-remove-tags-frontend.md`.
The backend still serves `/api/v1/tags` (`TagDto` with `id`/`name`/
`color`/`description`, an 8-color fixed palette, `409 Tag.InUse` on
in-use delete) and `ServiceDto` still embeds a `tags`/`tagIds` field (see
Services below) — this app just no longer builds against any of it.

### Categories

Expand Down Expand Up @@ -259,7 +217,10 @@ Response envelope (`PagedResult<ServiceDto>`):
}
```

`TagSummaryDto` (embedded on a `ServiceDto`, a slice of the full Tag):
`TagSummaryDto` (embedded on a `ServiceDto`, a slice of the full Tag) —
still part of the real backend contract even though the frontend Tags
vertical was removed (docs/adr/016-remove-tags-frontend.md); a future
Services UI needs to decide how to handle it:

```json
{ "id": "0b6e5b3c-8f4e-4a52-9d0e-1c2a3b4c5d6e", "name": "VIP", "color": "#0d9488" }
Expand Down
28 changes: 10 additions & 18 deletions apps/admin-frontend/docs/DOMAIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,12 @@ Key fields (confirmed, docs/API.md):
booking time (`0–100`)
- `categoryId` / `categoryName` — optional Category this service
belongs to (`null` when uncategorized)
- `tags` — `TagSummary[]` (`id`/`name`/`color`), a read-only slice of
the Tag catalog attached to this service; managing which tags exist
is the Tags vertical's job, this is just the attachment
- `tags` — the backend still returns a `TagSummary[]` slice (`id`/`name`/
`color`) on this field, since the backend `Tag` domain was intentionally
kept (docs/adr/016-remove-tags-frontend.md in this app's ADRs). The
frontend has no `Tag` entity or Tags vertical anymore — a future Services
UI needs to decide how to handle this field (e.g. reintroducing a minimal
read-only tag type, or dropping it from the form entirely)

Services are **tenant-scoped**. The AI references this list when
answering client questions about what's available.
Expand Down Expand Up @@ -137,21 +140,10 @@ Client history = their list of Appointments under this Business.

## Tag

A tenant-scoped label the business defines to organize its records —
"VIP", "New client", "Allergic to X". In v1 the Tags vertical manages
the tag _catalog_ only; attaching tags to Clients/Conversations ships
with those verticals.

Key fields:

- `id`
- `name` — 1–40 chars, trimmed, unique per Business (case-insensitive)
- `color` — one hex value from the fixed 8-color palette (see API.md);
free-form colors are not allowed
- `description` — optional, max 200 chars, guidance on when to use the tag

Tags are **tenant-scoped**: two businesses can both have a "VIP" tag;
they are unrelated records.
Removed from the frontend domain model — see
`docs/adr/016-remove-tags-frontend.md`. The backend still owns a `Tag`
entity and `/api/v1/tags` endpoints (project-owner decision to retain
them), but this app no longer models or surfaces Tags.

---

Expand Down
Loading
Loading