# Angular Frontend — Developer Guide

**Version:** 1.0  
**Date:** June 9, 2026  
**Stack:** Angular 17+, TypeScript, NgRx (Redux), Angular Material, Bootstrap  
**API:** [openapi.yaml](./openapi.yaml) via typed HTTP services  
**Related:** [node.md](./node.md), [chrome-app.md](./chrome-app.md), [USE_CASES.md](./USE_CASES.md)

---

## 1. Overview

The employee and HR portals are **Angular SPAs** that consume the leaves REST API. State is managed with **NgRx** (Redux pattern for Angular): a single immutable store, actions, reducers, effects, and selectors.

### Why NgRx here

| Concern | NgRx benefit |
|---------|--------------|
| Leave balances + pending requests | Single source of truth across dashboard, apply, calendar |
| Auth session | Consistent JWT + employee profile |
| Team shift approval (2 of 2) | Predictable async flows via Effects |
| Attendance / compensation widgets | Shared loading & error state |
| Chrome extension (future) | Same store shape can sync via messaging |

---

## 2. Repository layout

```
frontend/
├── angular.json
├── package.json
├── src/
│   ├── app/
│   │   ├── app.config.ts              # provideStore, provideEffects, provideRouter
│   │   ├── app.routes.ts
│   │   ├── core/
│   │   │   ├── auth/
│   │   │   │   ├── guards/            # employeeAuthGuard, hrAuthGuard
│   │   │   │   ├── interceptors/      # authInterceptor, errorInterceptor
│   │   │   │   └── services/          # TokenStorageService (thin)
│   │   │   ├── api/                   # OpenAPI-aligned HTTP clients
│   │   │   │   ├── leave-api.service.ts
│   │   │   │   ├── attendance-api.service.ts
│   │   │   │   ├── shift-api.service.ts
│   │   │   │   ├── calendar-api.service.ts
│   │   │   │   └── hr-api.service.ts
│   │   │   └── models/                # DTOs matching openapi schemas
│   │   ├── store/                     # NgRx (Redux)
│   │   │   ├── app.state.ts
│   │   │   ├── auth/
│   │   │   │   ├── auth.actions.ts
│   │   │   │   ├── auth.reducer.ts
│   │   │   │   ├── auth.effects.ts
│   │   │   │   ├── auth.selectors.ts
│   │   │   │   └── auth.facade.ts     # UI-facing facade
│   │   │   ├── leave/
│   │   │   ├── backdated-leave/
│   │   │   ├── attendance/
│   │   │   ├── shift/
│   │   │   ├── compensation/
│   │   │   ├── calendar/
│   │   │   └── hr/
│   │   ├── features/
│   │   │   ├── employee/
│   │   │   │   ├── login/
│   │   │   │   ├── dashboard/
│   │   │   │   ├── apply-leave/
│   │   │   │   ├── backdated-leave/
│   │   │   │   ├── my-leaves/
│   │   │   │   ├── my-attendance/
│   │   │   │   ├── compensation/
│   │   │   │   ├── team-shift/
│   │   │   │   └── team-calendar/
│   │   │   └── hr/
│   │   │       ├── dashboard/
│   │   │       ├── employees/
│   │   │       ├── holidays/
│   │   │       ├── shift-compliance/
│   │   │       └── reports/
│   │   └── shared/
│   │       ├── components/
│   │       └── pipes/
│   ├── environments/
│   │   ├── environment.ts
│   │   └── environment.prod.ts
│   └── styles.scss
└── ...
```

**Note:** Fix folder name `frondend` → `frontend` when scaffolding.

---

## 3. NgRx (Redux) architecture

### 3.1 Data flow

```
Component
    │ dispatch(Action)
    ▼
  Store ──► Reducer (pure state update)
    ▲
    │ dispatch(success|failure Action)
  Effect ──► ApiService (HTTP)
    │
Component ◄── Selector (memoized read)
    │
  Facade (optional ergonomic API for components)
```

### 3.2 Root state shape

```typescript
// src/app/store/app.state.ts
export interface AppState {
  auth: AuthState;
  leave: LeaveState;
  backdatedLeave: BackdatedLeaveState;
  attendance: AttendanceState;
  shift: ShiftState;
  compensation: CompensationState;
  calendar: CalendarState;
  hr: HrState;
}

export interface AuthState {
  employeeId: string | null;
  token: string | null;
  loading: boolean;
  error: string | null;
  otpRequested: boolean;
}

export interface LeaveState {
  balances: LeaveBalances | null;
  requests: LeaveRequest[];
  pendingApproval: LeaveRequest[];  // for managers
  applyFormPreview: { leaveDays: number; sandwichApplied: boolean } | null;
  loading: boolean;
  error: string | null;
}

export interface ShiftState {
  currentTeamId: string | null;
  schedules: Record<string, TeamShiftSchedule>; // key: YYYY-MM
  delegatePlannerId: string | null;
  loading: boolean;
  error: string | null;
}
```

### 3.3 Actions (example — leave)

```typescript
// leave.actions.ts
export const LeaveActions = createActionGroup({
  source: 'Leave',
  events: {
    'Load Balances': emptyProps(),
    'Load Balances Success': props<{ balances: LeaveBalances }>(),
    'Load Balances Failure': props<{ error: string }>(),

    'Apply Leave': props<{ dto: CreateLeaveRequest }>(),
    'Apply Leave Success': props<{ request: LeaveRequest }>(),
    'Apply Leave Failure': props<{ error: string }>(),

    'Approve Leave': props<{ id: string; comment?: string }>(),
    'Approve Leave Success': props<{ request: LeaveRequest }>(),
    'Reject Leave': props<{ id: string; comment?: string }>(),
  },
});
```

### 3.4 Effects (side effects only)

```typescript
// leave.effects.ts
@Injectable()
export class LeaveEffects {
  loadBalances$ = createEffect(() =>
    this.actions$.pipe(
      ofType(LeaveActions.loadBalances),
      switchMap(() =>
        this.leaveApi.getBalances().pipe(
          map(b => LeaveActions.loadBalancesSuccess({ balances: b })),
          catchError(err => of(LeaveActions.loadBalancesFailure({ error: err.message }))),
        ),
      ),
    ),
  );

  applyLeave$ = createEffect(() =>
    this.actions$.pipe(
      ofType(LeaveActions.applyLeave),
      switchMap(({ dto }) =>
        this.leaveApi.applyLeave(dto).pipe(
          map(request => LeaveActions.applyLeaveSuccess({ request })),
          catchError(err => of(LeaveActions.applyLeaveFailure({ error: err.message }))),
        ),
      ),
    ),
  );
}
```

### 3.5 Facade services (recommended for components)

**Facades** wrap store dispatch + selectors so components stay dumb:

```typescript
// leave.facade.ts
@Injectable({ providedIn: 'root' })
export class LeaveFacade {
  balances$ = this.store.select(selectLeaveBalances);
  loading$ = this.store.select(selectLeaveLoading);
  requests$ = this.store.select(selectMyLeaveRequests);

  constructor(private store: Store) {}

  loadBalances(): void {
    this.store.dispatch(LeaveActions.loadBalances());
  }

  applyLeave(dto: CreateLeaveRequest): void {
    this.store.dispatch(LeaveActions.applyLeave({ dto }));
  }
}
```

**Rule:** Components inject **Facades**, not `Store` or `HttpClient` directly (except shared low-level widgets).

---

## 4. Feature modules & routing

### 4.1 Employee routes

| Path | Component | Store slice | Use case |
|------|-----------|-------------|----------|
| `/login` | LoginComponent | auth | UC-AUTH-001 |
| `/dashboard` | DashboardComponent | leave, attendance, compensation | — |
| `/leave/apply` | ApplyLeaveComponent | leave | UC-LEAVE-001 |
| `/leave/backdated` | BackdatedLeaveComponent | backdatedLeave | UC-LEAVE-003–005 |
| `/leave/history` | MyLeavesComponent | leave | — |
| `/attendance` | MyAttendanceComponent | attendance | UC-ATT-005 |
| `/compensation` | CompensationComponent | compensation | UC-COMP-002 |
| `/team/shift` | TeamShiftComponent | shift | UC-SHIFT-* |
| `/calendar` | TeamCalendarComponent | calendar | UC-CAL-001 |

### 4.2 HR routes

| Path | Component | Store slice |
|------|-----------|-------------|
| `/hr/login` | HrLoginComponent | auth |
| `/hr/dashboard` | HrDashboardComponent | hr |
| `/hr/holidays` | HolidaysComponent | hr |
| `/hr/shift-compliance` | ShiftComplianceComponent | hr, shift |

Lazy-load `employee` and `hr` route trees.

---

## 5. API services layer

Thin HTTP wrappers aligned with [openapi.yaml](./openapi.yaml). **No business logic** — Effects/facades own orchestration.

```typescript
@Injectable({ providedIn: 'root' })
export class LeaveApiService {
  private base = `${environment.apiUrl}/leaves`;

  constructor(private http: HttpClient) {}

  getBalances(): Observable<LeaveBalances> {
    return this.http.get<LeaveBalances>(`${environment.apiUrl}/balances/me`);
  }

  applyLeave(dto: CreateLeaveRequest): Observable<LeaveRequest> {
    return this.http.post<LeaveRequest>(this.base, dto);
  }

  approve(id: string, comment?: string): Observable<LeaveRequest> {
    return this.http.post<LeaveRequest>(`${this.base}/${id}/approve`, { comment });
  }
}
```

Generate types from OpenAPI (optional):

```bash
npx openapi-typescript docs/openapi.yaml -o src/app/core/models/api-types.ts
```

---

## 6. Key UI flows (store coordination)

### 6.1 Apply leave with sandwich preview

1. User selects Fri–Mon → component dispatches `previewSandwich` (or included in apply validation response).
2. Effect calls API; reducer stores `applyFormPreview.leaveDays = 4`.
3. User confirms → `applyLeave` action.

### 6.2 Backdated leave (2 approvals + mark)

| Step | Action | State change |
|------|--------|--------------|
| Submit | `submitBackdated` | status `PENDING_BACKDATE_APPROVAL` |
| Peer 1 approves | `approveBackdated` | `approvalCount: 1` |
| Peer 2 approves | `approveBackdated` | status `APPROVED_PENDING_MARK` |
| Employee marks | `markBackdated` | status `APPROVED`; refresh balances |

Selectors: `selectBackdatedApprovalReasons` (team-visible reasons).

### 6.3 Team shift (manager / planner / peers)

- **ShiftFacade** loads `schedules[month]`, `delegatePlannerId`.
- Submit → `PENDING_APPROVAL`; peers dispatch `approveShift` twice.
- Selector `selectShiftApprovalProgress` → `{ current: 1, required: 2 }`.

---

## 7. Auth & interceptors

### 7.1 Token storage

`TokenStorageService` — sessionStorage for employee JWT; separate key for HR.

### 7.2 authInterceptor

Attach `Authorization: Bearer ${token}` for non-login routes.

### 7.3 errorInterceptor

Map API `{ code, message }` to toast/snackbar; on `401` dispatch `AuthActions.logout()`.

---

## 8. Shared services (non-Redux)

Use plain services only for:

| Service | Purpose |
|---------|---------|
| `TokenStorageService` | Persist JWT |
| `DateCompanyService` | Today in `Asia/Kolkata`; cutoff 10:00 for same-day leave |
| `TeamContextService` | Cache primary teamId from `/me/team` endpoint |
| `ChromeBridgeService` | PostMessage to extension (see [chrome-app.md](./chrome-app.md)) |

Everything else goes through **NgRx Effects**.

---

## 9. Chrome extension integration (Angular side)

```typescript
// chrome-bridge.service.ts
@Injectable({ providedIn: 'root' })
export class ChromeBridgeService {
  isExtensionInstalled(): boolean {
    return typeof chrome !== 'undefined' && !!chrome.runtime?.id;
  }

  notifyExtension(event: 'LOGIN' | 'LOGOUT' | 'LEAVE_APPROVED', payload: unknown): void {
    chrome.runtime?.sendMessage?.({ type: event, payload });
  }
}
```

Dispatch from Effects after successful attendance-relevant actions (optional).

---

## 10. Environment

```typescript
// environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api/v1',
  companyTimezone: 'Asia/Kolkata',
  chromeExtensionId: 'YOUR_EXTENSION_ID', // from chrome-app build
};
```

Match `FRONTEND_URL` in backend `.env` for CORS.

---

## 11. Dependencies to add

```bash
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/router-store
```

Optional: `@angular/material`, keep Bootstrap from salary-slip for consistency.

---

## 12. Testing

| Layer | Tool |
|-------|------|
| Reducers | Pure function tests |
| Selectors | `memoized` output tests |
| Effects | `provideMockActions` + marble tests |
| Facades | Mock Store |
| Components | TestBed + Mock Facade |

---

## 13. Bootstrap checklist

```bash
ng new frontend --routing --style=scss
cd frontend
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools
# Copy patterns from salary-slip/frontend (auth, hr layout)
# Wire app.config.ts:
```

```typescript
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
    provideStore(appReducers),
    provideEffects([
      AuthEffects,
      LeaveEffects,
      BackdatedLeaveEffects,
      AttendanceEffects,
      ShiftEffects,
      CompensationEffects,
      CalendarEffects,
      HrEffects,
    ]),
    provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
  ],
};
```

---

## 14. Document history

| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2026-06-09 | Initial Angular + NgRx developer guide |
