# Capstone - Employee Management System

Course: [Angular](https://www.devtutorials.ai/learn/angular) · Section: Project-Based Learning
Source: https://www.devtutorials.ai/learn/angular/projects/employee-management

> Build a complete Employee Management System: project structure, authentication and roles, reusable components, lazy routing, a signal store, reactive forms with validation, API integration, error handling and performance.

## Project Overview

In this lesson, you build an **Employee Management System**: an app for an HR team to view and manage the employees of a company. It uses the most important topics of this course in one project.

### Real-World Example

Most companies have an internal HR app like this one. HR administrators add new employees, update positions and salaries, and mark employees who have left as inactive. Team leads may only look up their colleagues.

The app must load fast, show clear messages when the server fails, and never let a viewer change employee data.

What the finished app can do:

- **Log in** with email and password. Two roles: **HR admin** and **viewer**.
- **List employees** with search, department filter and an "active" counter.
- **View** the details of one employee.
- **Add**, **edit** and **delete** employees (HR admin only), with form validation.
- **Protect** pages with route guards, and log out automatically when the token expires.
- **Handle errors** with clear messages and a global error banner.

| Feature | Lesson where you learned it |
|---|---|
| Components, `@Input()`, content projection | Components, Component Communication |
| Routing, route parameters, lazy loading | Routing |
| Services and dependency injection | Services, Dependency Injection |
| Reactive forms and validators | Reactive Forms, Form Validation |
| `HttpClient` and interceptors | API Integration |
| Signals and a store service | Angular Signals, State Management |
| JWT, guards and roles | User Access Control |
| `ErrorHandler`, `OnPush`, `track` | Error Handling, Performance Optimization |

## Project Setup

The project starts like every Angular app:

```bash
ng new employee-management
cd employee-management
ng generate environments
```

Then it gets a small Node.js **API server** in `server/server.js`, which you know from earlier capstone projects. The server:

- signs JWTs at login (`POST /api/auth/login`),
- answers the employee API (`GET`, `POST`, `PUT` and `DELETE` on `/api/employees`),
- returns **401** without a valid token and **403** when a viewer tries to change data.

Two scripts in `package.json` start the two parts:

```json
"scripts": {
  "start": "ng serve",
  "api": "node server/server.js"
}
```

The practice blocks on this page show each part of the app in a small, runnable form, with a practice server in the browser. The complete project, with the real API server, is in the download at the end.

## Application Structure

The code is grouped by **feature** and by **job**. Each folder has one clear purpose:

```text
src/app/
├── auth/           AuthService, interceptor, guards, user model
├── core/           global error handler, error banner state
├── shared/         reusable components: page header, status badge, search box
├── employees/      everything about employees
│   ├── employee.model.ts
│   ├── employee-api.service.ts     HTTP requests
│   ├── employee-store.ts           state with signals
│   ├── employee-validators.ts      custom validator
│   ├── employee-list.ts
│   ├── employee-detail.ts
│   ├── employee-form.ts
│   └── employees.routes.ts         lazy-loaded routes
├── pages/          login, access denied, not found
├── app.routes.ts
├── app.config.ts
└── app.ts
```

<figure className="ngc-fig">
<svg viewBox="0 0 640 134" role="img" aria-label="Components call the EmployeeStore, the store calls EmployeeApiService, the interceptor adds the token, and the server answers.">
<defs><marker id="ngc-m1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-solid c-blue"><rect x="6" y="30" width="116" height="64" rx="9" /><text x="64.0" y="59" textAnchor="middle" style={{fontSize:"13px"}}>Components</text><text className="ngc-sub" x="64.0" y="77" textAnchor="middle">list, details, form</text></g></g><line className="ngc-link ngc-flow c-slate" x1="123" y1="62" x2="133" y2="62" markerEnd="url(#ngc-m1)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-purple"><rect x="134" y="30" width="116" height="64" rx="9" /><text x="192.0" y="59" textAnchor="middle" style={{fontSize:"13px"}}>EmployeeStore</text><text className="ngc-sub" x="192.0" y="77" textAnchor="middle">signals + methods</text></g></g><line className="ngc-link ngc-flow c-slate" x1="251" y1="62" x2="261" y2="62" markerEnd="url(#ngc-m1)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-orange"><rect x="262" y="30" width="116" height="64" rx="9" /><text x="320.0" y="59" textAnchor="middle" style={{fontSize:"13px"}}>Employee API</text><text className="ngc-sub" x="320.0" y="77" textAnchor="middle">EmployeeApiService</text></g></g><line className="ngc-link ngc-flow c-slate" x1="379" y1="62" x2="389" y2="62" markerEnd="url(#ngc-m1)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-red"><rect x="390" y="30" width="116" height="64" rx="9" /><text x="448.0" y="59" textAnchor="middle" style={{fontSize:"13px"}}>Interceptor</text><text className="ngc-sub" x="448.0" y="77" textAnchor="middle">adds the token</text></g></g><line className="ngc-link ngc-flow c-slate" x1="507" y1="62" x2="517" y2="62" markerEnd="url(#ngc-m1)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="518" y="30" width="116" height="64" rx="9" /><text x="576.0" y="59" textAnchor="middle" style={{fontSize:"13px"}}>API server</text><text className="ngc-sub" x="576.0" y="77" textAnchor="middle">JWT + role checks</text></g></g><circle className="ngc-packet c-purple" r="5"><animateMotion dur="4.5s" repeatCount="indefinite" path="M64 106 L576 106" /></circle><text className="ngc-note" x="320" y="128" textAnchor="middle">Data moves in one direction. Components never call HttpClient directly.</text>
</svg>
<figcaption>The layers of the Employee Management System.</figcaption>
</figure>

Data always moves in the same direction: components call the **store**, the store calls the **API service**, and the API service talks to the **server**. Components never call `HttpClient` directly.

## Authentication

Authentication is the `AuthService` from the User Access Control lesson, without changes:

- `login()` sends the email and password and stores the JWT in a signal and in `localStorage`.
- `user`, `isLoggedIn` and `isAdmin` are computed from the token's payload.
- A timer logs the user out when the token expires.
- `authInterceptor` adds `Authorization: Bearer <token>` to every request and logs out on **401**.

```ts
// auth/auth.service.ts (shortened)
readonly user = computed(() => {
  const token = this._token();
  return token ? decodeToken(token) : null;
});
readonly isLoggedIn = computed(() => this.user() !== null);
readonly isAdmin = computed(() => this.user()?.role === 'admin');
```

The login page uses a template-driven form with `ngModel`. After a successful login, it opens the page that the user wanted, or `/employees`.

## Authorization

The app checks permissions in **three** places:

| Place | What it does | Example |
|---|---|---|
| Route guards | Block a whole page | `/employees/new` only for admins |
| Templates | Hide buttons the user may not use | **Add**, **Edit** and **Delete** only for admins |
| The server | Refuses forbidden requests with 403 | A viewer's `DELETE` request fails |

The server check is the one that really protects the data. The other two give a better experience.

```angular runnable title="Role-based buttons and routes"
import { Component, Injectable, computed, inject, signal } from '@angular/core';
import { CanActivateFn, Router, RouterLink, RouterOutlet, Routes } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class AuthService {
  readonly role = signal<string | null>('user');
  readonly isAdmin = computed(() => this.role() === 'admin');
}

export const roleGuard: CanActivateFn = route => {
  const allowed: string[] = route.data['roles'] ?? [];
  return allowed.includes(inject(AuthService).role() ?? '') ? true : inject(Router).createUrlTree(['/forbidden']);
};

@Component({
  selector: 'app-employee-list',
  imports: [RouterLink],
  template: `
    <h4>Employees</h4>
    @if (auth.isAdmin()) {
      <p><a routerLink="/new">+ Add employee</a></p>
    }
    <p>Asha Rao, Ravi Kumar, Meena Iyer</p>
  `,
})
export class EmployeeList {
  auth = inject(AuthService);
}

@Component({ selector: 'app-employee-form', template: `<h4>Add employee</h4><p>The form for HR admins.</p>` })
export class EmployeeForm {}

@Component({ selector: 'app-forbidden', template: `<p>⛔ Only HR administrators can open this page.</p>` })
export class Forbidden {}

export const routes: Routes = [
  { path: '', component: EmployeeList },
  { path: 'new', component: EmployeeForm, canActivate: [roleGuard], data: { roles: ['admin'] } },
  { path: 'forbidden', component: Forbidden },
];

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <p>
      Role: <b>{{ auth.role() }}</b>
      <button (click)="auth.role.set('admin')">HR admin</button>
      <button (click)="auth.role.set('user')">Viewer</button>
      | <a routerLink="/">List</a> | <a routerLink="/new">/new (typed URL)</a>
    </p>
    <router-outlet />
  `,
})
export class App {
  auth = inject(AuthService);
}
```

As a viewer, the **Add employee** link is hidden, and opening `/new` directly shows the "access denied" page. Click **HR admin** and try again.

## Reusable Components

Three small components in `shared/` are used on several pages:

- **`PageHeader`**: a title, a subtitle, and an area for buttons. The buttons are passed in with `<ng-content />`, so each page can add its own.
- **`StatusBadge`**: a green "Active" or grey "Inactive" label.
- **`SearchBox`**: an input that reports its text with an `@Output()`.

All three use `OnPush` and get their data only through inputs, so Angular checks them only when an input changes.

```angular runnable title="Reusable PageHeader and StatusBadge"
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
  selector: 'app-page-header',
  template: `
    <div class="header">
      <div>
        <h3>{{ title }}</h3>
        @if (subtitle) {
          <p class="subtitle">{{ subtitle }}</p>
        }
      </div>
      <div><ng-content /></div>
    </div>
  `,
  styles: `
    .header { display: flex; justify-content: space-between; align-items: center; }
    h3 { margin: 0; }
    .subtitle { margin: 2px 0 0; color: #6b7280; }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PageHeader {
  @Input() title = '';
  @Input() subtitle = '';
}

@Component({
  selector: 'app-status-badge',
  template: `<span [class.active]="active">{{ active ? 'Active' : 'Inactive' }}</span>`,
  styles: `
    span { padding: 2px 10px; border-radius: 10px; font-size: 13px; background: #e5e7eb; }
    span.active { background: #dcfce7; color: #166534; }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StatusBadge {
  @Input() active = true;
}

@Component({
  selector: 'app-root',
  imports: [PageHeader, StatusBadge],
  template: `
    <app-page-header title="Employees" subtitle="3 active of 4">
      <button>+ Add employee</button>
    </app-page-header>
    @for (employee of employees; track employee.id) {
      <p>{{ employee.name }} <app-status-badge [active]="employee.active" /></p>
    }
    <app-page-header title="Asha Rao" subtitle="Frontend Developer">
      <button>Edit</button> <button>Delete</button>
    </app-page-header>
  `,
})
export class App {
  employees = [
    { id: 1, name: 'Asha Rao', active: true },
    { id: 2, name: 'John Peter', active: false },
  ];
}
```

The same `PageHeader` shows one button on the list page and two buttons on the details page.

## Routing

The app has public routes, protected routes and a lazy-loaded employee section:

```ts
// app.routes.ts
export const routes: Routes = [
  { path: '', redirectTo: 'employees', pathMatch: 'full' },
  { path: 'login', component: Login, canActivate: [guestGuard] },
  {
    path: 'employees',
    canActivate: [authGuard],
    loadChildren: () => import('./employees/employees.routes').then(m => m.EMPLOYEE_ROUTES),
  },
  { path: 'forbidden', component: Forbidden },
  { path: '**', component: NotFound },
];
```

```ts
// employees/employees.routes.ts
export const EMPLOYEE_ROUTES: Routes = [
  { path: '', component: EmployeeList },
  { path: 'new', component: EmployeeForm, canActivate: [roleGuard], data: { roles: ['admin'] } },
  { path: ':id', component: EmployeeDetail },
  { path: ':id/edit', component: EmployeeForm, canActivate: [roleGuard], data: { roles: ['admin'] } },
];
```

<figure className="ngc-fig">
<svg viewBox="0 0 640 244" role="img" aria-label="The root routes lead to login, the lazy employees section with its four child routes, and the forbidden and not-found pages.">
<defs><marker id="ngc-m2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-solid c-blue"><rect x="230" y="6" width="180" height="48" rx="9" /><text x="320.0" y="35" textAnchor="middle" style={{fontSize:"14px"}}>app.routes.ts</text></g></g><line className="ngc-link ngc-flow c-slate" x1="320" y1="56" x2="100" y2="88" markerEnd="url(#ngc-m2)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-orange"><rect x="10" y="90" width="180" height="50" rx="9" /><text x="100.0" y="112" textAnchor="middle" style={{fontSize:"14px"}}>login</text><text className="ngc-sub" x="100.0" y="130" textAnchor="middle">guestGuard</text></g></g><line className="ngc-link ngc-flow c-slate" x1="320" y1="56" x2="320" y2="88" markerEnd="url(#ngc-m2)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-purple"><rect x="230" y="90" width="180" height="50" rx="9" /><text x="320.0" y="112" textAnchor="middle" style={{fontSize:"14px"}}>employees</text><text className="ngc-sub" x="320.0" y="130" textAnchor="middle">authGuard + lazy</text></g></g><line className="ngc-link ngc-flow c-slate" x1="320" y1="56" x2="540" y2="88" markerEnd="url(#ngc-m2)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-orange"><rect x="450" y="90" width="180" height="50" rx="9" /><text x="540.0" y="112" textAnchor="middle" style={{fontSize:"14px"}}>** / forbidden</text><text className="ngc-sub" x="540.0" y="130" textAnchor="middle">public</text></g></g><line className="ngc-link ngc-flow c-slate" x1="320" y1="142" x2="81" y2="178" markerEnd="url(#ngc-m2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="10" y="180" width="142" height="50" rx="9" /><text x="81.0" y="202" textAnchor="middle" style={{fontSize:"13px"}}>''</text><text className="ngc-sub" x="81.0" y="220" textAnchor="middle">EmployeeList</text></g></g><line className="ngc-link ngc-flow c-slate" x1="320" y1="142" x2="239" y2="178" markerEnd="url(#ngc-m2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="168" y="180" width="142" height="50" rx="9" /><text x="239.0" y="202" textAnchor="middle" style={{fontSize:"13px"}}>new</text><text className="ngc-sub" x="239.0" y="220" textAnchor="middle">roleGuard admin</text></g></g><line className="ngc-link ngc-flow c-slate" x1="320" y1="142" x2="397" y2="178" markerEnd="url(#ngc-m2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="326" y="180" width="142" height="50" rx="9" /><text x="397.0" y="202" textAnchor="middle" style={{fontSize:"13px"}}>:id</text><text className="ngc-sub" x="397.0" y="220" textAnchor="middle">EmployeeDetail</text></g></g><line className="ngc-link ngc-flow c-slate" x1="320" y1="142" x2="555" y2="178" markerEnd="url(#ngc-m2)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-green"><rect x="484" y="180" width="142" height="50" rx="9" /><text x="555.0" y="202" textAnchor="middle" style={{fontSize:"13px"}}>:id/edit</text><text className="ngc-sub" x="555.0" y="220" textAnchor="middle">roleGuard admin</text></g></g>
</svg>
<figcaption>The route tree. The employees section is loaded only after login.</figcaption>
</figure>

- `withComponentInputBinding()` puts `:id` into an `@Input() id` of the details and form components.
- `new` is listed **before** `:id`.
- The same `EmployeeForm` is used for **add** (no id) and **edit** (with an id).

Route order matters. If `:id` came before `new`, the router would match `/employees/new` with `:id` = "new" and open the details page instead of the form.

## Employee State

`EmployeeStore` holds all employee data in signals, as in the State Management lesson:

- **Private writable signals:** the employee list, the search text and the department.
- **Read-only signals for components:** created with `asReadonly()`.
- **Computed signals:** `filtered`, `total` and `activeCount`.
- **Methods:** `load()`, `setSearch()`, `setDepartment()`, `create()`, `update()` and `delete()`.

```angular runnable title="EmployeeStore with search and filter"
import { Component, Injectable, computed, inject, signal } from '@angular/core';

export interface Employee {
  id: number;
  name: string;
  department: string;
  active: boolean;
}

@Injectable({ providedIn: 'root' })
export class EmployeeStore {
  private readonly _employees = signal<Employee[]>([
    { id: 1, name: 'Asha Rao', department: 'Engineering', active: true },
    { id: 2, name: 'Ravi Kumar', department: 'Engineering', active: true },
    { id: 3, name: 'Meena Iyer', department: 'Design', active: true },
    { id: 4, name: 'John Peter', department: 'Sales', active: false },
  ]);
  private readonly _search = signal('');
  private readonly _department = signal('All');

  readonly department = this._department.asReadonly();
  readonly filtered = computed(() => {
    const text = this._search().toLowerCase();
    const department = this._department();
    return this._employees().filter(
      e => (department === 'All' || e.department === department) && e.name.toLowerCase().includes(text),
    );
  });
  readonly total = computed(() => this._employees().length);
  readonly activeCount = computed(() => this._employees().filter(e => e.active).length);

  setSearch(text: string) {
    this._search.set(text);
  }
  setDepartment(department: string) {
    this._department.set(department);
  }
}

@Component({
  selector: 'app-root',
  template: `
    <p>{{ store.activeCount() }} active of {{ store.total() }} employees</p>
    <input placeholder="Search by name" (input)="store.setSearch($any($event.target).value)" />
    <p>
      @for (d of departments; track d) {
        <button (click)="store.setDepartment(d)" [disabled]="d === store.department()">{{ d }}</button>
      }
    </p>
    @for (e of store.filtered(); track e.id) {
      <p>{{ e.name }} ({{ e.department }})</p>
    } @empty {
      <p>No employees match your search.</p>
    }
  `,
})
export class App {
  store = inject(EmployeeStore);
  departments = ['All', 'Engineering', 'Design', 'Sales'];
}
```

Type a name, click a department, and combine both. The list is a computed signal, so it updates by itself.

After a successful save, the store updates its list with `update()`, so every page shows the new data without loading the whole list again:

```ts
update(employee: Employee) {
  return this.api.update(employee).pipe(
    tap(saved => this._employees.update(list => list.map(e => (e.id === saved.id ? saved : e)))),
  );
}
```

## Employee Forms

`EmployeeForm` is a reactive form built with `FormBuilder`:

```ts
form = this.fb.nonNullable.group({
  name: ['', [Validators.required, Validators.minLength(2)]],
  email: ['', [Validators.required, Validators.email]],
  department: ['Engineering', Validators.required],
  position: ['', Validators.required],
  salary: [50000, [Validators.required, Validators.min(0)]],
  joinDate: ['', [Validators.required, notInFuture]],
  active: [true],
});
```

- `nonNullable` makes `reset()` return each control to its first value instead of `null`.
- In **edit** mode, the component loads the employee and fills the form with `setValue()`.
- On **Save**, it calls `store.create()` or `store.update()` and opens the details page.

## Form Validation

The form uses built-in validators and one **custom validator**, `notInFuture`, from the Form Validation lesson:

```ts
export function notInFuture(control: AbstractControl): ValidationErrors | null {
  if (!control.value) return null;
  const today = new Date().toISOString().slice(0, 10);
  return control.value > today ? { futureDate: true } : null;
}
```

Messages appear only after the user touched or changed a field. On **Save**, `markAllAsTouched()` shows every message at once.

```angular runnable title="The employee form with validation"
import { Component, inject, signal } from '@angular/core';
import { AbstractControl, FormBuilder, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';

export function notInFuture(control: AbstractControl): ValidationErrors | null {
  if (!control.value) return null;
  const today = new Date().toISOString().slice(0, 10);
  return control.value > today ? { futureDate: true } : null;
}

@Component({
  selector: 'app-root',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="save()">
      <p>Name <input formControlName="name" />
        @if (showError('name')) { <span class="error">At least 2 characters.</span> }</p>
      <p>Email <input formControlName="email" />
        @if (showError('email')) { <span class="error">Enter a valid email.</span> }</p>
      <p>Salary <input formControlName="salary" type="number" />
        @if (showError('salary')) { <span class="error">0 or more.</span> }</p>
      <p>Joining date <input formControlName="joinDate" type="date" />
        @if (form.controls.joinDate.hasError('futureDate')) {
          <span class="error">Cannot be in the future.</span>
        } @else if (showError('joinDate')) {
          <span class="error">Required.</span>
        }</p>
      <button type="submit">Save</button>
    </form>
    <p>{{ result() }}</p>
  `,
  styles: `.error { color: #b91c1c; font-size: 13px; }`,
})
export class App {
  private fb = inject(FormBuilder);
  result = signal('');

  form = this.fb.nonNullable.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    salary: [50000, [Validators.required, Validators.min(0)]],
    joinDate: ['2099-01-01', [Validators.required, notInFuture]],
  });

  showError(name: string) {
    const control = this.form.get(name);
    return !!control && control.invalid && (control.touched || control.dirty);
  }

  save() {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      this.result.set('Please fix the marked fields.');
      return;
    }
    this.result.set(`Saved: ${this.form.getRawValue().name}`);
  }
}
```

The joining date is in the future, so its message is shown from the start. Click **Save** to see all the other messages, then fill in the fields correctly.

## Employee Records

The list and details pages display the employee records:

- The **list** shows a table row per employee, with `track employee.id`, and a link to `['/employees', employee.id]`.
- The **details** page reads the `id` input, requests `GET /api/employees/:id`, and shows the record with the `currency` and `date` pipes.
- **Delete** asks for confirmation with the browser's `confirm()` dialog, calls `store.delete()`, and returns to the list.

```angular runnable title="Records: list, details and delete"
import { ApplicationConfig, Component, Input, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { Router, RouterLink, RouterOutlet, Routes, provideRouter, withComponentInputBinding } from '@angular/router';
import { delay, mergeMap, of, throwError, timer } from 'rxjs';

@Component({
  selector: 'app-employee-list',
  imports: [RouterLink],
  template: `
    <h4>Employees</h4>
    @for (e of employees(); track e.id) {
      <p><a [routerLink]="['/employees', e.id]">{{ e.name }}</a> ({{ e.position }})</p>
    }
  `,
})
export class EmployeeList {
  employees = signal<any[]>([]);
  constructor() {
    inject(HttpClient).get<any[]>('/api/employees').subscribe(list => this.employees.set(list));
  }
}

@Component({
  selector: 'app-employee-detail',
  imports: [RouterLink],
  template: `
    <p><a routerLink="/employees">← All employees</a></p>
    @if (employee(); as e) {
      <h4>{{ e.name }}</h4>
      <p>{{ e.position }}, {{ e.department }}, {{ e.active ? 'active' : 'inactive' }}</p>
      <button (click)="remove(e.id)">Delete</button>
    } @else {
      <p>{{ message() }}</p>
    }
  `,
})
export class EmployeeDetail {
  private http = inject(HttpClient);
  private router = inject(Router);
  employee = signal<any>(null);
  message = signal('Loading...');

  @Input() set id(value: string) {
    this.http.get<any>(`/api/employees/${value}`).subscribe({
      next: e => this.employee.set(e),
      error: (err: HttpErrorResponse) => this.message.set(err.error.message),
    });
  }

  remove(id: number) {
    this.http.delete(`/api/employees/${id}`).subscribe(() => this.router.navigate(['/employees']));
  }
}

const routes: Routes = [
  { path: '', redirectTo: 'employees', pathMatch: 'full' },
  { path: 'employees', component: EmployeeList },
  { path: 'employees/:id', component: EmployeeDetail },
];

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([practiceServer])),
  ],
};

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `<router-outlet />`,
})
export class App {}
// ─── Practice server ──────────────────────────────────────────────
// Answers the employees API inside the browser, like in the API Integration lesson.
let employees = [
  { id: 1, name: 'Asha Rao', department: 'Engineering', position: 'Frontend Developer', active: true },
  { id: 2, name: 'Ravi Kumar', department: 'Engineering', position: 'Backend Developer', active: true },
  { id: 3, name: 'Meena Iyer', department: 'Design', position: 'UX Designer', active: true },
  { id: 4, name: 'John Peter', department: 'Sales', position: 'Account Manager', active: false },
];
let serverDown = false;

function practiceServer(req: HttpRequest<any>, next: HttpHandlerFn) {
  if (serverDown) return fail(500, 'Internal Server Error');
  const id = Number(req.url.split('/')[3]);
  if (req.url === '/api/employees' && req.method === 'GET') return reply(employees);
  if (req.method === 'GET') {
    const employee = employees.find(e => e.id === id);
    return employee ? reply(employee) : fail(404, 'Employee not found.');
  }
  if (req.method === 'DELETE') {
    employees = employees.filter(e => e.id !== id);
    return reply(null);
  }
  return fail(404, 'Not Found');
}

function reply(body: unknown) {
  return of(new HttpResponse({ status: 200, body })).pipe(delay(250));
}

function fail(status: number, message: string) {
  return timer(250).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status, error: { message } }))));
}

```

Open an employee, delete them, and see that the list has one employee less. (The practice block deletes without asking, to keep it short.)

## API Integration

`EmployeeApiService` contains every request for employees. It reads the server address from the environment file:

```ts
@Injectable({ providedIn: 'root' })
export class EmployeeApiService {
  private http = inject(HttpClient);
  private url = `${environment.apiUrl}/api/employees`;

  getAll() { return this.http.get<Employee[]>(this.url); }
  getOne(id: number) { return this.http.get<Employee>(`${this.url}/${id}`); }
  create(employee: NewEmployee) { return this.http.post<Employee>(this.url, employee); }
  update(employee: Employee) { return this.http.put<Employee>(`${this.url}/${employee.id}`, employee); }
  delete(id: number) { return this.http.delete<void>(`${this.url}/${id}`); }
}
```

- `NewEmployee` is `Omit<Employee, 'id'>`: a new employee has no id yet. The server creates it.
- The interceptor adds the token, so the service does not need to know about authentication.

## Error Handling

The app handles errors at three levels:

1. **Requests:** the store catches HTTP errors and turns the status code into a clear message with `messageFor()`. The list shows the message with a **Try again** button.
2. **Forms:** server errors such as **409** ("This email is already used.") appear under the form.
3. **Everything else:** `AppErrorHandler` replaces `ErrorHandler`, writes the error to the console and shows a banner at the top of the app.

```ts
export function messageFor(err: HttpErrorResponse, fallback: string) {
  if (err.status === 0) return 'Could not reach the server. Is "npm run api" running?';
  if (err.status === 403) return 'Only administrators can do this.';
  return err.error?.message ?? fallback;
}
```

```angular runnable title="Loading and error states with Try again"
import { ApplicationConfig, Component, inject, signal } from '@angular/core';
import {
  HttpClient, HttpErrorResponse, HttpHandlerFn, HttpRequest, HttpResponse,
  provideHttpClient, withInterceptors,
} from '@angular/common/http';
import { delay, finalize, mergeMap, of, throwError, timer } from 'rxjs';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([practiceServer]))],
};

@Component({
  selector: 'app-root',
  template: `
    <label><input type="checkbox" (change)="toggleServer()" /> Server is down</label>
    <button (click)="load()">Reload</button>
    @if (loading()) {
      <p>Loading employees...</p>
    } @else if (error()) {
      <p>⚠ {{ error() }} <button (click)="load()">Try again</button></p>
    } @else {
      <p>{{ count() }} employees loaded.</p>
    }
  `,
})
export class App {
  private http = inject(HttpClient);
  loading = signal(false);
  error = signal('');
  count = signal(0);

  constructor() {
    this.load();
  }

  toggleServer() {
    serverDown = !serverDown;
  }

  load() {
    this.loading.set(true);
    this.error.set('');
    this.http
      .get<any[]>('/api/employees')
      .pipe(finalize(() => this.loading.set(false)))
      .subscribe({
        next: list => this.count.set(list.length),
        error: (err: HttpErrorResponse) => this.error.set(`Could not load the employees (${err.status}).`),
      });
  }
}
// ─── Practice server ──────────────────────────────────────────────
// Answers the employees API inside the browser, like in the API Integration lesson.
let employees = [
  { id: 1, name: 'Asha Rao', department: 'Engineering', position: 'Frontend Developer', active: true },
  { id: 2, name: 'Ravi Kumar', department: 'Engineering', position: 'Backend Developer', active: true },
  { id: 3, name: 'Meena Iyer', department: 'Design', position: 'UX Designer', active: true },
  { id: 4, name: 'John Peter', department: 'Sales', position: 'Account Manager', active: false },
];
let serverDown = false;

function practiceServer(req: HttpRequest<any>, next: HttpHandlerFn) {
  if (serverDown) return fail(500, 'Internal Server Error');
  const id = Number(req.url.split('/')[3]);
  if (req.url === '/api/employees' && req.method === 'GET') return reply(employees);
  if (req.method === 'GET') {
    const employee = employees.find(e => e.id === id);
    return employee ? reply(employee) : fail(404, 'Employee not found.');
  }
  if (req.method === 'DELETE') {
    employees = employees.filter(e => e.id !== id);
    return reply(null);
  }
  return fail(404, 'Not Found');
}

function reply(body: unknown) {
  return of(new HttpResponse({ status: 200, body })).pipe(delay(250));
}

function fail(status: number, message: string) {
  return timer(250).pipe(mergeMap(() => throwError(() => new HttpErrorResponse({ status, error: { message } }))));
}

```

Tick **Server is down**, then click **Reload**. Untick it and click **Try again**.

## Performance Optimization

The project uses the techniques from the Performance Optimization lesson:

- **Lazy loading:** the employee section is a separate file, loaded after login with `loadChildren`. The login page loads faster.
- **`OnPush`** in every component. All data comes from signals and inputs, so no update is missed.
- **Computed signals** for the filtered list and the counters, instead of methods in templates.
- **`track employee.id`** in the table, so a reload keeps the existing rows.
- **Store updates after saving**, instead of loading the whole list again.

Run `npm run build` in the downloaded project. The output shows the employee section under **Lazy chunk files**, separate from the main bundle.

## Final Review

<figure className="ngc-fig">
<svg viewBox="0 0 640 134" role="img" aria-label="Login stores a token, guards check access, the store loads employees through the API, and errors show a message.">
<defs><marker id="ngc-m3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-step ngc-t1"><g className="ngc-node ngc-solid c-blue"><rect x="6" y="30" width="113" height="64" rx="9" /><text x="62.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Log in</text><text className="ngc-sub" x="62.5" y="77" textAnchor="middle">JWT stored</text></g></g><line className="ngc-link ngc-flow c-slate" x1="120" y1="62" x2="134" y2="62" markerEnd="url(#ngc-m3)" /><g className="ngc-step ngc-t2"><g className="ngc-node ngc-soft c-orange"><rect x="135" y="30" width="113" height="64" rx="9" /><text x="191.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Guards</text><text className="ngc-sub" x="191.5" y="77" textAnchor="middle">authGuard, roleGuard</text></g></g><line className="ngc-link ngc-flow c-slate" x1="249" y1="62" x2="263" y2="62" markerEnd="url(#ngc-m3)" /><g className="ngc-step ngc-t3"><g className="ngc-node ngc-soft c-purple"><rect x="264" y="30" width="113" height="64" rx="9" /><text x="320.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Store loads</text><text className="ngc-sub" x="320.5" y="77" textAnchor="middle">GET /api/employees</text></g></g><line className="ngc-link ngc-flow c-slate" x1="377" y1="62" x2="391" y2="62" markerEnd="url(#ngc-m3)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-green"><rect x="392" y="30" width="113" height="64" rx="9" /><text x="448.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Records</text><text className="ngc-sub" x="448.5" y="77" textAnchor="middle">list, add, edit, delete</text></g></g><line className="ngc-link ngc-flow c-slate" x1="506" y1="62" x2="520" y2="62" markerEnd="url(#ngc-m3)" /><g className="ngc-step ngc-t4"><g className="ngc-node ngc-soft c-red"><rect x="521" y="30" width="113" height="64" rx="9" /><text x="577.5" y="59" textAnchor="middle" style={{fontSize:"14px"}}>Errors</text><text className="ngc-sub" x="577.5" y="77" textAnchor="middle">messages + banner</text></g></g><text className="ngc-note" x="320" y="128" textAnchor="middle">Every part of the app, from login to error handling.</text>
</svg>
<figcaption>One user journey through the whole app.</figcaption>
</figure>

Check your understanding of the whole app with these questions:

- **Where is the employee list stored?** In `EmployeeStore`, in a private signal.
- **Who sends the token?** `authInterceptor`, for every request.
- **What stops a viewer from deleting an employee?** The server answers 403. The UI also hides the button and the guard blocks the edit page.
- **What happens when the token expires?** The timer calls `logout()`, and the login page shows "Your session has expired".
- **What happens when the server is down?** The list shows a clear message with **Try again**.

Ideas to extend the project on your own:

- Add **pagination** to the employee list.
- Add a **dashboard** page with counts per department, using computed signals.
- Add a **"My profile"** page for viewers.

## Download Project Files

### Download Employee Management Project Source Code

<a href="/downloads/angular-employee-management.zip" download>Download angular-employee-management.zip</a>

The ZIP file contains the complete project: the Angular app and the Node.js API server, which needs no extra packages.

| Folder | What it contains |
|---|---|
| `server/` | Login with JWT, and the employee API with role checks |
| `src/app/auth/` | `AuthService`, interceptor and guards |
| `src/app/core/` | The global error handler and the error banner |
| `src/app/shared/` | `PageHeader`, `StatusBadge` and `SearchBox` |
| `src/app/employees/` | Model, API service, store, list, details, form, validator and lazy routes |
| `src/app/pages/` | Login, access denied and not found |

### Project Setup Instructions

You need **Node.js 20.19 or newer**.

1. Unzip the file and open a terminal in the `employee-management` folder.
2. Install the packages:

   ```bash
   npm install
   ```

3. Start the API server on `http://localhost:3000`. Keep this terminal open:

   ```bash
   npm run api
   ```

4. In a second terminal, start the Angular app:

   ```bash
   npm start
   ```

5. Open `http://localhost:4200` and log in:

   | Email | Password | Role |
   |---|---|---|
   | admin@company.com | Admin123! | HR admin: view, add, edit, delete |
   | viewer@company.com | Viewer123! | Viewer: view only |

To test automatic logout, start the API with `npm run api:short` instead. Tokens then expire after 30 seconds.

## Common Mistakes

<div className="ngc-mistakes">

| Mistake | What you see | Fix |
|---|---|---|
| Components call `HttpClient` directly | Request code is repeated, and pages show different data | Use the API service and the store |
| `:id` route listed before `new` | `/employees/new` opens the details page for the id "new" | List `new` before `:id` |
| Only hiding buttons for viewers | Viewers can still change data with direct API calls | Check the role on the server too |
| Loading the whole list after every save | Slow pages and extra requests | Update the store's signal with the saved employee |
| No `markAllAsTouched()` on save | An invalid form shows no messages | Call it when the form is invalid |

</div>

## Check Yourself

## Exercises

## What to Remember

- Organize a real app by feature: models, API services, a signal store, components and lazy-loaded routes. Data flows from components to the store, to the API service, to the server.
- Combine authentication (JWT, interceptor) with authorization in three places: guards, templates and the server.
- Use reactive forms with built-in and custom validators, handle errors at every level, and keep the app fast with lazy loading, `OnPush`, computed signals and `track`.

Next: **Cheat Sheet**, a short reference of everything in this course.
