Frontend Architecture
Technical architecture overview of the B12 SIS Angular frontend.
Technology Stack
Technology |
Version |
Purpose |
|---|---|---|
Angular |
19 |
Core framework |
Angular Material |
19 |
UI components |
Bootstrap |
5 |
Layout utilities |
ngx-translate |
Latest |
Internationalization |
ngx-charts |
Latest |
Data visualization |
FullCalendar |
Latest |
Calendar views |
RxJS |
7+ |
Reactive programming |
Application Architecture
┌─────────────────────────────────────────────────────────────┐
│ App Component │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ Auth Layout │ │ Main Layout │ │
│ │ (Login pages) │ │ ┌─────────────────────────┐ │ │
│ │ │ │ │ Header │ │ │
│ │ │ │ ├─────────────────────────┤ │ │
│ │ │ │ │ Sidebar │ Content │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ <router- │ │ │
│ │ │ │ │ │ outlet> │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ └─────────────────────────┘ │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Module Architecture
Lazy Loading Strategy
All feature modules are lazy-loaded to optimize initial bundle size:
// app.routes.ts
{
path: 'module',
canActivate: [AuthGuard],
loadChildren: () =>
import('./features/features.routes').then((m) => m.ROUTE),
}
// features/features.routes.ts
{
path: 'students',
loadChildren: () => import('./students/routes').then((m) => m.ROUTE),
}
Feature Modules
75+ feature modules covering all SIS functionality:
Academic Management
academic_sessions- Academic years/termseducation_levels- Education level definitionsgrade_levels- Grade/year levelssubjects- Subject catalogcourses- Course managementclasses- Class sectionstimetables- Scheduling
People Management
students- Student recordsteachers- Teacher recordsguardians- Parent/guardian recordsusers- System users
Enrollment
student_enroll_classes- Class enrollmentstudent_enroll_courses- Course enrollmentstudent_enroll_programs- Program enrollment
Grading & Assessment
gradebooks- Gradebook managementassessments- Assessment definitionsassessment_results- Student scoresgrade_scales- Grading scalesreport_card_templates- Report card templatesstudent_report_cards- Generated report cards
Attendance & Behavior
attendances- Attendance recordsattendance_configs- Attendance settingsbehaviors- Behavior incidents
Medical & Health
medical_cares- Medical visitsmedical_care_histories- Medical historymedical_timeslots- Appointment scheduling
Administration
roles- Role managementteams- Multi-tenant teamsimport_datas- Data importsystem_settings- System configuration
Core Module
The core/ module provides application-wide services and utilities:
Services
core/services/
├── api.service.ts # HTTP client wrapper
├── auth.service.ts # Authentication state
├── backend.service.ts # Higher-level API operations
├── broadcast.service.ts # Cross-component communication
├── language.service.ts # Language management
├── lti-auth.service.ts # LTI authentication
├── modal.service.ts # Modal dialogs
├── notification.service.ts # Toast notifications
└── oidc-auth.service.ts # OIDC/SSO authentication
HTTP Interceptors
core/interceptor/
├── jwt.interceptor.ts # Bearer token injection
├── team.interceptor.ts # X-Team-ID header
├── academic-session.interceptor.ts # Academic session context
└── error.interceptor.ts # Global error handling
Request Flow:
Request → JwtInterceptor → TeamInterceptor → AcademicSessionInterceptor → API
Response ← ErrorInterceptor ← HTTP Response
Route Guards
// auth.guard.ts - Protects authenticated routes
canActivate(route: ActivatedRouteSnapshot): boolean {
const currentUser = this.authService.currentUserValue;
if (currentUser && currentUser.token?.access_token) {
return true;
}
this.router.navigate(['/authentication/signin']);
return false;
}
// module-import.guard.ts - Controls module access
canActivate(route: ActivatedRouteSnapshot): boolean {
// Check user permissions for module
return this.hasModuleAccess(route.data['module']);
}
Base Component Pattern
Abstract base components provide consistent behavior:
Component Hierarchy
BaseComponent
├── BaseListComponent (list views)
├── BaseDetailComponent (detail/edit views)
└── BaseCreateComponent (create forms)
BaseComponent
@Component({ template: '' })
export abstract class BaseComponent implements OnDestroy {
protected destroy$ = new Subject<void>();
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
BaseListComponent
@Component({ template: '' })
export abstract class BaseListComponent extends BaseComponent {
abstract listConfig: GenericListConfig;
@Output() extraActionClick = new EventEmitter<{ action: any; model: any }>();
@Output() headerExtraActionClick = new EventEmitter<{ action: any }>();
protected setupTranslations(): void {
// Auto-translate list configuration
}
onExtraActionClicked(event: { action: any; model: any }): void {
// Override in child components
}
}
State Management
Service-based state with RxJS observables:
@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUserSubject: BehaviorSubject<Auth>;
public currentUser: Observable<Auth>;
constructor() {
this.currentUserSubject = new BehaviorSubject<Auth>(
JSON.parse(localStorage.getItem('currentUser') || '{}')
);
this.currentUser = this.currentUserSubject.asObservable();
}
public get currentUserValue(): Auth {
return this.currentUserSubject.value;
}
}
State Flow
User Action → Component → Service → BehaviorSubject → Subscribers
↓
API Call → Backend
↓
Response → Update Subject → UI Update
Multi-Tenancy
Team-based data isolation:
// TeamInterceptor adds header to all requests
request = request.clone({
setHeaders: {
'X-Team-ID': `${teamId}`,
},
});
Team Selection:
// AuthService.setActiveTeam()
setActiveTeam(teamId: string): Observable<Auth> {
this.currentUserValue.active_team_id = teamId;
this.currentUserSubject.next(this.currentUserValue);
localStorage.setItem('currentUser', JSON.stringify(this.currentUserValue));
return of(this.currentUserValue);
}
Authentication Flow
Standard Login
1. User submits credentials
2. AuthService.login() calls POST /api/auth/login
3. Server returns JWT tokens
4. AuthService stores tokens in localStorage
5. AuthService calls GET /api/auth/me for user details
6. Permissions loaded into auth state
7. JwtInterceptor adds token to subsequent requests
Token Storage
// In AuthService
localStorage.setItem('currentUser', JSON.stringify({
token: { access_token, refresh_token },
user: { ... },
permissions: [...],
active_team_id: '...'
}));
// In JwtInterceptor
localStorage.setItem('access_token', currentUser.token.access_token);
LTI Authentication
// LtiAuthService handles LTI 1.3 launches
// Tokens provided by LMS platform
localStorage.setItem('lti_mode', 'true');
OIDC/SSO Authentication
// OidcAuthService handles OIDC flows
// Redirects to identity provider
// Processes callback with tokens
Internationalization
ngx-translate Setup
// app.config.ts
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient]
}
})
Translation Keys
// Component
this.translate.instant('students.list.title')
// Template
{{ 'students.list.title' | translate }}
// With parameters
{{ 'students.enrolled' | translate:{ count: studentCount } }}
Language Switching
// LanguageService
setLanguage(lang: string) {
this.translate.use(lang);
localStorage.setItem('lang', lang);
}
Error Handling
ErrorInterceptor
intercept(request: HttpRequest<any>, next: HttpHandler) {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Token expired - logout
this.authService.logout();
this.router.navigate(['/authentication/signin']);
}
// Show notification
this.notification.showError(error.error?.message || 'Error');
return throwError(() => error);
})
);
}
Component-Level Handling
this.api.post('endpoint', data).subscribe({
next: (response) => {
this.notification.showSuccess('Saved successfully');
},
error: (error) => {
// ErrorInterceptor handles common cases
// Custom handling if needed
console.error('Specific error handling:', error);
}
});
Performance Optimization
Lazy Loading
All feature modules loaded on-demand reduces initial bundle.
Change Detection
Use OnPush strategy where applicable:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
Subscription Management
Always cleanup subscriptions:
this.api.get('endpoint')
.pipe(takeUntil(this.destroy$))
.subscribe(...);
TrackBy Functions
Optimize ngFor loops:
<tr *ngFor="let item of items; trackBy: trackById">