# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
npm start           # dev server (localhost:4200, uses localhost:5000 as API)
npm run build       # production build
npm run build:dev   # build targeting the dev backend (api.urbino.io)
npm test            # Vitest unit tests
npm run apigen      # regenerate src/app/api/ from OpenAPI spec (backend must run on :5000)
```

## Architecture Overview

This is a general-purpose Angular 21 SPA scaffolded around a reusable core. The "education" domain (schools, users) exists solely to exercise that core — new domains follow the same patterns.

### Project structure

```
src/app/
  core/       # reusable infrastructure (auth, table, validation, layout, lookup, state)
  api/        # OpenAPI-generated HTTP client — do not edit by hand, run apigen
  features/   # lazy-loaded domain modules (home, school, user, …)
  i18n/       # ngx-translate translation files (hu.json only)
```

Path alias `@/*` resolves to `src/*` (e.g. `@/app/core/auth/auth.service`).

### Core systems

**Table system** (`core/table/`)  
The central CRUD pattern. A feature's list page applies the `[tableState]` directive (exported as `#ts="tableState"`) to a host element, passing a `TableStateConfig`. The directive drives the `<core-table>` component and owns: pagination, sorting, filter encoding/decoding into URL query params, column visibility, and reorder mode. Feature code only supplies the `TableStateConfig` and an API load function — everything else is handled by the directive + service pair.

`TableStateConfig` key fields: `columns` (column definitions), `filters` (optional `FilterConfig` keyed by field), `apiLoad`, `apiDelete`, `apiBulkDelete`, `apiReorder`, `translationPrefix`, `actions`.

**Validation system** (`core/validation/`)  
Server-side field errors (HTTP 400 `fieldErrors`) flow through `ValidationInterceptor` → `FormErrorService` → `ValidationFormDirective`. Wrap a form with `[validationForm]` and each field with `<form-field>` to get automatic error display without any feature-level error handling.

**Auth** (`core/auth/`)  
OAuth2 Authorization Code flow via `angular-oauth2-oidc` against a Zitadel OIDC issuer. `AuthService` exposes a readonly `authState` signal (`'loading' | 'authenticated' | 'unauthenticated'`) and role helpers (`hasRole`, `hasAnyRole`). Roles come from the `urn:zitadel:iam:org:project:roles` JWT claim. Route protection: `authGuard(["admin"])` — roles are optional; omitting them requires only authentication.

**Role naming:** lowercase, underscore-separated (e.g. `admin`, `tenant_manager_praga`). Tenant-scoped roles always start with `tenant_` and end with the tenant code (e.g. `tenant_manager_praga`). To check a tenant role use `hasAnyRole(["tenant_manager_" + tenantCode])` or the equivalent `hasRole` helper.

**Tenant context:** the active tenant is sent to the backend in the `Tenant` HTTP request header. There are no tenant subdomains. The current tenant code can be read from `TenantAccessService`.

**Lookup system** (`core/lookup/`)  
Master/reference data (enums, relations) is fetched via `LookupLoaderService` and mapped to `LookupItemDto[]` by `LookupMapperService`. Used as `source` callbacks in `FilterConfig` and in form select inputs.

**Navigation state** (`core/state/nav.store.ts`)  
`NavStore` is a signals-based store holding page title and breadcrumbs. Feature pages inject it to set their own title/breadcrumb on init.

**Layout** (`core/layout/main/`)  
`MainLayout` wraps all authenticated routes. Auth-independent pages (callback, logout, 403, 404) render outside it.

### State management

No NgRx. State lives in Angular signals, with RxJS used only for HTTP and OAuth event streams. `toSignal()` bridges where needed.

### API layer

`src/app/api/` is fully generated — running `npm run apigen` overwrites it. The generator targets `typescript-angular`. Configuration (base URL) comes from `environment.ts` / `environment.dev.ts` switched by the Angular build configuration.

### Styling

Tailwind CSS v4 + PrimeNG v21 (Aura theme, Blue primary). Global styles in `src/styles.css`. Component-level styles are minimal; prefer Tailwind utilities and PrimeNG classes.

### i18n

`@ngx-translate` with a static loader. All strings are in `src/i18n/hu.json`. The `translationPrefix` option on `TableStateConfig` auto-generates column headers from the translation file.

### CI/CD

Woodpecker CI (`../.woodpecker/backend-spa-dev.yml`): build → Docker image → deploy webhook. Targets the `develop` branch. Image based on `nginx:stable-alpine`; built dist is served as static files.

### BaseFormComponent (`core/components/base-form/`)

Minden szerkesztő form alaposztálya. Két generikus paramétere van: az API service típusa és a form model típusa. Biztosítja a betöltés / mentés / validáció életciklust.

**`loaded` signal:** `loaded = signal(false)`. Új entitásnál azonnal `true`; meglévőnél az API válasz után, az `afterLoad()` hook lefutásával egyidőben áll `true`-ra. Célja: szinkronizálni az alárendelt komponenseket (pl. `GalleryComponent`) a szülő adatbetöltésével — megakadályozza, hogy a child korábban indítson API hívást, mint hogy a szülőnek megvannak az adatai (pl. a galéria ID).

### GalleryComponent (`core/components/gallery/`)

Selector: `app-gallery`. Implementálja a `ControlValueAccessor` interfészt, ezért `formControlName`-mel köthető be; értéke `number | null` (galéria ID).

**Inputok:**
- `type: GalleryType` — `GalleryType.Tenant` (default) vagy `GalleryType.Core`; meghatározza, melyik API endpointot használja (`/v1/gallery` vs `/v1/core-gallery`)
- `readOnly: boolean` — ha true, letiltja a feltöltést/törlést/átrendezést
- `loaded: boolean` — a szülő `BaseFormComponent.loaded()` értékét kell ide kötni

**Működési minta:** Amikor `loaded` `false`→`true`-ra vált (`ngOnChanges`), a komponens lekér egy galériát az API-ról. Ha a form control értéke még null (új entitás), automatikusan létrehoz egy új galériát és visszaírja az ID-t a formba. Ha már van ID (meglévő entitás szerkesztésekor), betölti a hozzá tartozó képeket.

**Képkezelés:** CDK drag-drop alapú átrendezés (sorrend mentése `POST /{id}/order`), batch feltöltés (`forkJoin`), egyedi törlés. A `filePath` mező az API-tól URL-ként érkezik, új ablakban megnyitható.

**Tipikus használat egy form template-ben:**
```html
<app-gallery formControlName="galleryId" [type]="GalleryType.Tenant" [loaded]="loaded()" />
```

### Adding a new feature

1. Create `src/app/features/<name>/` with `<name>.routes.ts` and lazy-loaded page components.
2. Register the route in `app.routes.ts` under `MainLayout`, with `authGuard` if needed.
3. Generate or reuse API services via `apigen`.
4. Use `TableStateConfig` + `[tableState]` for list pages; use `[validationForm]` + `<form-field>` for create/update forms.
5. Add translations to `src/i18n/hu.json`.
