Frontend Development Guide

Complete guide for developing the B12 SIS Angular frontend application.

Prerequisites

  • Node.js 18+ and npm

  • Angular CLI 19+

  • IDE with TypeScript support (VS Code recommended)

Project Setup

# Clone repository
git clone <repo-url> b12-frontend
cd b12-frontend

# Install dependencies
npm install

# Start development server
npm start

The development server runs at http://localhost:4200.

Project Structure

b12-frontend/
├── src/
│   ├── app/
│   │   ├── authentication/     # Login, signup, password reset
│   │   ├── config/             # App configuration
│   │   ├── core/               # Core services, guards, interceptors
│   │   │   ├── base/           # Base components (list, detail, create)
│   │   │   ├── guard/          # Route guards
│   │   │   ├── i18n/           # Internationalization
│   │   │   ├── interceptor/    # HTTP interceptors
│   │   │   ├── models/         # Data models
│   │   │   ├── services/       # Core services
│   │   │   └── types/          # TypeScript types
│   │   ├── features/           # Feature modules (75+ modules)
│   │   ├── layout/             # App layouts (main, auth)
│   │   └── shared/             # Shared components, directives, pipes
│   ├── assets/                 # Static assets
│   └── environments/           # Environment configurations
├── angular.json                # Angular CLI config
├── package.json
└── tsconfig.json

Core Concepts

Base Components

The frontend uses base component classes to standardize CRUD operations:

// Base list component example
export class StudentsListComponent extends BaseListComponent {
  override listConfig: GenericListConfig = {
    module: 'Student',
    title: 'Students',
    createRoute: '/module/students/create',
    detailRoute: '/module/students',
    showActionsColumn: true
  };
}

Available base components:

  • BaseComponent - Root component with destroy$ subject

  • BaseListComponent - List views with GenericListComponent

  • BaseDetailComponent - Detail/edit views

  • BaseCreateComponent - Create forms

HTTP Interceptors

Four interceptors handle cross-cutting concerns:

Interceptor

Purpose

JwtInterceptor

Adds Bearer token to requests

TeamInterceptor

Adds X-Team-ID header for multi-tenancy

AcademicSessionInterceptor

Adds academic session context

ErrorInterceptor

Handles HTTP errors globally

Route Guards

// AuthGuard protects all authenticated routes
{
  path: '',
  component: MainLayoutComponent,
  canActivate: [AuthGuard],
  children: [...]
}

Feature Modules

Each feature module follows a consistent structure:

features/students/
├── list/
│   ├── list.component.ts
│   ├── list.component.html
│   └── list.component.scss
├── detail/
│   ├── detail.component.ts
│   ├── detail.component.html
│   └── detail.component.scss
├── create/
│   ├── create.component.ts
│   ├── create.component.html
│   └── create.component.scss
└── routes.ts

Creating a New Feature Module

  1. Create module directory:

ng generate component features/my-module/list
ng generate component features/my-module/detail
ng generate component features/my-module/create
  1. Define routes (routes.ts):

import { Route } from '@angular/router';
import { ListComponent } from './list/list.component';
import { DetailComponent } from './detail/detail.component';
import { CreateComponent } from './create/create.component';

export const ROUTE: Route[] = [
  { path: 'list', component: ListComponent },
  { path: 'create', component: CreateComponent },
  { path: ':id', component: DetailComponent },
  { path: '**', redirectTo: 'list' }
];
  1. Add to features routes (features/features.routes.ts):

{
  path: 'my_modules',
  loadChildren: () => import('./my-module/routes').then((m) => m.ROUTE),
},
  1. Implement list component:

import { Component } from '@angular/core';
import { BaseListComponent } from '@core/base/list.base.component';
import { GenericListConfig } from '@shared/components/generic-list/generic-list.component';

@Component({
  selector: 'app-my-module-list',
  templateUrl: './list.component.html',
})
export class ListComponent extends BaseListComponent {
  override listConfig: GenericListConfig = {
    module: 'MyModule',
    title: 'My Modules',
    createRoute: '/module/my_modules/create',
    detailRoute: '/module/my_modules',
    showActionsColumn: true,
    showCreateButton: true,
    showDeleteButton: true
  };
}

Core Services

ApiService

Generic HTTP client wrapper:

import { ApiService } from '@core/services/api.service';

@Injectable()
export class MyService {
  constructor(private api: ApiService) {}

  getItems() {
    return this.api.get<Response>('my_modules/list');
  }

  createItem(data: any) {
    return this.api.post<Response>('my_modules', data);
  }

  updateItem(id: string, data: any) {
    return this.api.put<Response>(`my_modules/${id}`, data);
  }

  deleteItem(id: string) {
    return this.api.delete<Response>(`my_modules/${id}`);
  }
}

AuthService

Handles authentication state:

import { AuthService } from '@core/services/auth.service';

@Component({...})
export class MyComponent {
  constructor(private authService: AuthService) {}

  get currentUser() {
    return this.authService.currentUserValue;
  }

  get permissions() {
    return this.authService.currentUserValue.permissions;
  }

  get activeTeamId() {
    return this.authService.currentUserValue.active_team_id;
  }
}

BackendService

Higher-level API operations:

import { BackendService } from '@core/services/backend.service';

// Generic list with filtering
this.backend.list('students', {
  filters: [...],
  page: 1,
  pageSize: 25
});

Shared Components

113+ reusable components in shared/components/:

Key Components

Component

Purpose

generic-list

Standard list with filtering, pagination

generic-form

Dynamic form generation

editable-field-renderer

Field type rendering

file-upload

File upload with S3 integration

breadcrumb

Navigation breadcrumbs

Using GenericListComponent

<app-generic-list
  [config]="listConfig"
  (extraActionClick)="onExtraActionClicked($event)"
  (headerExtraActionClick)="onHeaderExtraActionClicked($event)">
</app-generic-list>

Internationalization (i18n)

The app uses ngx-translate for multi-language support:

// In component
constructor(private translate: TranslateService) {}

// Switch language
this.translate.use('vi');

// Get translation
this.translate.instant('students.title');
<!-- In template -->
{{ 'students.title' | translate }}

Languages are loaded from the backend API.

Environment Configuration

// environments/environment.ts (development)
export const environment = {
  production: false,
  apiUrl: 'http://localhost:8080/api'
};

// environments/environment.prod.ts (production)
export const environment = {
  production: true,
  apiUrl: 'https://api.school.edu/api'
};

Building & Deployment

# Development build
npm run build:dev

# Production build
npm run build:prod

# Run tests
npm test

# Lint code
npm run lint

Docker Build

# Build image
docker build -t b12-frontend .

# Run container
docker run -p 80:80 b12-frontend

Kubernetes Deployment

# Interactive deployment
make deploy

# Select environment when prompted

Best Practices

  1. Extend base components for consistent behavior

  2. Use lazy loading for all feature modules

  3. Follow naming conventions: module_name for paths, ModuleName for classes

  4. Add translations for all user-facing text

  5. Use shared components instead of duplicating code

  6. Handle errors via the ErrorInterceptor

  7. Clean up subscriptions using takeUntil(this.destroy$)

Common Tasks

Adding a Column to GenericList

listConfig: GenericListConfig = {
  module: 'Student',
  // Add custom columns
  extraColumns: [
    { field: 'custom_field', header: 'Custom Field' }
  ]
};

Custom Row Actions

listConfig: GenericListConfig = {
  module: 'Student',
  extraActions: [
    { icon: 'assignment', action: 'enroll', tooltip: 'Enroll' }
  ]
};

onExtraActionClicked(event: { action: any; model: any }) {
  if (event.action === 'enroll') {
    this.enrollStudent(event.model);
  }
}

Accessing Route Parameters

import { ActivatedRoute } from '@angular/router';

constructor(private route: ActivatedRoute) {}

ngOnInit() {
  const id = this.route.snapshot.paramMap.get('id');
}