## 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; 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( 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 items$ = http.get(url); // New pattern const items = toSignal(http.get(url), { initialValue: [] }); ``` Expose signals from stores & services: ```ts // BAD (forces template async pipe + subscription mgmt) getItems(): Observable { return this.http.get(...); } // GOOD items = toSignal(this.http.get(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( 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( 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.