## ISA Frontend – AI Assistant Working Rules

Concise, project-specific guidance so an AI agent can be productive quickly. Focus on THESE patterns; avoid generic boilerplate.

### 1. Monorepo & Tooling

- Nx workspace (Angular 20 + Libraries under `libs/**`, main app `apps/isa-app`).
- Scripts (see `package.json`):
  - Dev serve: `npm start` (=> `nx serve isa-app --ssl`).
  - Library tests (exclude app): `npm test` (Jest + emerging Vitest). CI uses `npm run ci`.
  - Build dev: `npm run build`; prod: `npm run build-prod`.
  - Storybook: `npm run storybook`.
  - Swagger codegen: `npm run generate:swagger` then `npm run fix:files:swagger`.
- Default branch in Nx: `develop` (`nx.json: defaultBase`). Use affected commands when adding libs.
- Node >=22, TS 5.8, ESLint flat config (`eslint.config.js`).

### 1.a Project Tree (Detailed Overview)

```
.
├─ apps/
│  └─ isa-app/                         # Main Angular app (Jest). Legacy non-standalone root component pattern.
│     ├─ project.json                  # Build/serve/test targets
│     ├─ src/
│     │  ├─ main.ts / index.html       # Angular bootstrap
│     │  ├─ app/main.component.ts      # Root component (standalone:false)
│     │  ├─ environments/              # Environment files (prod replace)
│     │  ├─ assets/                    # Static assets
│     │  └─ config/                    # Runtime config JSON (read via Config service)
│     └─ .storybook/                   # App Storybook config
│
├─ libs/                                # All reusable code (grouped by domain / concern)
│  ├─ core/                             # Cross-cutting infrastructure
│  │  ├─ logging/                       # Logging service + providers + sinks
│  │  │  ├─ src/lib/logging.service.ts
│  │  │  ├─ src/lib/logging.providers.ts
│  │  │  └─ README.md                   # Full API & patterns
│  │  ├─ config/                        # `Config` service (Zod validated lookup)
│  │  └─ storage/                       # User-scoped storage + signal store feature (`withStorage`)
│  │     ├─ src/lib/signal-store-feature.ts
│  │     └─ src/lib/storage.ts
│  │
│  ├─ shared/                           # Shared UI/services not domain specific
│  │  └─ scanner/                       # Scandit integration (tokens, service, components, platform gating)
│  │     ├─ src/lib/scanner.service.ts
│  │     └─ src/lib/render-if-scanner-is-ready.directive.ts
│  │
│  ├─ remission/                        # Remission domain features (newer pattern; Vitest)
│  │  ├─ feature/
│  │  │  ├─ remission-return-receipt-details/
│  │  │  │  ├─ vite.config.mts         # Signals + Vitest example
│  │  │  │  └─ src/lib/resources/      # Resource factories (signals async pattern)
│  │  │  └─ remission-return-receipt-list/
│  │  └─ shared/                        # Dialogs / shared remission UI pieces
│  │
│  ├─ common/                           # Cross-domain utilities (decorators, print, data-access)
│  ├─ utils/                            # Narrow utility libs (ean-validation, z-safe-parse, etc.)
│  ├─ ui/                               # Generic UI components (presentational)
│  ├─ icons/                            # Icon sets / wrappers
│  ├─ catalogue/                        # Domain area (legacy Jest)
│  ├─ customer/                         # Domain area (legacy Jest)
│  └─ oms/                              # Domain area (legacy Jest)
│
├─ generated/swagger/                   # Generated API clients (regen via scripts; do not hand edit)
├─ tools/                               # Helper scripts (e.g. swagger fix script)
├─ testresults/                         # JUnit XML (jest-junit). CI artifact pickup.
├─ coverage/                            # Per-project coverage outputs
├─ tailwind-plugins/                    # Custom Tailwind plugin modules used by `tailwind.config.js`
├─ vitest.workspace.ts                  # Glob enabling multi-lib Vitest detection
├─ nx.json / package.json               # Workspace + scripts + defaultBase=develop
└─ eslint.config.js                     # Flat ESLint root config
```

Guidelines: create new code in the closest domain folder; expose public API via each lib `src/index.ts`; follow existing naming (`feature-name.type.ts`). Keep generated swagger untouched—extend via wrapper libs if needed.

### 1.b Import Path Aliases

Use existing TS path aliases (see `tsconfig.base.json`) instead of long relative paths:

Core / Cross-cutting:

- `@isa/core/logging`, `@isa/core/config`, `@isa/core/storage`, `@isa/core/tabs`, `@isa/core/notifications`

Domain & Features:

- Catalogue: `@isa/catalogue/data-access`
- Customer: `@isa/customer/data-access`
- OMS features: `@isa/oms/feature/return-details`, `.../return-process`, `.../return-review`, `.../return-search`, `.../return-summary`
- OMS shared: `@isa/oms/shared/product-info`, `@isa/oms/shared/task-list`
- Remission: `@isa/remission/data-access`, feature libs (`@isa/remission/feature/remission-return-receipt-details`, `...-list`) and shared (`@isa/remission/shared/remission-start-dialog`, `.../search-item-to-remit-dialog`, `.../return-receipt-actions`, `.../product`)

Shared / UI:

- Shared libs: `@isa/shared/scanner`, `@isa/shared/filter`, `@isa/shared/product-image`, `@isa/shared/product-router-link`, `@isa/shared/product-format`
- UI components: `@isa/ui/buttons`, `@isa/ui/dialog`, `@isa/ui/input-controls`, `@isa/ui/layout`, `@isa/ui/menu`, `@isa/ui/toolbar`, etc. (one alias per folder under `libs/ui/*`)
- Icons: `@isa/icons`

Utilities:

- `@isa/utils/ean-validation`, `@isa/utils/z-safe-parse`, `@isa/utils/scroll-position`

Generated Swagger Clients:

- `@generated/swagger/isa-api`, `@generated/swagger/oms-api`, `@generated/swagger/inventory-api`, etc. (one per subfolder). Never edit generated sources—wrap in a domain lib if extension needed.

App-local (only inside `apps/isa-app` context):

- Namespaced folders: `@adapter/*`, `@domain/*`, `@hub/*`, `@modal/*`, `@page/*`, `@shared/*` (and nested: `@shared/components/*`, `@shared/services/*`, etc.), `@ui/*`, `@utils/*`, `@swagger/*`.

Patterns:

- Always add new reusable code as a library then expose via an `@isa/...` alias; do not add new generic code under app-local aliases if it may be reused later.
- When introducing a new library ensure its `src/index.ts` re-exports only stable public surface; internal helpers stay un-exported.
- For new generated API groups, extend via thin wrappers in a domain `data-access` lib rather than patching generated code.

### 2. Testing Strategy

- Legacy tests: Jest (`@nx/jest:jest`). New feature libs (e.g. remission feature) use Vitest + Vite plugin (`vite.config.mts`).
- When adding a new library today prefer Vitest unless consistency with existing Jest-only area is required.
- Do NOT mix frameworks inside one lib. Check presence of `vite.config.*` to know it is Vitest-enabled.
- App (`isa-app`) still uses Jest.

### 3. Architecture & Cross-Cutting Services

- Core libraries underpin features: `@isa/core/logging`, `@isa/core/config`, `@isa/core/storage`.
- Feature domains grouped (e.g. `libs/remission/**`, `libs/shared/**`, `libs/common/**`). Keep domain-specific code there; UI-only pieces in `ui/` or `shared/`.
- Prefer standalone components but some legacy components set `standalone: false` (see `MainComponent`). Maintain existing pattern unless doing a focused migration.

### 4. Logging (Critical Pattern)

- Central logging via `@isa/core/logging` (files: `logging.service.ts`, `logging.providers.ts`).
- Configure once in app config using provider builders: `provideLogging(withLogLevel(...), withSink(ConsoleLogSink), withContext({...}))`.
- Use factory `logger(() => ({ dynamicContext }))` (see README) rather than injecting `LoggingService` directly unless extending framework code.
- Context hierarchy: global -> component (`provideLoggerContext`) -> instance (factory param) -> message (callback arg). Always pass context as lazy function `() => ({ ... })` for perf.
- Respect log level threshold; do not perform expensive serialization before calling (let sinks handle it or gate behind dev checks).

### 5. Configuration Access

- Use `Config` service (`@isa/core/config/src/lib/config.ts`). Fetch values with Zod schema: `config.get('licence.scandit', z.string())` (see `SCANDIT_LICENSE` token). Avoid deprecated untyped access.

### 6. Storage & State Persistence

- Storage abstraction: `injectStorage(SomeProvider)` wraps a `StorageProvider` (local/session/indexedDB/custom user storage) and prefixes keys with current authenticated user `sub` (OAuth `sub` fallback 'anonymous').
- When adding persisted signal stores, use `withStorage(storageKey, ProviderType)` feature (`signal-store-feature.ts`) to auto debounce-save (1s) + restore on init. Only pass plain serializable state.

### 7. Signals & State

- Internal state often via Angular signals & NgRx Signals (`@ngrx/signals`). Avoid manual subscriptions—prefer computed/signals and `rxMethod` for side effects.
- When persisting, ensure objects are JSON-safe; validation via Zod if deserializing external data.

#### 7.a NgRx Signals Deep Dive

Core building blocks we use:

- `signalStore(...)` + features: `withState`, `withComputed`, `withMethods`, `withHooks`, `withStorage` (custom feature in `core/storage`).
- `rxMethod` (from `@ngrx/signals/rxjs-interop`) to bridge imperative async flows (HTTP calls, debounce, switchMap) into store-driven mutations.
- `getState`, `patchState` for immutable, shallow merges; avoid manually mutating nested objects—spread + patch.

Patterns:

1. Store Shape: Keep initial state small & serializable (no class instances, functions, DOM nodes). Derive heavy or view-specific projections with `withComputed`.
2. Side Effects: Wrap fetch/update flows inside `rxMethod` pipes; ensure cancellation semantics (`switchMap`) to drop stale requests.
3. Persistence: Apply `withStorage(key, Provider)` last so hooks run after other features; persisted state must be plain JSON (no Dates—convert to ISO strings). Debounce already handled (1s) in `withStorage`—do NOT add another debounce upstream unless burst traffic is extreme.
4. Error Handling: Keep an `error` field in state for presentation; log via `logger()` at Warn/Error levels but do not store full Error object (serialize minimal fields: `message`, maybe `code`).
5. Loading Flags: Prefer a boolean `loading` OR a discriminated union `status: 'idle'|'loading'|'success'|'error'` for richer UI; avoid multiple booleans that can drift.
6. Computed Selectors: Name as `XComputed` or just semantic (e.g. `filteredItems`) using `computed(() => ...)` inside `withComputed`; never cause side-effects in a computed.
7. Resource Factory Pattern: For remote data needed in multiple components, create a factory function returning an object with `value`, `isLoading`, `error` signals plus a `reload()` method; see remission `resources/` directory.

Store Lifecycle Hooks:

- Use `withHooks({ onInit() { ... }, onDestroy() { ... } })` for restoration, websockets, or timers. Pair cleanups explicitly.

Persistence Feature (`withStorage`):

- Implementation: Debounced `storeState` rxMethod listens to any state change, saves hashed user‑scoped key (see `hash.utils.ts`). On init it calls `restoreState()`.
- Extending: If you need to blacklist transient fields from persistence, add a method wrapping `getState` and remove keys before `storage.set` (extend feature locally rather than editing shared code unless broadly needed).

Typical Store Template:

```ts
// feature-x.store.ts
import {
  signalStore,
  withState,
  withComputed,
  withMethods,
  withHooks,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { debounceTime, switchMap, tap, catchError, of } from 'rxjs';
import { withStorage } from '@isa/core/storage';
import { logger } from '@isa/core/logging';

interface FeatureXState {
  items: ReadonlyArray<Item>;
  query: string;
  loading: boolean;
  error?: string;
}

const initialState: FeatureXState = { items: [], query: '', loading: false };

export const FeatureXStore = signalStore(
  withState(initialState),
  withProps((store, logger = logger(() => ({ store: 'FeatureX' }))) => ({
    _logger: logger,
  })),
  withComputed(({ items, query }) => ({
    filtered: computed(() => items().filter((i) => i.name.includes(query()))),
    hasError: computed(() => !!query() && !items().length),
  })),
  withMethods((store) => ({
    setQuery: (q: string) => patchState(store, { query: q }),
    // rxMethod side effect to load items
    loadItems: rxMethod<string | void>(
      pipe(
        debounceTime(150),
        tap(() => patchState(store, { loading: true, error: undefined })),
        switchMap(() =>
          fetchItems(store.query()).pipe(
            tap((items) => patchState(store, { items, loading: false })),
            catchError((err) => {
              store._logger.error('Load failed', err as Error, () => ({
                query: store.query(),
              }));
              patchState(store, {
                loading: false,
                error: (err as Error).message,
              });
              return of([]);
            }),
          ),
        ),
      ),
    ),
  })),
  withHooks((store) => ({
    onInit() {
      store.loadItems();
    },
  })),
  withStorage('feature-x', LocalStorageProvider),
);
```

Testing Signal Stores (Vitest or Jest):

- Use `runInInjectionContext(TestBed.inject(Injector), () => FeatureXStore)` or instantiate via exported factory if provided.
- For async rxMethod flows, flush microtasks (`await vi.runAllTimersAsync()` if timers used) or rely on returned observable completion when you subscribe inside the test harness.
- Snapshot only primitive slices (avoid full object snapshots with volatile ordering).

Migration Tips:

- Converting legacy NgRx reducers: Start by lifting static initial state + selectors into `withState` + `withComputed`; replace effects with `rxMethod` maintaining cancellation semantics (`switchMap` mirrors effect flattening strategy).
- Keep action names only if externally observed (analytics, logging). Otherwise remove ceremony—call store methods directly.

Anti-Patterns to Avoid:

- Writing to signals inside a computed or inside another signal setter (causes cascading updates).
- Storing large unnormalized arrays and then repeatedly filtering/sorting in multiple components—centralize that in computed selectors.
- Persisting secrets or PII directly; hash keys already user-scoped but content still plain—sanitize if needed.
- Returning raw subscriptions from store methods; expose signals or idempotent methods only.

#### 7.b Prefer Signals over Observables (Practical Rules)

Default to signals for all in-memory UI & derived state; keep Observables only at I/O edges.

Use Observables for:

- HTTP / WebSocket / SignalR streams at the boundary.
- Timer / interval / external event sources.
- Interop with legacy NgRx store pieces not yet migrated.

Immediately convert inbound Observables to signals:

```ts
// Legacy service returning Observable<Item[]>
items$ = http.get<Item[]>(url);
// New pattern
const items = toSignal(http.get<Item[]>(url), { initialValue: [] });
```

Expose signals from stores & services:

```ts
// BAD (forces template async pipe + subscription mgmt)
getItems(): Observable<Item[]> { return this.http.get(...); }

// GOOD
items = toSignal(this.http.get<Item[]>(url), { initialValue: [] });
```

Bridge when needed:

```ts
// Signal -> Observable (rare):
const queryChanges$ = fromSignal(query, { requireSync: true });

// Observable -> Signal (preferred):
const data = toSignal(data$, { initialValue: undefined });
```

Side-effects: never subscribe manually—wrap in `rxMethod` (cancels stale work via `switchMap`).

```ts
loadData: rxMethod<void>(
  pipe(
    switchMap(() =>
      this.api.fetch().pipe(tap((r) => patchState(store, { data: r }))),
    ),
  ),
);
```

Template usage: reference signals directly (`{{ item.name }}`) or in control flow; no `| async` needed.

Replacing combineLatest / map chains:

```ts
// Before (Observable)
vm$ = combineLatest([a$, b$]).pipe(map(([a, b]) => buildVm(a, b)));

// After (Signals)
const vm = computed(() => buildVm(a(), b()));
```

Debounce / throttle user input:
Keep raw form value as a signal; create an rxMethod for debounced fetch instead of debouncing inside a computed.

```ts
search = signal('');
runSearch: rxMethod<string>(
  pipe(
    debounceTime(300),
    switchMap((term) =>
      this.api
        .search(term)
        .pipe(tap((results) => patchState(store, { results }))),
    ),
  ),
);
effect(() => {
  runSearch(this.search());
});
```

Avoid converting a signal back to an Observable just to use a single RxJS operator; prefer inline signal `computed` or small helper.

Migration heuristic:

1. Identify component `foo$` fields used only in template -> convert to signal via `toSignal`.
2. Collapse chains of `combineLatest` + `map` into `computed`.
3. Replace imperative `subscribe` side-effects with `rxMethod` + `patchState`.
4. Add persistence last via `withStorage` if state must survive reload.

Performance tip: heavy derived computations (sorting large arrays) belong in a memoized `computed`; if expensive & infrequently needed, gate behind another signal flag.

### 8. Scanner Integration (Scandit)

- Barcode scanning encapsulated in `@isa/shared/scanner` (`scanner.service.ts`). Use provided injection tokens for license & defaults (override via DI if needed). Service auto-configures once; `ready` signal triggers `configure()` lazily.
- Always catch and log errors with proper context; platform gating throws `PlatformNotSupportedError` which is downgraded to warn.

### 9. Styling

- Tailwind with custom semantic tokens (`tailwind.config.js`). Prefer design tokens like `text-isa-neutral-700`, spacing utilities with custom `px-*` scales rather than ad‑hoc raw values.
- Global overlays rely on CDK classes; retain `@angular/cdk/overlay-prebuilt.css` in style arrays when creating new entrypoints or Storybook stories.

### 10. Library Conventions

- File naming: kebab-case; feature first then type (e.g. `return-receipt-list.component.ts`).
- Provide public API via each lib `src/index.ts`. Export only stable symbols; keep internal utilities in subfolders not re-exported.
- Add `project.json` with `test` & `lint` targets; for new Vitest libs include `vite.config.mts` and adjust `tsconfig.spec.json` references to vitest types.

### 11. Adding / Modifying Tests

- For Jest libs: standard `*.spec.ts` with `TestBed`. Spectator may appear in legacy code—do not introduce Spectator in new tests; use Angular Testing Utilities.
- For Vitest libs: ensure `vite.config.mts` includes `setupFiles`. Use `describe/it` from `vitest` and Angular TestBed (see remission resource spec for pattern of using `runInInjectionContext`).
- Prefer resource-style factories returning signals for async state (pattern in `createSupplierResource`).

### 12. Performance & Safety

- Logging: rely on lazy context function; avoid `JSON.stringify()` unless behind a dev guard.
- Storage: hashing keys (see `hash.utils.ts`) ensures stable key space; do not bypass if you need consistent per-user scoping.
- Scanner overlay: always clean up overlay + event listeners (follow existing `open` implementation for pattern).

### 13. CI / Coverage / Artifacts

- JUnit XML placed in `testresults/` (Jest configured with `jest-junit`). Keep filename stability for pipeline consumption; do not rename those outputs.
- Coverage output under `coverage/libs/...`; respect Nx caching—avoid side effects outside project roots.

### 14. When Unsure

- Search existing domain folder for analogous implementation (e.g. new feature under remission: inspect sibling feature libs for structure).
- Preserve existing DI token patterns instead of introducing new global singletons.

### 15. Quick Examples

```ts
// New feature logger usage
const log = logger(() => ({ feature: 'ReturnReceipt', action: 'init' }));
log.info('Mount');

// Persisting a signal store slice
export const FeatureStore = signalStore(
  withState(initState),
  withStorage('return:filters', LocalStorageProvider),
);

// Fetch config value safely
const apiBase = inject(Config).get('api.baseUrl', z.string().url());
```

---

Let me know if any area (e.g. auth flow, NgRx usage, Swagger generation details) needs deeper coverage and I can extend this file.
