mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
416 lines
21 KiB
Markdown
416 lines
21 KiB
Markdown
## 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.
|