Files
ISA-Frontend/.github/copilot-instructions.md
2025-09-17 13:56:35 +00:00

21 KiB
Raw Blame History

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 userscoped 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:

// 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:

// 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:

// 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:

// 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).

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:

// 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.

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 adhoc 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

// 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.