# CLAUDE.md — GB4.7MFE

## Project
Angular 20 MFE monorepo using Native Federation. 39 micro-frontends, 67 shared features, ~830 components.
All new code must follow the standards below without exception.

---

## Architecture

- **Layer:** Component → Service → DBService → GbHttpService → Backend
- **Host:** `projects/gbhost/` — shell, routing, layout
- **Shared state:** `DataPassingService` in `libs/common/src/lib/gbservice/gbdatapassing/`
- **Global state:** `GbAppStateService` (signals-based) in `libs/common/` — replaces NGXS
- **Path alias:** `@gbcommon/common` → `libs/common/src/public-api.ts`
- **Form configs:** `projects/gbhost/public/formjson/` (440+ JSON files)
- **i18n files:** `projects/public/i18n/`
- **Analysis docs:** `docs/analysis/` — security, memory leaks, i18n, responsive, architecture

---

## Module Folder Structure

Every project module follows this two-tier layout. Master = reference/config data. Transaction = workflow/operational forms.

```
projects/{module}/
├── master/
│   ├── dbservice/                        # HTTP-only wrappers (one file per entity or shared)
│   │   └── {entity}.db.service.ts
│   ├── service/                          # Validation + orchestration (one file per entity)
│   │   └── {entity}.service.ts
│   └── {entity}/                         # One folder per form entity
│       ├── {entity}.component.ts
│       ├── {entity}.component.html
│       ├── {entity}.component.scss
│       └── {entity}.component.spec.ts
└── transaction/                          # Same sub-structure as master/
    ├── dbservice/
    │   └── {entity}.db.service.ts
    ├── service/
    │   └── {entity}.service.ts
    └── {entity}/
        ├── {entity}.component.ts
        ├── {entity}.component.html
        ├── {entity}.component.scss
        └── {entity}.component.spec.ts
```

When generating a new entity, always produce **all five files** in a single pass (`.ts`, `.html`, `.scss`, `.spec.ts`, and the service). Never generate the component alone without its service and dbservice.

---

## Naming Conventions & Import Rules

| Item | Convention | Example |
|------|-----------|---------|
| File names | lowercase, no separators | `port.component.ts`, `port.db.service.ts` |
| Component class | PascalCase + suffix | `PortComponent` |
| Service class | PascalCase + `Service` | `PortService` |
| DBService class | PascalCase + `DbService` | `PortDbService` |
| Component selector | `gb-{entity}` | `gb-port` |
| DB service filename | `{entity}.db.service.ts` | `port.db.service.ts` |
| Service filename | `{entity}.service.ts` | `port.service.ts` |

### Directives Import — Always `GbDirectivesModule` as One Import

`GbDirectivesModule` (`libs/gbdirectives/src/directives`) bundles all form controls (input, combo, date, checkbox, picklist, formgrid, etc.). **Never import individual directives separately.** Always import the module as a single entry:

```typescript
// CORRECT — one import brings all form controls
imports: [ReactiveFormsModule, GbDirectivesModule, GbFormActionComponent, NgIf]

// WRONG — never split out individual directives
imports: [ReactiveFormsModule, GbInputComponent, GbComboboxComponent, GbDateComponent, ...]
```

### GBBaseFormGroup Arguments (exact order)

```typescript
this.form = new GBBaseFormGroup(
  this.injector,   // Injector instance (from inject(Injector) — injected in class body)
  'port',          // Form name — must match the JSON file in {module}/public/formjson/{name}.json
  this.MenuId,     // MenuId string: MenuRights.MenuId.toString() + MenuRights.TabId
  'shipping'       // Module project name (lowercase folder name under projects/)
);
```

---

## Generating a New Form Entity — All Files Together

Use this as the canonical template. Replace `Port` / `port` / `PortId` / `shipping` with the actual entity, name, primary key, and module.

### 1. DBService — `port.db.service.ts`

HTTP-only. No business logic. Constructor injection for `GBHttpService` is the exception to the `inject()` rule (existing project-wide pattern in dbservices).

```typescript
import { Injectable } from '@angular/core';
import { GBHttpService } from '@gbcommon/httpservice';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class PortDbService {
  constructor(public http: GBHttpService) {}

  public getPort(portId: number): Observable<any> {
    const url = 'Shipping.Port.Get';
    const params = '/?PortId=' + portId;
    return this.http.gbhttpget(url, false, false, params);
  }

  public savePort(criteria: any): Observable<any> {
    const url = 'Shipping.Port.Save';
    return this.http.gbhttppost(url, criteria, false);
  }

  public deletePort(portId: number): Observable<any> {
    const url = 'Shipping.Port.Delete';
    const params = '/?PortId=' + portId;
    return this.http.gbhttpdelete(url, false, params);
  }
}
```

### 2. Service — `port.service.ts`

Handles JSON schema loading, field validation, criteria building, and orchestration. Uses `FormActiondbservice` for generic HTTP calls. `IdField` is used to look up the primary key field name (required for delete).

```typescript
import { inject, Injectable } from '@angular/core';
import { IdField } from 'projects/gbhost/public/formactionurls/idfield';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { TranslocoService } from '@jsverse/transloco';
import { GbDialogBoxComponent } from 'features/gbdialogbox/gbdialogbox.component';
import { FormActiondbservice } from 'features/gblayout/dbservice/gbformaction.db.service';

@Injectable({ providedIn: 'root' })
export class PortService {
  IdField = IdField as any;
  LoginDTODetail: any;
  public dialog = inject(MatDialog);
  public translate = inject(TranslocoService);
  private formActiondbservice = inject(FormActiondbservice);

  constructor(private localhttp: HttpClient) {
    this.LoginDTODetail = JSON.parse(sessionStorage.getItem('LoginDTO') as any);
  }

  public formloadservice(formname: string, id: number): Observable<any> {
    const url = 'Shipping.Port.Get';
    const params = '/?PortId=' + id;
    return this.formActiondbservice.formloaddbservice(url, false, params);
  }

  public formsaveservice(formname: string, FormValue: any): Observable<any> {
    const jsonFile = `formjson/${formname}.json`;
    return new Observable((observer) => {
      this.localhttp.get(jsonFile).subscribe((JSON: any) => {
        let isthereAlert = false;
        const alertdata: string[] = [];

        for (const jsonvalue of JSON.ObjectFields) {
          if (
            jsonvalue.Required &&
            (jsonvalue.RequireBasedOnFieldName === undefined ||
              jsonvalue.RequireBasedOnFieldValue.includes(FormValue[jsonvalue.RequireBasedOnFieldName]))
          ) {
            if (
              FormValue[jsonvalue.Name] === '' ||
              FormValue[jsonvalue.Name] === null ||
              FormValue[jsonvalue.Name] === undefined
            ) {
              isthereAlert = true;
              alertdata.push(this.translate.translate('' + jsonvalue.Label) + ' Should not be Empty');
            }
          }
        }

        if (isthereAlert) {
          this.dialog.open(GbDialogBoxComponent, {
            data: { message: alertdata, heading: 'Error' },
            width: 'min(600px, 95vw)',
            maxWidth: '95vw',
          });
        } else {
          const criteria: any = {};
          for (const jsonvalue of JSON.ObjectFields) {
            if (jsonvalue.PostData === undefined || jsonvalue.PostData) {
              criteria[jsonvalue.Name] = FormValue[jsonvalue.Name];
            }
          }
          const url = 'Shipping.Port.Save';
          this.formActiondbservice.formsavedbservice(url, criteria, false, '/').subscribe((result: any) => {
            observer.next(result);
            observer.complete();
          });
        }
      });
    });
  }

  public formdeleteservice(formname: string, FormValue: any): Observable<any> {
    return new Observable((observer) => {
      const id = FormValue[this.IdField[formname]];
      if (!(id === 0 || id === -1)) {
        const url = 'Shipping.Port.Delete';
        const params = '/?PortId=' + id;
        this.formActiondbservice.formdeletedbservice(url, false, params).subscribe((result: any) => {
          observer.next(result);
          observer.complete();
        });
      }
    });
  }
}
```

### 3. Component — `port.component.ts`

`GbDirectivesModule` is always a single import. All services injected via `inject()` in the class body — constructor is only for `@Inject` tokens + `GBBaseFormGroup` initialization.

```typescript
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, inject, Injector, OnDestroy, OnInit, signal } from '@angular/core';
import { NgIf } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { Subject, takeUntil } from 'rxjs';
import { GBBaseFormGroup } from 'libs/common/src/lib/gbformgroup/gbformgroup';
import { GbDirectivesModule } from 'libs/gbdirectives/src/directives';
import { GbFormActionComponent } from 'features/gblayout/gbformaction/formactionbar/gbformaction.component';
import { GbDialogBoxComponent } from 'features/gbdialogbox/gbdialogbox.component';
import { FormActionservice } from 'features/gblayout/service/gbformaction.service';
import { PicklistService } from 'libs/gbdirectives/src/lib/gbpicklist/service/gbpicklist.service';
import { IDrillDownDetails, RolesandRights } from 'features/gbformviewer/gbformviewer.model';
import { PortService } from '../service/port.service';

@Component({
  selector: 'gb-port',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [NgIf, ReactiveFormsModule, GbDirectivesModule, GbFormActionComponent],
  templateUrl: 'port.component.html',
  styleUrls: ['port.component.scss'],
})
export class PortComponent implements OnInit, OnDestroy {
  form: GBBaseFormGroup;
  MenuId = '';
  GridRefresh = signal<boolean>(false);

  private destroy$ = new Subject<void>();
  private injector = inject(Injector);
  private service = inject(PortService);
  private formservice = inject(FormActionservice);
  private picklistservice = inject(PicklistService);
  private cdr = inject(ChangeDetectorRef);
  public dialog = inject(MatDialog);

  constructor(
    @Inject('selectedId') public selectedId: any,
    @Inject('MenuRights') public MenuRights: RolesandRights,
    @Inject('DrillDownDetails') public DrillDownDetails: IDrillDownDetails,
  ) {
    this.MenuId = this.MenuRights.MenuId.toString() + this.MenuRights.TabId;
    this.form = new GBBaseFormGroup(this.injector, 'port', this.MenuId, 'shipping');
  }

  ngOnInit(): void {
    if (this.selectedId.SelectedId !== -1) {
      this.OnpicklistLoad(this.selectedId);
      this.formservice.setMenuEditable(this.MenuId, false);
      this.cdr.detectChanges();
    }
  }

  public OnpicklistLoad(Event: any): void {
    if (Event.SelectedId !== 0 && Event.SelectedId !== -1) {
      this.service.formloadservice('port', Event.SelectedId)
        .pipe(takeUntil(this.destroy$))
        .subscribe((data: any) => {
          this.form.patchValue(data.responseValue);
          this.GridRefresh.set(!this.GridRefresh());
        });
    }
  }

  public OnPicklistChange(Event: any): void {
    if (Event.SelectedData === '') {
      this.form.get(Event.Field.LinkId)?.patchValue(-1);
      if (Event.Field.LinkSubTitle) {
        this.form.get(Event.Field.LinkSubTitle)?.patchValue('');
      }
    } else {
      this.form.get(Event.Field.LinkId)?.patchValue(Event.SelectedId);
      if (Event.Field.LinkSubTitle) {
        this.form.get(Event.Field.LinkSubTitle)?.patchValue(Event.SelectedSubTitle);
      }
    }
  }

  public FormOutput(event: any): void {
    if (event === 'Save') {
      this.service.formsaveservice('port', this.form.value)
        .pipe(takeUntil(this.destroy$))
        .subscribe((result: any) => this.handleFormResult(result));
    } else if (event === 'Delete') {
      this.service.formdeleteservice('port', this.form.value)
        .pipe(takeUntil(this.destroy$))
        .subscribe((result: any) => this.handleFormResult(result));
    }
  }

  private handleFormResult(result: any): void {
    if (result.responsedata.Status === 200) {
      this.dialog.open(GbDialogBoxComponent, {
        data: { message: result.responseValue.Body ?? result.responseValue, heading: 'Success' },
        width: 'min(600px, 95vw)',
        maxWidth: '95vw',
      });
      this.picklistservice.removepicklistdexieservice('Port', false);
      this.resetForm();
    }
  }

  private resetForm(): void {
    this.form.reset();
    this.formservice.setFormReset(!this.formservice.isFormReset());
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    this.form = null!;
  }
}
```

### 4. Template — `port.component.html`

```html
<form [formGroup]="form" *ngIf="form.bFormReady">
  <gb-formaction
    [FormDetails]="form"
    [FormData]="form.value"
    [MenuRights]="MenuRights"
    (FormOutput)="FormOutput($event)">
  </gb-formaction>
  <div class="FormOverView scrollbar">
    <gb-newpicklist
      (PicklistValue)="OnpicklistLoad($event)"
      [FormData]="form.value"
      formControlName="PortCode"
      class="adjacentField">
    </gb-newpicklist>
    <gb-newpicklist
      (PicklistValue)="OnpicklistLoad($event)"
      [FormData]="form.value"
      formControlName="PortName"
      class="adjacentField">
    </gb-newpicklist>
    <!-- Add further gb-* directives matching the form JSON fields -->
  </div>
</form>
```

### 5. SCSS — `port.component.scss`

```scss
// Form layout is handled by shared styles.
// Add only component-specific overrides here.
```

### 6. Spec — `port.component.spec.ts`

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PortComponent } from './port.component';

describe('PortComponent', () => {
  let component: PortComponent;
  let fixture: ComponentFixture<PortComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [PortComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(PortComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
```

---

## API Endpoint Registration for New Entities

When adding a new entity, register its endpoints in two places:

### Step 1 — Add primary key to `IdField`

File: `projects/gbhost/public/formactionurls/idfield.ts`

```typescript
export class IdField {
  // ... existing entries ...
  public static port = 'PortId';  // ← add new entry
}
```

### Step 2 — Add URL mapping to the module API file

File: `projects/gbhost/public/api/gb5api/{module}.ts` (e.g. `shipping.ts`)

```typescript
export const shippingApi = {
  // ... existing entries ...
  'Shipping.Port.Get':    '/sh/Port.svc/',
  'Shipping.Port.Save':   '/sh/Port.svc/',
  'Shipping.Port.Delete': '/sh/Port.svc/',
};
```

The dot-separated code pattern (`Module.Entity.Action`) is the required format. Do **not** hardcode raw `.svc/` URLs in service files — always go through the mapping.

---

## State Management — Signals Only

- **Use Angular Signals exclusively.** No new NGXS. No new BehaviorSubjects.
- **Reads** in components via `toSignal()` or `rxResource()` — no raw `.subscribe()` for data reads
- **Writes/mutations** (save, update, delete) use `.subscribe()` with `takeUntilDestroyed(this.destroyRef)` — HTTP observables complete after one emit so there is no leak, but `takeUntilDestroyed` cancels any in-flight request if the component is destroyed mid-call
- **`httpResource` is not applicable** — `GbHttpFormingUrlService.buildFinalUrl()` is async; `httpResource` requires a synchronous URL factory and bypasses `GbHttpService` entirely (losing deduplication, encryption, loading indicators, and response handling)
- Read `DataPassingService` state directly — it is already signals, no subscribe needed
- `destroy$` / `ngOnDestroy` only required for `setInterval`, `setTimeout`, or raw DOM event listeners
- RxJS stays inside services (`HttpClient` calls in dbservice files). `toSignal()` is the component boundary.

```typescript
// Simple read — no reactive params, no loading state needed
data = toSignal(this.dbService.getList(), { initialValue: [] as MyType[] });

// Reactive read — params driven by signals, or loading/error state needed in template
resource = rxResource({
  request: () => ({ id: this.selectedId(), filter: this.filter() }),
  loader: ({ request }) => this.dbService.getFiltered(request.id, request.filter)
});
// resource.value(), resource.isLoading(), resource.error() — all signals, all free

// Mutation (save / update / delete) — subscribe is correct here, not a leak
private destroyRef = inject(DestroyRef);

save() {
  this.dbService.save(this.form.value)
    .pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe({
      next: () => this.notify('Saved!'),
      error: (err) => this.handleError(err)
    });
}

// Derived state — lazy, cached, zero overhead
filtered = computed(() => this.data().filter(x => x.active));
```

### When to Use Which Pattern

| Scenario | Pattern |
|---|---|
| Simple read, no reactive params | `toSignal()` |
| Read depends on signals (id, filters) | `rxResource()` |
| Need `isLoading()` / `error()` in template | `rxResource()` |
| Save / update / delete | `.subscribe()` + `takeUntilDestroyed` |
| Ongoing stream (Subject, EventEmitter) | `toSignal()` — never raw subscribe |

---

## Every Component Must Have

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,  // always
})
export class MyComponent {
  private service = inject(MyService);  // inject(), not constructor

  data = toSignal(this.service.getData(), { initialValue: [] as MyType[] });
  filtered = computed(() => this.data().filter(x => x.active));
  isOpen = signal(false);
}
// No ChangeDetectorRef — signals handle CD automatically
// No destroy$ unless using timers or raw DOM listeners
```

---

## TypeScript Standards

- **No `any`** — define interfaces for all API responses, signals, and component inputs
- **No `console.log`** — use `GbConsoleService`
- **Use `inject()`** — not constructor injection
- No magic strings for injection tokens — use typed `InjectionToken<T>`

---

## Security (Non-Negotiable)

- **No `bypassSecurityTrustHtml` or `bypassSecurityTrustResourceUrl`** — use DOMPurify for HTML content
- **No credentials, keys, or secrets in source code** — use environment config or secrets manager
- **No tokens or sensitive data in `localStorage` / `sessionStorage`** — use `httpOnly` cookies
- **No auth tokens or user data in `console.log`**
- **No secrets or ClientSecrets in URL query parameters** — use POST body only
- **No hardcoded encryption keys** — must come from server config

### Known P0 Violations (fix when touching these files, do not regress)
- `projects/costing/transaction/budget/budget.component.ts` — hardcoded Jasper credentials
- `projects/security/transaction/visitorpass/visitorpass.component.ts` — hardcoded Jasper credentials
- `features/login/service/login.service.ts:176,521` — hardcoded AES-256 key
- `features/gbrichtexteditor/gbrichtexteditor.component.ts:101` — bypassSecurityTrustHtml
- `features/gbdynamichml/gbdynamichtml.component.ts:20` — bypassSecurityTrustHtml
- `features/login/service/login.service.ts:639` — SSO token in localStorage
- `features/login/service/login.service.ts:319` — ClientSecret in URL

---

## i18n

- **Use `@jsverse/transloco` only** — `@ngx-translate/core` is being removed
- **All display strings use Transloco keys** — no hardcoded English in templates or TypeScript
- **Plurals** via Transloco ICU or `I18nPluralPipe` — never string concatenation
- **Dates and numbers** via Angular locale pipes (`DatePipe`, `CurrencyPipe`) — never manual formatting
- **Transloco config** must have `fallbackLang: 'en'` and `missingHandler.useFallbackTranslation: true`
- **Translation key convention:** `module.section.key` (e.g. `recruitment.status.active`)

### Date/Currency/Quantity Format — Always `LoginDTO`, Never Hardcoded or Browser-Default

The user's own date/currency/quantity format preference is set on `MUSER` and populated into `LoginDTO.DateFormat` / `LoginDTO.CurrencyFormat` / `LoginDTO.QuantityFormat` / `LoginDTO.TimeFormat` at login (backend: `AuthenticationBLL.cs`). Every date or number shown to the user — report titles, grid columns, summary cards, anywhere — must ultimately derive its pattern/decimal-places from these fields, not from the browser's default locale, not from the active Transloco UI language, and not from a hardcoded literal like `'dd/MM/yyyy'` or `.toFixed(2)`.

- **Reuse the existing shared utilities** — don't hand-roll another parser:
  - `libs/gbpipes/dateformat.pipe.ts` (`DateFormaterPipe`, pipe name `DateFormatter`) — dates/times in templates.
  - `libs/gbpipes/numberformatter.pipe.ts` (`NumberFormatPipe`, pipe name `NumberFormater`) — currency/decimal numbers in templates.
  - For non-template contexts (e.g. building a SlickGrid cell formatter function, which can't use an Angular pipe directly), read `LoginDTO.DateFormat`/`CurrencyFormat` from `sessionStorage` the same way `features/gbslickgrid/gbslickgrid.component.ts`'s `dateformat` property does, and feed it into `DatePipe`/`toLocaleString` yourself — see `reportviewer.component.ts`'s `userDateFormat`/`userCurrencyDecimalPlaces` getters for the pattern.
- **A field-level admin override (e.g. a report's `CalFormat` mask) may still take precedence when explicitly configured** — but the *fallback*, when no such override exists, must be the user's `LoginDTO` preference, never a hardcoded default or the UI language.
- **Known violations** (found live 2026-08-22, `reportviewer.component.ts`'s date/number formatters ignored `LoginDTO` entirely before being fixed): ~19 components across FAM/Training/Recruitment/PayRoll call `toLocaleDateString()` with a hardcoded or missing locale string (e.g. `'en-GB'`, `'en-IN'`, or none at all). Fix these when touching any of those files — do not regress them further, and do not copy their pattern into new code.

---

## Responsive Design

- **MatDialog widths:** always `min(Xpx, 95vw)` with `maxWidth: '95vw'` — never fixed px
- **No `window.addEventListener('resize')`** — use Angular Material `BreakpointObserver`
- **All layout components** must include `[dir="rtl"]` CSS selectors for Arabic support
- **No hardcoded viewport offsets** (e.g. `height - 160px`) — use `dvh` or CSS custom properties

---

## Real-Time / SignalR Modules

For modules requiring real-time updates (e.g., CollabSpace):

- **One `SignalRDbService` per module** — connection setup, `on()` wiring, and `invoke()` calls all live in the DBService; never in components or business services
- **Auth token via `accessTokenFactory`** from the auth service signal — never `sessionStorage` / `localStorage`:
  ```typescript
  private connection = new signalR.HubConnectionBuilder()
    .withUrl('/hubs/collabspace', { accessTokenFactory: () => this.auth.accessToken() ?? '' })
    .withAutomaticReconnect()
    .build();
  ```
- **Hub events → Signals** via `fromEventPattern` + `toSignal()` — components stay signal-only, no `connection.on()` in components:
  ```typescript
  // DBService exposes an Observable
  readonly message$ = fromEventPattern<CollabMessage>(
    handler => this.connection.on('ReceiveMessage', handler),
    handler => this.connection.off('ReceiveMessage', handler)
  );
  // Component consumes it as a signal
  messages = toSignal(this.collabService.message$, { initialValue: [] as CollabMessage[] });
  ```
- **Connection lifecycle in DBService** — start in constructor, stop via `DestroyRef`:
  ```typescript
  constructor() {
    this.connection.start();
    inject(DestroyRef).onDestroy(() => this.connection.stop());
  }
  ```
- `destroy$` only needed in a component that pipes a Hub Observable through `takeUntil`; prefer `toSignal()` to avoid it entirely

---

## Known Memory Leaks (fix when touching these files)

- `features/gblayout/gblayout.component.ts:53` — subscribe without cleanup
- `features/gblayout/gblayout.component.ts:60` — `window.addEventListener('resize')` never removed
- `features/gblayout/gbheader/gbusersetting/gbusersetting.component.ts:93` — subscribe without cleanup
- `features/login/service/login.service.ts:145–652` — nested subscribes, no takeUntil (singleton = permanent)
- `features/gbqrscanner/gbqrscanner.component.ts:122` — setInterval not cleared on destroy

---

## API Service URLs — Dot-Separated Code Pattern (Required)

All service URLs must use the **dot-separated code pattern** instead of raw URL paths. This enables centralized URL management and easier maintenance.

### ✅ Recommended Pattern (New Code)
```typescript
// Use dot-separated code: Module.ServiceName.MethodName
let url = 'Ess.Declaration.GetDeclarationView';
let url = 'Framework.Menu.MenuList';
let url = 'Admin.OrganizationUnit.OuDataGet';
```

### ❌ Anti-Pattern (Legacy - Do Not Use)
```typescript
// OLD: Raw URL path with .svc/
let url = '/prs/Declaration.svc/GetDeclrationView/?FirstNumber=-1&MaxResult=-1';
let url = '/fws/Menu.svc/ReportMenuDetailsForMenu/?UserId=' + userId;
let url = '/ads/UserAccessRights.svc/UserAccessRightsOnUser/?UserId=' + userId;
```

### How It Works
- The dot-separated code (e.g., `Ess.Declaration.GetDeclarationView`) is resolved to the actual URL via API endpoint files in `projects/gbhost/public/api/gb4api/` and `projects/gbhost/public/api/gb5api/`
- The `GbHttpFormingUrlService` (`libs/common/src/lib/gbservice/gbhttpservice/gbhttpformingurl.service.ts`) performs the conversion
- When adding new endpoints, add the mapping to the appropriate API endpoint file

### Parameter Handling
Query parameters are passed separately to `gbhttpget()` / `gbhttppost()`, not embedded in the URL code:
```typescript
// Parameters passed separately, not in the URL code
this.http.gbhttpget('Ess.Declaration.GetDeclarationView', true, true, 'FirstNumber=-1&MaxResult=-1');
```

### Conversion Example
```typescript
// Old code:
let url = '/fws/Comment.svc/?CommentId=' + commentId;
this.http.gbhttpget(url);

// New code:
let url = 'Framework.Comment.GetComment';  // Add mapping to framework.ts if not exists
this.http.gbhttpget(url, true, true, 'CommentId=' + commentId);
```

### Adding New Endpoints
When a new endpoint is needed, add the mapping to the appropriate module file:
- GB4 APIs: `projects/gbhost/public/api/gb4api/<module>.ts`
- GB5 APIs: `projects/gbhost/public/api/gb5api/<module>.ts`

Example in `ess.ts`:
```typescript
export const essApi = {
  'Ess.Declaration.GetDeclarationView': '/prs/Declaration.svc/GetDeclrationView/',
};
```

### Exceptions
Some legacy code uses hardcoded external report URLs (e.g., `http://newserver:81/gb4/...`) — these may be intentional exceptions for external reporting services.

---

## MCP Tools — Live API & Schema Access

Four MCP servers are configured in the BE repo's `.claude/settings.json` and are available in every session.
**Use them before writing any Angular service or dbservice that calls the API.**

| Tool | When to use |
|------|-------------|
| `gb5_list_endpoints` | Before writing any dbservice — confirm the BE route exists in swagger |
| `gb5_call_endpoint` | After generating a dbservice — call the live endpoint and inspect the real response shape |
| `gb5-filesystem` | Search existing FE services, components, and the `Gb5ApiEndpoints` mapping for patterns |
| `gb5-cypress` | Run the existing Cypress API spec for the entity to confirm correctness end-to-end |

**Reference files (always available offline):**
- `docs/api-snapshot.json` (in BE repo) — swagger snapshot; all endpoint paths and parameter names
- `libs/common/src/lib/generated/gb5-api-types.ts` — TypeScript interfaces generated from swagger; authoritative response shapes

**Critical serialization fact:** The BE uses `PropertyNamingPolicy = null` in all module `Program.cs` files.
Every API response uses **PascalCase** property names: `AccountId`, `ItemCode`, `PlannedQty` — never `accountId`, `itemCode`, `plannedQty`.
Always access response fields with PascalCase. This is not optional — camelCase access silently returns `undefined`.

---

## Critical Analysis & Verification Protocol

This protocol is **mandatory** for every task that generates or modifies Angular components, services, or API calls. Skip no step.

### Phase 1 — Planning (before writing a single line)

**Step 1 — Confirm the endpoint exists and get its real response shape:**
```
gb5_list_endpoints  filter="MMHead"
```
Find the exact route path. Then call it:
```
gb5_call_endpoint  GET  /mms/MMHead/GetMMHead  {MMHeadId: "1"}
```
Read the JSON response. Every field you access in the component must exist in this response.
Do not assume field names — the actual API response is authoritative.

**Step 2 — Confirm the Gb5ApiEndpoints key:**
Search `gb5-filesystem` for the entity in `projects/gbhost/public/api/gb5api/*.ts`:
- If the key exists: use it. Never create a duplicate.
- If missing: add it to the correct module file, matching the convention `Module.Entity.Action`.
- Confirm the URL value in the key matches the route returned by `gb5_list_endpoints`.

**Step 3 — Critical analysis questions (answer all before coding):**
- What does the actual API response look like? (Step 1 call)
- Are all response fields PascalCase? (yes — always, per BE serializer setting)
- Does the response wrap the data in `ResponseStandardDTO.Body` or return it directly?
- Is there an existing dbservice for this entity in the same or a sibling module? (avoid duplication — `gb5-filesystem` search)
- What is the primary key field name? (needed for `IdField` entry)
- Does the formjson file exist? (needed before `GBBaseFormGroup` can load)

**Step 4 — Check for existing patterns in the same module:**
Use `gb5-filesystem` to read one complete working entity in the same module (component + service + dbservice).
Match the exact imports, inject() pattern, signal pattern, and rxResource usage.

---

### Phase 2 — Verification (after generating code, before reporting done)

**Step 1 — Confirm the API call works with the generated parameters:**
Using the exact parameter names and casing from the generated dbservice, call:
```
gb5_call_endpoint  GET  /mms/MMHead/GetMMHead  {MMHeadId: "1"}
```
If this returns 400, the parameter name is wrong — fix the dbservice before reporting done.

**Step 2 — Cross-verify response field access in the component:**
For every field accessed in the template as `data.FieldName` or `data().FieldName`:
- Confirm that field name exists in the API response from Step 1
- Confirm PascalCase — never camelCase
- Confirm the field is on `Body` (not on the response root) for `ResponseStandardDTO` responses

**Step 3 — TypeScript compile check:**
```bash
cd ~/gb/gb4.7mfe && npx tsc --noEmit --project projects/{module}/tsconfig.app.json
```
Zero type errors required. A type error on a response field means the field name is wrong.

**Step 4 — Cypress spec if available:**
```
cypress_list_specs  filter="ModuleName"  testType="API_Tests"
cypress_run_spec    "cypress/src/features/API_Tests/{Module}/{Entity}API.feature"
```
If a spec exists and was passing, it must still pass after your changes.

---

## New Module Checklist

When creating any new module or subproject:

- [ ] **MCP pre-check:** `gb5_list_endpoints` called — endpoint confirmed to exist in swagger
- [ ] **MCP pre-check:** `gb5_call_endpoint` called — actual response shape confirmed and PascalCase verified
- [ ] **MCP pre-check:** `gb5-filesystem` searched — no duplicate dbservice or Gb5ApiEndpoints key
- [ ] `ChangeDetectionStrategy.OnPush` on every component
- [ ] Reads via `toSignal()` or `rxResource()` — no raw `.subscribe()` for data streams
- [ ] Mutations (save/update/delete) use `.subscribe()` + `takeUntilDestroyed(this.destroyRef)`
- [ ] `destroy$` + `ngOnDestroy` only if using timers or raw DOM event listeners
- [ ] All signals typed with real interfaces — no `any`; interfaces derived from actual API response
- [ ] No NGXS — use `GbAppStateService` or `DataPassingService` signals
- [ ] No BehaviorSubjects in components or shared services
- [ ] All display strings via Transloco keys — no hardcoded English
- [ ] No hardcoded credentials, keys, or secrets
- [ ] No `bypassSecurityTrust*` — use DOMPurify
- [ ] MatDialog widths use `min(Xpx, 95vw)`
- [ ] RTL styles added for layout components
- [ ] Unit tests for service layer
- [ ] No `console.log` — use `GbConsoleService`
- [ ] `npm audit` passes for any new dependencies added
- [ ] All five files generated together: `.component.ts`, `.component.html`, `.component.scss`, `.component.spec.ts`, and `{entity}.service.ts` (plus `{entity}.db.service.ts` if not shared)
- [ ] `GbDirectivesModule` imported as a single module — never individual directive imports
- [ ] `IdField` entry added for the new entity's primary key
- [ ] API URL mapping added to `projects/gbhost/public/api/gb5api/{module}.ts` — confirmed against swagger
- [ ] `GBBaseFormGroup` initialised with correct `(injector, 'formname', menuId, 'moduleName')` args
- [ ] Form JSON file created at `projects/{module}/public/formjson/{formname}.json`
- [ ] **Post-check:** TypeScript compile passes: `npx tsc --noEmit`
- [ ] **Post-check:** Cypress API spec runs green (if exists for this entity)
