import { Component, inject, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { BehaviorSubject, catchError, combineLatest, debounceTime, distinctUntilChanged, map, Observable, of, retry, Subject, takeUntil, startWith } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; import { ComponentType } from '@angular/cdk/portal'; import { Qualification } from '../Qualification'; import QualificationService from '../../services/qualification.service'; import { CreateComponent } from '../create/create.component'; import { EditComponent } from '../edit/edit.component'; import { DeleteComponent } from '../delete/delete.component'; import { DetailsComponent } from '../details/details.component'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBar, MatSnackBarConfig, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatDividerModule } from '@angular/material/divider'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatMenuModule } from '@angular/material/menu'; import { MatTableModule } from '@angular/material/table'; import { MatSortModule } from '@angular/material/sort'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; class SearchEngine { static readonly DEBOUNCE_TIME = 300; constructor( private readonly items: Qualification[], private readonly searchTerm: string ) {} execute(): Qualification[] { if (!this.searchTerm?.trim()) { return this.items; } const normalizedTerm = this.searchTerm.toLowerCase().trim(); return this.items.filter(item => item.skill?.toLowerCase().includes(normalizedTerm) ); } } class ErrorHandler { private readonly config: MatSnackBarConfig = { duration: 5000, horizontalPosition: 'end', verticalPosition: 'bottom', panelClass: ['error-snackbar'] }; constructor(private readonly snackBar: MatSnackBar) {} handleHttpError(error: HttpErrorResponse): Observable { console.error('API Error:', error); this.showError('Failed to load qualifications. Please try again.'); return of([]); } showError(message: string): void { this.snackBar.open(message, 'Close', this.config); } } class DialogManager { constructor( private readonly dialog: MatDialog, private readonly errorHandler: ErrorHandler ) {} openDialog( component: ComponentType, data?: T ): MatDialogRef { return this.dialog.open(component, { data }); } } interface QualificationState { items: Qualification[]; filteredItems: Qualification[]; searchTerm: string; isLoading: boolean; isSearching: boolean; error: string | null; } class QualificationStore { private readonly initialState: QualificationState = { items: [], filteredItems: [], searchTerm: '', isLoading: false, isSearching: false, error: null }; private readonly state = new BehaviorSubject(this.initialState); private readonly searchTerm$ = new Subject(); readonly items$ = this.state.pipe(map(state => state.filteredItems)); readonly isLoading$ = this.state.pipe(map(state => state.isLoading)); readonly isSearching$ = this.state.pipe(map(state => state.isSearching)); readonly error$ = this.state.pipe(map(state => state.error)); constructor( private readonly api: QualificationService, private readonly errorHandler: ErrorHandler ) { this.setupSearch(); } private setupSearch(): void { this.searchTerm$.pipe( startWith(''), debounceTime(SearchEngine.DEBOUNCE_TIME), distinctUntilChanged() ).subscribe(term => { this.state.next({ ...this.state.value, isSearching: true, searchTerm: term }); const currentState = this.state.value; const engine = new SearchEngine(currentState.items, term); const filteredItems = engine.execute(); setTimeout(() => { this.state.next({ ...currentState, searchTerm: term, filteredItems, isSearching: false }); }, 150); }); } load(): void { this.setLoading(true); this.api.getAll().pipe( retry(3), catchError(error => this.errorHandler.handleHttpError(error)) ).subscribe(items => { const engine = new SearchEngine(items, this.state.value.searchTerm); this.state.next({ ...this.state.value, items, filteredItems: engine.execute(), isLoading: false, error: null }); }); } search(term: string): void { this.searchTerm$.next(term); } private setLoading(isLoading: boolean): void { this.state.next({ ...this.state.value, isLoading }); } complete(): void { this.state.complete(); this.searchTerm$.complete(); } } @Component({ selector: 'app-qualifications', standalone: true, imports: [ CommonModule, MatCardModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, MatDividerModule, MatTooltipModule, MatMenuModule, MatTableModule, MatSortModule, MatFormFieldModule, MatInputModule ], templateUrl: './table.component.html', styleUrl: './table.component.css' }) export class QualificationsComponent implements OnInit, OnDestroy { private readonly errorHandler = new ErrorHandler(inject(MatSnackBar)); private readonly dialogManager = new DialogManager( inject(MatDialog), this.errorHandler ); private readonly store = new QualificationStore( inject(QualificationService), this.errorHandler ); private readonly destroy$ = new Subject(); protected readonly qualifications$ = this.store.items$; protected readonly isLoading$ = this.store.isLoading$; protected readonly isSearching$ = this.store.isSearching$; protected readonly error$ = this.store.error$; protected readonly displayedColumns = ['skill', 'actions'] as const; ngOnInit(): void { this.loadQualifications(); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); this.store.complete(); } protected onSearch(event: Event): void { const searchInput = event.target as HTMLInputElement; this.store.search(searchInput.value); } protected createQualification(): void { this.openModalAndReload(CreateComponent); } protected editQualification(qualification: Qualification): void { this.openModalAndReload(EditComponent, qualification); } protected deleteQualification(id: number): void { this.openModalAndReload(DeleteComponent, id); } protected viewQualificationDetails(qualification: Qualification): void { this.dialogManager.openDialog(DetailsComponent, qualification); } private loadQualifications(): void { this.store.load(); } private openModalAndReload(component: ComponentType, data?: T): void { const dialogRef = this.dialogManager.openDialog(component, data); dialogRef.afterClosed() .pipe(takeUntil(this.destroy$)) .subscribe(success => { if (success === true) { this.loadQualifications(); } }); } }