diff --git a/README.md b/README.md index 75b2576..58cb66e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # Starter für das LF10 Projekt - ## Requirements -* Docker https://docs.docker.com/get-docker/ -* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/ - +- Docker https://docs.docker.com/get-docker/ +- Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/ ### Abhängigkeiten starten (Postgres, EmployeeBackend) @@ -37,7 +35,7 @@ http://localhost:8089/swagger # Postgres -``` +```` ### Intellij-Ansicht für Postgres Datenbank einrichten (geht nicht in Webstorm!) @@ -50,8 +48,8 @@ http://localhost:8089/swagger 6. URL der DB einfügen (jdbc:postgresql://postgres-employee:5432/employee_db) und PostgreSQL-Treiber auswählen, mit OK bestätigen 7. Username lf10_starter und Passwort secret eintragen (siehe application.properties), mit Apply bestätigen 8. im Reiter Schemas alle Häkchen entfernen und lediglich vor lf10_starter_db und public Häkchen setzen -9. mit Apply und ok bestätigen -``` +9. mit Apply und ok bestätigen +```` # Keycloak @@ -80,57 +78,54 @@ die ClientId deines Angular Frontends lautet: employee-management-service-fronte Hier ein Beispiel einer app.config.ts mit der Konfiguration für Keycloak. Mit dem KeycloakService, der hier definiert wird, kannst du in einem AuthGuard z.B. feststellen, ob der Benutzer eingeloggt ist oder nicht oder ihn mit keycloakService.login() zum Login weiterleiten. ```typescript -import {APP_INITIALIZER, ApplicationConfig} from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { APP_INITIALIZER, ApplicationConfig } from "@angular/core"; +import { provideRouter } from "@angular/router"; -import { routes } from './app.routes'; -import {KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular"; -import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http"; +import { routes } from "./app.routes"; +import { KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService } from "keycloak-angular"; +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; export const initializeKeycloak = (keycloak: KeycloakService) => async () => keycloak.init({ config: { - url: 'KEYCLOAK_URL', - realm: 'REALM', - clientId: 'CLIENT_ID', + url: "KEYCLOAK_URL", + realm: "REALM", + clientId: "CLIENT_ID", }, loadUserProfileAtStartUp: true, initOptions: { - onLoad: 'check-sso', - silentCheckSsoRedirectUri: - window.location.origin + '/silent-check-sso.html', + onLoad: "check-sso", + silentCheckSsoRedirectUri: window.location.origin + "/silent-check-sso.html", checkLoginIframe: false, - redirectUri: 'http://localhost:4200', + redirectUri: "http://localhost:4200", }, }); - function initializeApp(keycloak: KeycloakService): () => Promise { return () => initializeKeycloak(keycloak)(); } export const appConfig: ApplicationConfig = { - providers: [provideRouter(routes), - KeycloakAngularModule, - { - provide: APP_INITIALIZER, - useFactory: initializeApp, - multi: true, - deps: [KeycloakService] - }, - KeycloakService, - provideHttpClient(withInterceptorsFromDi()), - { - provide: HTTP_INTERCEPTORS, - useClass: KeycloakBearerInterceptor, - multi: true - } - ] - }; - + providers: [ + provideRouter(routes), + KeycloakAngularModule, + { + provide: APP_INITIALIZER, + useFactory: initializeApp, + multi: true, + deps: [KeycloakService], + }, + KeycloakService, + provideHttpClient(withInterceptorsFromDi()), + { + provide: HTTP_INTERCEPTORS, + useClass: KeycloakBearerInterceptor, + multi: true, + }, + ], +}; ``` - Der Benutzer, mit dem ihr eure Integration testen könnt, hat den Benutzernamen user und das Passwort test. Die einzige Rolle heißt user. Des Weiteren ist der Client mit der Bezeichnung employee-management-service-frontend wie folgt konfiguriert: @@ -140,5 +135,4 @@ Des Weiteren ist der Client mit der Bezeichnung employee-management-service-fron # Bugs -Trage hier die Features ein, die nicht funktionieren. Beschreibe den jeweiligen Fehler. - +Trage hier die Features ein, die nicht funktionieren. Beschreibe den jeweiligen Fehler. diff --git a/angular.json b/angular.json index ab37160..f61b593 100644 --- a/angular.json +++ b/angular.json @@ -16,9 +16,7 @@ "outputPath": "dist/lf10-starter2024", "index": "src/index.html", "browser": "src/main.ts", - "polyfills": [ - "zone.js" - ], + "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": [ { @@ -74,10 +72,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], + "polyfills": ["zone.js", "zone.js/testing"], "tsConfig": "tsconfig.spec.json", "assets": [ { @@ -95,10 +90,7 @@ "lint": { "builder": "@angular-eslint/builder:lint", "options": { - "lintFilePatterns": [ - "src/**/*.ts", - "src/**/*.html" - ] + "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] } } } @@ -106,8 +98,6 @@ }, "cli": { "analytics": "33c8483f-3876-4eb5-9c9b-1001cab9b273", - "schematicCollections": [ - "angular-eslint" - ] + "schematicCollections": ["angular-eslint"] } } diff --git a/compose.yml b/compose.yml index 5c6c64a..f4d1aeb 100644 --- a/compose.yml +++ b/compose.yml @@ -1,4 +1,3 @@ - volumes: employee_postgres_data: driver: local diff --git a/eslint.config.js b/eslint.config.js index 99a007a..8379fae 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -39,5 +39,5 @@ module.exports = tseslint.config( ...angular.configs.templateAccessibility, ], rules: {}, - } + }, ); diff --git a/package.json b/package.json index c39545f..2de770f 100644 --- a/package.json +++ b/package.json @@ -47,4 +47,4 @@ "typescript": "~5.5.4", "typescript-eslint": "8.18.0" } -} \ No newline at end of file +} diff --git a/public/silent-check-sso.html b/public/silent-check-sso.html index 8eca9a7..b3bd540 100644 --- a/public/silent-check-sso.html +++ b/public/silent-check-sso.html @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/src/app/app.component.html b/src/app/app.component.html index e824d2e..a972e3e 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,5 +1,4 @@

{{ title }}

- +
- diff --git a/src/app/app.component.ts b/src/app/app.component.ts index db3e8f5..71801fe 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,13 +1,13 @@ -import {Component} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {RouterOutlet} from '@angular/router'; +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', imports: [CommonModule, RouterOutlet], templateUrl: './app.component.html', standalone: true, - styleUrl: './app.component.css' + styleUrl: './app.component.css', }) export class AppComponent { title = 'Employee Management System'; diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 15465eb..7df7984 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,9 +1,17 @@ -import {APP_INITIALIZER, ApplicationConfig} from '@angular/core'; -import {provideRouter} from '@angular/router'; -import {KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular"; -import {routes} from './app.routes'; -import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http"; -import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; +import { APP_INITIALIZER, ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { + KeycloakAngularModule, + KeycloakBearerInterceptor, + KeycloakService, +} from 'keycloak-angular'; +import { routes } from './app.routes'; +import { + HTTP_INTERCEPTORS, + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; export const initializeKeycloak = (keycloak: KeycloakService) => async () => keycloak.init({ @@ -22,7 +30,6 @@ export const initializeKeycloak = (keycloak: KeycloakService) => async () => }, }); - function initializeApp(keycloak: KeycloakService): () => Promise { return () => initializeKeycloak(keycloak)(); } @@ -37,14 +44,14 @@ export const appConfig: ApplicationConfig = { provide: APP_INITIALIZER, useFactory: initializeApp, multi: true, - deps: [KeycloakService] + deps: [KeycloakService], }, KeycloakService, provideHttpClient(withInterceptorsFromDi()), { provide: HTTP_INTERCEPTORS, useClass: KeycloakBearerInterceptor, - multi: true - } - ] + multi: true, + }, + ], }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 9c15317..597ac57 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,10 +1,10 @@ -import {Routes} from '@angular/router'; -import {LoginComponent} from "./login/login.component"; -import {AuthGuardService} from "./services/auth-guard.service"; -import {HomeComponent} from "./home/home.component"; +import { Routes } from '@angular/router'; +import { LoginComponent } from './login/login.component'; +import { AuthGuardService } from './services/auth-guard.service'; +import { HomeComponent } from './home/home.component'; export const routes: Routes = [ - {path: 'login', component: LoginComponent}, - {path: '', component: HomeComponent, canActivate: [AuthGuardService]}, - {path: '**', redirectTo: ''} + { path: 'login', component: LoginComponent }, + { path: '', component: HomeComponent, canActivate: [AuthGuardService] }, + { path: '**', redirectTo: '' }, ]; diff --git a/src/app/employee/Employee.ts b/src/app/employee/Employee.ts index 10e19d1..5683fcc 100644 --- a/src/app/employee/Employee.ts +++ b/src/app/employee/Employee.ts @@ -1,4 +1,4 @@ -import {Qualification} from "../qualification/Qualification"; +import { Qualification } from '../qualification/Qualification'; export class Employee { constructor( @@ -9,7 +9,6 @@ export class Employee { public postcode?: string, public city?: string, public phone?: string, - public skillSet?: Qualification[] - ) { - } + public skillSet?: Qualification[], + ) {} } diff --git a/src/app/employee/create/create.component.html b/src/app/employee/create/create.component.html index 1c30474..780d66e 100644 --- a/src/app/employee/create/create.component.html +++ b/src/app/employee/create/create.component.html @@ -5,74 +5,98 @@
First Name - + Enter the first name - {{errors['firstName']}} + {{ + errors["firstName"] + }} Last Name - + Enter the last name - {{errors['lastName']}} + {{ + errors["lastName"] + }}
Street - + Enter the street address - {{errors['street']}} + {{ errors["street"] }}
City - + Enter the city - {{errors['city']}} + {{ errors["city"] }} Postcode - + Enter postcode - {{errors['postcode']}} + {{ + errors["postcode"] + }}
Phone - + Enter the phone number - {{errors['phone']}} + {{ errors["phone"] }} Qualifications Select the qualifications - - {{qualification.skill}} + + {{ qualification.skill }} - {{errors['qualifications']}} + {{ + errors["qualifications"] + }} - - - - diff --git a/src/app/employee/create/create.component.ts b/src/app/employee/create/create.component.ts index 6773957..7b99ee1 100644 --- a/src/app/employee/create/create.component.ts +++ b/src/app/employee/create/create.component.ts @@ -1,23 +1,29 @@ -import {Component, inject, OnInit} from '@angular/core'; -import {AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; -import {MatFormField, MatHint, MatLabel} from "@angular/material/form-field"; -import {MatError, MatInput} from "@angular/material/input"; -import {MatButton} from "@angular/material/button"; +import { Component, inject, OnInit } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatFormField, MatHint, MatLabel } from '@angular/material/form-field'; +import { MatError, MatInput } from '@angular/material/input'; +import { MatButton } from '@angular/material/button'; import { MatDialogActions, MatDialogClose, MatDialogContent, MatDialogRef, - MatDialogTitle -} from "@angular/material/dialog"; -import {Employee} from "../Employee"; -import EmployeeApiService from "../../services/employee-api.service"; -import {NgForOf, NgIf} from "@angular/common"; -import {MatOption} from "@angular/material/core"; -import {MatSelect} from "@angular/material/select"; -import QualificationService from "../../services/qualification.service"; -import {Qualification} from "../../qualification/Qualification"; -import {debounceTime} from "rxjs"; + MatDialogTitle, +} from '@angular/material/dialog'; +import { Employee } from '../Employee'; +import EmployeeApiService from '../../services/employee-api.service'; +import { NgForOf, NgIf } from '@angular/common'; +import { MatOption } from '@angular/material/core'; +import { MatSelect } from '@angular/material/select'; +import QualificationService from '../../services/qualification.service'; +import { Qualification } from '../../qualification/Qualification'; +import { debounceTime } from 'rxjs'; @Component({ selector: 'app-create-employee', @@ -36,11 +42,11 @@ import {debounceTime} from "rxjs"; MatSelect, NgForOf, MatError, - MatHint + MatHint, ], templateUrl: './create.component.html', standalone: true, - styleUrl: './create.component.css' + styleUrl: './create.component.css', }) export class CreateComponent implements OnInit { employeeForm!: FormGroup; @@ -49,17 +55,17 @@ export class CreateComponent implements OnInit { dialogRef: MatDialogRef = inject(MatDialogRef); qualificationService: QualificationService = inject(QualificationService); qualifications: Qualification[] = []; - errorMsgs: { [key: string]: string } = { + errorMsgs: Record = { firstName: 'First name is required', lastName: 'Last name is required', street: 'Street is required', postcode: 'Postcode must be 5 characters long', city: 'City is required', phone: 'Phone is required', - qualifications: 'Qualifications are required' - } + qualifications: 'Qualifications are required', + }; - errors: { [key: string]: string } = {} + errors: Record = {}; ngOnInit(): void { this.loadQualifications(); @@ -70,7 +76,7 @@ export class CreateComponent implements OnInit { postcode: ['', [Validators.required, this.validatePostcode]], city: ['', Validators.required], phone: ['', Validators.required], - qualifications: [[]] + qualifications: [[]], }); Object.keys(this.employeeForm.controls).forEach((controlName: string) => { @@ -82,9 +88,9 @@ export class CreateComponent implements OnInit { } loadQualifications() { - this.qualificationService.getAll().subscribe( - qualifications => this.qualifications = qualifications - ); + this.qualificationService + .getAll() + .subscribe((qualifications) => (this.qualifications = qualifications)); } submit() { @@ -95,7 +101,7 @@ export class CreateComponent implements OnInit { const formValue = this.employeeForm.value; const employeeData = { ...formValue, - skillSet: formValue.qualifications + skillSet: formValue.qualifications, }; this.employeeService.create(employeeData as Employee).subscribe(); @@ -111,7 +117,7 @@ export class CreateComponent implements OnInit { validatePostcode(control: AbstractControl) { const postcode = control.value as number; if (postcode.toString().length !== 5) { - return {invalidPostcode: true}; + return { invalidPostcode: true }; } return null; diff --git a/src/app/employee/delete/delete.component.html b/src/app/employee/delete/delete.component.html index 0c99100..7628a58 100644 --- a/src/app/employee/delete/delete.component.html +++ b/src/app/employee/delete/delete.component.html @@ -1,31 +1,48 @@ -

Delete Employee

+

+ Delete Employee +

- warning + warning

- Are you sure you want to delete {{employee.firstName}} {{employee.lastName}}? + Are you sure you want to delete {{ employee.firstName }} + {{ employee.lastName }}? +

+

+ This action cannot be undone.

-

This action cannot be undone.

- - - diff --git a/src/app/employee/delete/delete.component.ts b/src/app/employee/delete/delete.component.ts index d3bb241..3abd016 100644 --- a/src/app/employee/delete/delete.component.ts +++ b/src/app/employee/delete/delete.component.ts @@ -1,15 +1,16 @@ -import {Component, Inject, inject} from '@angular/core'; -import {Employee} from "../Employee"; +import { Component, inject } from '@angular/core'; +import { Employee } from '../Employee'; import { MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, - MatDialogContent, MatDialogRef, - MatDialogTitle -} from "@angular/material/dialog"; -import {MatButton} from "@angular/material/button"; -import {MatIcon} from "@angular/material/icon"; -import EmployeeApiService from "../../services/employee-api.service"; + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { MatButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import EmployeeApiService from '../../services/employee-api.service'; @Component({ selector: 'app-delete-employee', @@ -19,11 +20,11 @@ import EmployeeApiService from "../../services/employee-api.service"; MatDialogActions, MatButton, MatDialogClose, - MatIcon + MatIcon, ], templateUrl: './delete.component.html', standalone: true, - styleUrl: './delete.component.css' + styleUrl: './delete.component.css', }) export class DeleteComponent { private apiService: EmployeeApiService = inject(EmployeeApiService); diff --git a/src/app/employee/details/details.component.html b/src/app/employee/details/details.component.html index be56bee..4269952 100644 --- a/src/app/employee/details/details.component.html +++ b/src/app/employee/details/details.component.html @@ -1,21 +1,32 @@ -

Employee Details

+

+ Employee Details +

-
-
+
+
- {{ employee.firstName?.charAt(0) ?? '' }}{{ employee.lastName?.charAt(0) ?? '' }} + {{ employee.firstName?.charAt(0) ?? "" + }}{{ employee.lastName?.charAt(0) ?? "" }}
-

{{ employee.firstName }} {{ employee.lastName }}

+

+ {{ employee.firstName }} {{ employee.lastName }} +

ID: {{ employee.id }}

-

Contact Information

+

+ Contact Information +

Phone

@@ -45,11 +56,15 @@
-

Qualifications

+

+ Qualifications +

@for (skill of employee.skillSet; track skill.id) { -
- {{skill.skill}} +
+ {{ skill.skill }}
} @empty {

No qualifications added

@@ -60,9 +75,11 @@ - diff --git a/src/app/employee/details/details.component.ts b/src/app/employee/details/details.component.ts index 3dbafb4..a60c3e6 100644 --- a/src/app/employee/details/details.component.ts +++ b/src/app/employee/details/details.component.ts @@ -1,20 +1,20 @@ -import {Component, inject} from '@angular/core'; -import {MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogTitle} from "@angular/material/dialog"; -import {Employee} from "../Employee"; -import {MatButton} from "@angular/material/button"; -import {DialogRef} from "@angular/cdk/dialog"; +import { Component, inject } from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogContent, + MatDialogTitle, +} from '@angular/material/dialog'; +import { Employee } from '../Employee'; +import { MatButton } from '@angular/material/button'; +import { DialogRef } from '@angular/cdk/dialog'; @Component({ selector: 'app-details', - imports: [ - MatDialogTitle, - MatDialogContent, - MatButton, - MatDialogActions - ], + imports: [MatDialogTitle, MatDialogContent, MatButton, MatDialogActions], templateUrl: './details.component.html', standalone: true, - styleUrl: './details.component.css' + styleUrl: './details.component.css', }) export class DetailsComponent { employee: Employee = inject(MAT_DIALOG_DATA); diff --git a/src/app/employee/edit/edit.component.html b/src/app/employee/edit/edit.component.html index 3226dad..2312f79 100644 --- a/src/app/employee/edit/edit.component.html +++ b/src/app/employee/edit/edit.component.html @@ -5,74 +5,98 @@
First Name - + Enter the first name - {{errors['firstName']}} + {{ + errors["firstName"] + }} Last Name - + Enter the last name - {{errors['lastName']}} + {{ + errors["lastName"] + }}
Street - + Enter the street address - {{errors['street']}} + {{ errors["street"] }}
City - + Enter the city - {{errors['city']}} + {{ errors["city"] }} Postcode - + Enter postcode - {{errors['postcode']}} + {{ + errors["postcode"] + }}
Phone - + Enter phone number - {{errors['phone']}} + {{ errors["phone"] }} Qualifications - - {{qualification.skill}} + + {{ qualification.skill }} Select qualifications - {{errors['qualifications']}} + {{ + errors["qualifications"] + }} - - -
- diff --git a/src/app/employee/edit/edit.component.ts b/src/app/employee/edit/edit.component.ts index d67beee..769e9f9 100644 --- a/src/app/employee/edit/edit.component.ts +++ b/src/app/employee/edit/edit.component.ts @@ -1,23 +1,29 @@ -import {Component, inject, OnInit} from '@angular/core'; -import {AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; +import { Component, inject, OnInit } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent, MatDialogRef, - MatDialogTitle -} from "@angular/material/dialog"; -import {NgForOf, NgIf} from "@angular/common"; -import {MatFormField, MatHint} from "@angular/material/form-field"; -import {MatError, MatInput, MatLabel} from "@angular/material/input"; -import {MatButton} from "@angular/material/button"; -import {Employee} from "../Employee"; -import EmployeeApiService from "../../services/employee-api.service"; -import QualificationService from "../../services/qualification.service"; -import {Qualification} from "../../qualification/Qualification"; -import {MatOption, MatSelect} from "@angular/material/select"; -import {debounceTime} from "rxjs"; + MatDialogTitle, +} from '@angular/material/dialog'; +import { NgForOf, NgIf } from '@angular/common'; +import { MatFormField, MatHint } from '@angular/material/form-field'; +import { MatError, MatInput, MatLabel } from '@angular/material/input'; +import { MatButton } from '@angular/material/button'; +import { Employee } from '../Employee'; +import EmployeeApiService from '../../services/employee-api.service'; +import QualificationService from '../../services/qualification.service'; +import { Qualification } from '../../qualification/Qualification'; +import { MatOption, MatSelect } from '@angular/material/select'; +import { debounceTime } from 'rxjs'; @Component({ selector: 'app-edit', @@ -40,7 +46,7 @@ import {debounceTime} from "rxjs"; ], templateUrl: './edit.component.html', standalone: true, - styleUrl: './edit.component.css' + styleUrl: './edit.component.css', }) export class EditComponent implements OnInit { employeeForm!: FormGroup; @@ -50,17 +56,17 @@ export class EditComponent implements OnInit { dialogRef: MatDialogRef = inject(MatDialogRef); employee: Employee = inject(MAT_DIALOG_DATA); qualifications: Qualification[] = []; - errorMsgs: { [key: string]: string } = { + errorMsgs: Record = { firstName: 'First name is required', lastName: 'Last name is required', street: 'Street is required', postcode: 'Postcode must be 5 characters long', city: 'City is required', phone: 'Phone is required', - qualifications: 'Qualifications are required' - } + qualifications: 'Qualifications are required', + }; - errors: { [key: string]: string } = {} + errors: Record = {}; ngOnInit(): void { this.loadQualifications(); @@ -68,10 +74,13 @@ export class EditComponent implements OnInit { firstName: [this.employee.firstName, Validators.required], lastName: [this.employee.lastName, Validators.required], street: [this.employee.street, Validators.required], - postcode: [this.employee.postcode, [Validators.required, this.validatePostcode]], + postcode: [ + this.employee.postcode, + [Validators.required, this.validatePostcode], + ], city: [this.employee.city, Validators.required], phone: [this.employee.phone, Validators.required], - qualifications: [this.employee.skillSet?.map(skill => skill.id) ?? []] + qualifications: [this.employee.skillSet?.map((skill) => skill.id) ?? []], }); Object.keys(this.employeeForm.controls).forEach((controlName: string) => { @@ -83,9 +92,9 @@ export class EditComponent implements OnInit { } loadQualifications() { - this.qualificationService.getAll().subscribe( - qualifications => this.qualifications = qualifications - ); + this.qualificationService + .getAll() + .subscribe((qualifications) => (this.qualifications = qualifications)); } submit() { @@ -102,10 +111,12 @@ export class EditComponent implements OnInit { const formValue = this.employeeForm.value; const employeeData = { ...formValue, - skillSet: formValue.qualifications + skillSet: formValue.qualifications, }; - this.employeeService.update(employeeData as Employee, this.employee.id).subscribe(); + this.employeeService + .update(employeeData as Employee, this.employee.id) + .subscribe(); this.dialogRef.close(true); } @@ -118,7 +129,7 @@ export class EditComponent implements OnInit { validatePostcode(control: AbstractControl) { const postcode = control.value as number; if (postcode.toString().length !== 5) { - return {invalidPostcode: true}; + return { invalidPostcode: true }; } return null; diff --git a/src/app/employee/table/table.component.html b/src/app/employee/table/table.component.html index d60787b..67f3877 100644 --- a/src/app/employee/table/table.component.html +++ b/src/app/employee/table/table.component.html @@ -1,113 +1,163 @@
@defer { - @if (employees$ | async; as employees) { -
-
-
-

Employee Directory

- - search - -
- + @if (employees$ | async; as employees) { +
+
+
+

+ Employee Directory +

+ + search + +
+ +
+
- + +
+ + @if (employees) { +
+ + + + + + + + + + + + + +
+ Name + +
+
+ + {{ employee.firstName[0] }}{{ employee.lastName[0] }} + +
+
+ +
+
+
+ Actions + +
+ + +
+
+
+ } @else { + + + people +

No employees found

+
+
+ }
- -
- - @if (employees) { -
- - - - - - - - - - - - - -
Name -
-
- - {{ employee.firstName[0] }}{{ employee.lastName[0] }} - -
- -
-
Actions -
- - -
-
-
- } @else { - - - people -

No employees found

-
-
} -
- } } @placeholder { -
-
-
-
-
-
-
-
-
- @for (i of [1, 2, 3]; track i) { -
- } +
+
+
+
+
+
+
+
+
+ @for (i of [1, 2, 3]; track i) { +
+ } +
-
} @error { -
-
- error -
-

There was an error loading the employees.

-

Please try refreshing the page.

+
+
+ error +
+

+ There was an error loading the employees. +

+

+ Please try refreshing the page. +

+
-
} @loading { -
- -
+
+ +
} -
\ No newline at end of file + diff --git a/src/app/employee/table/table.component.ts b/src/app/employee/table/table.component.ts index c97b115..68090ff 100644 --- a/src/app/employee/table/table.component.ts +++ b/src/app/employee/table/table.component.ts @@ -1,28 +1,36 @@ -import {Component, inject, OnInit} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {catchError, debounceTime, distinctUntilChanged, Observable, of, retry, Subject} from 'rxjs'; -import {HttpErrorResponse} from '@angular/common/http'; -import {Employee} from '../Employee'; +import { Component, inject, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + catchError, + debounceTime, + distinctUntilChanged, + Observable, + of, + retry, + Subject, +} from 'rxjs'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Employee } from '../Employee'; -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 {MatSnackBarModule} from '@angular/material/snack-bar'; -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 {MatDialog} from "@angular/material/dialog"; -import {DeleteComponent} from "../delete/delete.component"; -import EmployeeApiService from "../../services/employee-api.service"; -import {CreateComponent} from "../create/create.component"; -import {EditComponent} from "../edit/edit.component"; -import {DetailsComponent} from "../details/details.component"; -import {MatFormFieldModule} from "@angular/material/form-field"; -import {MatInputModule} from "@angular/material/input"; -import {ErrorHandlerService} from "../../services/error.handler.service"; +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 { MatSnackBarModule } from '@angular/material/snack-bar'; +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 { MatDialog } from '@angular/material/dialog'; +import { DeleteComponent } from '../delete/delete.component'; +import EmployeeApiService from '../../services/employee-api.service'; +import { CreateComponent } from '../create/create.component'; +import { EditComponent } from '../edit/edit.component'; +import { DetailsComponent } from '../details/details.component'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { ErrorHandlerService } from '../../services/error.handler.service'; @Component({ selector: 'app-employee-list', @@ -43,12 +51,13 @@ import {ErrorHandlerService} from "../../services/error.handler.service"; MatInputModule, ], templateUrl: './table.component.html', - styleUrl: './table.component.css' + styleUrl: './table.component.css', }) export class TableComponent implements OnInit { private readonly apiService: EmployeeApiService = inject(EmployeeApiService); private readonly matDialog: MatDialog = inject(MatDialog); - private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService); + private readonly errorHandlerService: ErrorHandlerService = + inject(ErrorHandlerService); private static readonly MAX_RETRIES = 3; @@ -64,27 +73,27 @@ export class TableComponent implements OnInit { } private loadEmployees(): void { - this.fetchEmployees().subscribe(employees => { + this.fetchEmployees().subscribe((employees) => { this.allEmployees = employees; this.employees$ = of(employees); }); } private setupSearch(): void { - this.searchSubject.pipe( - debounceTime(300), - distinctUntilChanged() - ).subscribe(searchTerm => { - this.isSearching = true; - setTimeout(() => { - const filteredEmployees = this.allEmployees.filter(employee => - employee.firstName?.toLowerCase().includes(searchTerm) || - employee.lastName?.toLowerCase().includes(searchTerm) - ); - this.employees$ = of(filteredEmployees); - this.isSearching = false; - }, 150); - }); + this.searchSubject + .pipe(debounceTime(300), distinctUntilChanged()) + .subscribe((searchTerm) => { + this.isSearching = true; + setTimeout(() => { + const filteredEmployees = this.allEmployees.filter( + (employee) => + employee.firstName?.toLowerCase().includes(searchTerm) || + employee.lastName?.toLowerCase().includes(searchTerm), + ); + this.employees$ = of(filteredEmployees); + this.isSearching = false; + }, 150); + }); } private fetchEmployees(): Observable { @@ -92,14 +101,17 @@ export class TableComponent implements OnInit { retry(TableComponent.MAX_RETRIES), catchError((error: HttpErrorResponse) => { console.error('Error fetching employees:', error); - this.errorHandlerService.showErrorMessage('Failed to load employees. Please try again.'); + this.errorHandlerService.showErrorMessage( + 'Failed to load employees. Please try again.', + ); return of([]); - }) + }), ); } protected openDeleteDialogue(employee: Employee): void { - this.matDialog.open(DeleteComponent, {data: employee}) + this.matDialog + .open(DeleteComponent, { data: employee }) .afterClosed() .subscribe((deleted: boolean) => { if (deleted) { @@ -109,7 +121,8 @@ export class TableComponent implements OnInit { } protected showCreateEmployeeModal() { - this.matDialog.open(CreateComponent) + this.matDialog + .open(CreateComponent) .afterClosed() .subscribe((created: boolean) => { if (created) { @@ -119,7 +132,8 @@ export class TableComponent implements OnInit { } protected showEditEmployeeModal(employee: Employee) { - this.matDialog.open(EditComponent, {data: employee}) + this.matDialog + .open(EditComponent, { data: employee }) .afterClosed() .subscribe((edited: boolean) => { if (edited) { @@ -129,7 +143,7 @@ export class TableComponent implements OnInit { } protected openDetailModal(employee: Employee) { - this.matDialog.open(DetailsComponent, {data: employee}); + this.matDialog.open(DetailsComponent, { data: employee }); } protected filterEmployees(event: Event): void { diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 4d5f1a0..c258a37 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,2 +1,2 @@ - - + + diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 8c5b336..4bde9d6 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,16 +1,11 @@ import { Component } from '@angular/core'; -import {TableComponent} from "../employee/table/table.component"; -import {QualificationsComponent} from "../qualification/table/table.component"; +import { TableComponent } from '../employee/table/table.component'; +import { QualificationsComponent } from '../qualification/table/table.component'; @Component({ selector: 'app-home', - imports: [ - TableComponent, - QualificationsComponent - ], + imports: [TableComponent, QualificationsComponent], templateUrl: './home.component.html', - styleUrl: './home.component.css' + styleUrl: './home.component.css', }) -export class HomeComponent { - -} +export class HomeComponent {} diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 5de9dba..9925a0a 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -1 +1,3 @@ -
Logging in{{ dots }}
+
+ Logging in{{ dots }} +
diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index efa3330..c776617 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -1,16 +1,16 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; -import {interval, Subscription} from "rxjs"; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { interval, Subscription } from 'rxjs'; @Component({ selector: 'app-login', imports: [], templateUrl: './login.component.html', standalone: true, - styleUrl: './login.component.css' + styleUrl: './login.component.css', }) -export class LoginComponent implements OnInit, OnDestroy{ - dots: string = ''; - private maxDots: number = 4; // Maximum number of dots +export class LoginComponent implements OnInit, OnDestroy { + dots = ''; + private maxDots = 4; // Maximum number of dots private intervalSub!: Subscription; ngOnInit(): void { diff --git a/src/app/qualification/Qualification.ts b/src/app/qualification/Qualification.ts index b71d662..0f39f76 100644 --- a/src/app/qualification/Qualification.ts +++ b/src/app/qualification/Qualification.ts @@ -1,4 +1,6 @@ export class Qualification { - constructor(public id: number, public skill?: string) { - } + constructor( + public id: number, + public skill?: string, + ) {} } diff --git a/src/app/qualification/create/create.component.html b/src/app/qualification/create/create.component.html index 7eaba29..445d18e 100644 --- a/src/app/qualification/create/create.component.html +++ b/src/app/qualification/create/create.component.html @@ -1,15 +1,30 @@ -

Create Qualification

+

+ Create Qualification +

-
+
@if (apiErrorMessage) {
- error + error
-

There was an error creating the qualification.

-

{{ apiErrorMessage }}

+

+ There was an error creating the qualification. +

+

+ {{ apiErrorMessage }} +

@@ -18,27 +33,36 @@
Skill - + Enter the skill name - {{ getErrorMessage('skill') }} + {{ getErrorMessage("skill") }}
- - - diff --git a/src/app/qualification/create/create.component.ts b/src/app/qualification/create/create.component.ts index f88996b..badcae3 100644 --- a/src/app/qualification/create/create.component.ts +++ b/src/app/qualification/create/create.component.ts @@ -1,86 +1,94 @@ -import {Component, inject} from '@angular/core'; -import {FormBuilder, ReactiveFormsModule, Validators} from "@angular/forms"; -import QualificationService from "../../services/qualification.service"; +import { Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import QualificationService from '../../services/qualification.service'; import { - MatDialogActions, - MatDialogClose, - MatDialogContent, - MatDialogRef, - MatDialogTitle -} from "@angular/material/dialog"; -import {NgIf} from "@angular/common"; -import {MatError, MatFormField, MatHint, MatLabel} from "@angular/material/form-field"; -import {MatButton} from "@angular/material/button"; -import {MatInput} from "@angular/material/input"; -import {MatIcon} from "@angular/material/icon"; -import {filter} from "rxjs"; + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { NgIf } from '@angular/common'; +import { + MatError, + MatFormField, + MatHint, + MatLabel, +} from '@angular/material/form-field'; +import { MatButton } from '@angular/material/button'; +import { MatInput } from '@angular/material/input'; +import { MatIcon } from '@angular/material/icon'; @Component({ - selector: 'app-create-qualification', - imports: [ - ReactiveFormsModule, - MatError, - NgIf, - MatLabel, - MatDialogTitle, - MatDialogContent, - MatFormField, - MatDialogActions, - MatButton, - MatInput, - MatDialogClose, - MatHint, - MatIcon - ], - templateUrl: './create.component.html', - styleUrl: './create.component.css' + selector: 'app-create-qualification', + imports: [ + ReactiveFormsModule, + MatError, + NgIf, + MatLabel, + MatDialogTitle, + MatDialogContent, + MatFormField, + MatDialogActions, + MatButton, + MatInput, + MatDialogClose, + MatHint, + MatIcon, + ], + templateUrl: './create.component.html', + styleUrl: './create.component.css', }) export class CreateComponent { - private formBuilder: FormBuilder = inject(FormBuilder); - private qualificationService: QualificationService = inject(QualificationService); - private dialogRef: MatDialogRef = inject(MatDialogRef); + private formBuilder: FormBuilder = inject(FormBuilder); + private qualificationService: QualificationService = + inject(QualificationService); + private dialogRef: MatDialogRef = inject(MatDialogRef); - public apiErrorMessage: string = ''; + public apiErrorMessage = ''; - qualificationForm = this.formBuilder.group({ - 'skill': ['', Validators.required], + qualificationForm = this.formBuilder.group({ + skill: ['', Validators.required], + }); + + isFieldInvalid(fieldName: string): boolean { + const field = this.qualificationForm.get(fieldName); + + if (!field) { + throw new Error('Form field does not exist: ' + fieldName); + } + + return field.invalid && (field.dirty || field.touched); + } + + getErrorMessage(fieldName: string): string { + const field = this.qualificationForm.get(fieldName); + + if (field?.errors?.['required']) { + return 'This field is required'; + } + + return ''; + } + + create() { + if (!this.qualificationForm.valid) { + console.error('Validation failed'); + return; + } + + const qualification = { + skill: this.qualificationForm.value.skill || '', + }; + + this.qualificationService.create(qualification).subscribe({ + next: (createdQualification) => { + this.dialogRef.close(createdQualification); + }, + error: (error) => { + console.error('Error creating qualification:', error); + this.apiErrorMessage = 'API Error'; + }, }); - - isFieldInvalid(fieldName: string): boolean { - const field = this.qualificationForm.get(fieldName); - - if (!field) { - throw new Error('Form field does not exist: ' + fieldName) - } - - return field.invalid && (field.dirty || field.touched); - } - - getErrorMessage(fieldName: string): string { - const field = this.qualificationForm.get(fieldName); - - if (field?.errors?.['required']) { - return 'This field is required'; - } - - return ''; - } - - create() { - if (!this.qualificationForm.valid) { - console.error('Validation failed'); - return; - } - - this.qualificationService.create(this.qualificationForm.value).subscribe({ - next: (createdQualification) => { - this.dialogRef.close(createdQualification); - }, - error: (error) => { - console.error('Error creating qualification:', error); - - this.apiErrorMessage = 'API Error'; - } - }); - } + } } diff --git a/src/app/qualification/delete/delete.component.html b/src/app/qualification/delete/delete.component.html index 16e8f63..fa7acfe 100644 --- a/src/app/qualification/delete/delete.component.html +++ b/src/app/qualification/delete/delete.component.html @@ -1,41 +1,62 @@ -

Delete Qualification

+

+ Delete Qualification +

@if (apiError) {
- error + error
-

There was an error deleting the qualification.

+

+ There was an error deleting the qualification. +

{{ apiError }}

} - +
- warning + warning
-

Are you sure you want to delete this qualification?

-

This action cannot be undone.

+

+ Are you sure you want to delete this qualification? +

+

+ This action cannot be undone. +

- - - - diff --git a/src/app/qualification/delete/delete.component.ts b/src/app/qualification/delete/delete.component.ts index eb16805..bbf468f 100644 --- a/src/app/qualification/delete/delete.component.ts +++ b/src/app/qualification/delete/delete.component.ts @@ -1,17 +1,16 @@ -import {Component, inject} from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogRef, - MatDialogTitle -} from "@angular/material/dialog"; -import {FormsModule, ReactiveFormsModule} from "@angular/forms"; -import QualificationService from "../../services/qualification.service"; -import {MatButton} from "@angular/material/button"; -import {HttpErrorResponse} from "@angular/common/http"; -import { MatError } from '@angular/material/form-field' -import {MatIcon} from "@angular/material/icon"; + MatDialogTitle, +} from '@angular/material/dialog'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import QualificationService from '../../services/qualification.service'; +import { MatButton } from '@angular/material/button'; +import { HttpErrorResponse } from '@angular/common/http'; +import { MatIcon } from '@angular/material/icon'; @Component({ selector: 'app-delete-qualification', @@ -22,17 +21,18 @@ import {MatIcon} from "@angular/material/icon"; ReactiveFormsModule, MatDialogActions, MatButton, - MatIcon + MatIcon, ], templateUrl: './delete.component.html', standalone: true, - styleUrl: './delete.component.css' + styleUrl: './delete.component.css', }) export class DeleteComponent { public id: number = inject(MAT_DIALOG_DATA); public apiError: string | null = null; - private qualificationService: QualificationService = inject(QualificationService); + private qualificationService: QualificationService = + inject(QualificationService); private dialogRef: MatDialogRef = inject(MatDialogRef); delete() { @@ -45,11 +45,12 @@ export class DeleteComponent { if (error.error.message.includes('SQL')) { // The API message is undescriptive but this is the most common - this.apiError = 'This qualification cannot be deleted because it is currently assigned to one or more employees'; + this.apiError = + 'This qualification cannot be deleted because it is currently assigned to one or more employees'; } else { this.apiError = 'API Error'; } - } + }, }); } diff --git a/src/app/qualification/details/details.component.html b/src/app/qualification/details/details.component.html index d44a958..34e4c34 100644 --- a/src/app/qualification/details/details.component.html +++ b/src/app/qualification/details/details.component.html @@ -1,4 +1,7 @@ -

+

{{ qualification.skill }} Developers

@@ -7,25 +10,37 @@ @if (employees$ | async; as employees) { @if (employees.length === 0) {
- person_off -

No employees found with this qualification.

+ person_off +

+ No employees found with this qualification. +

} @else { } @@ -34,9 +49,11 @@ - diff --git a/src/app/qualification/details/details.component.ts b/src/app/qualification/details/details.component.ts index bacae6b..e10c299 100644 --- a/src/app/qualification/details/details.component.ts +++ b/src/app/qualification/details/details.component.ts @@ -1,19 +1,19 @@ -import {Component, inject} from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogActions, MatDialogContent, MatDialogRef, - MatDialogTitle -} from "@angular/material/dialog"; -import QualificationService from "../../services/qualification.service"; -import {Qualification} from "../Qualification"; -import {AsyncPipe} from "@angular/common"; -import {MatButton} from "@angular/material/button"; -import {DetailsComponent as EmployeeDetailsComponent} from "../../employee/details/details.component"; -import EmployeeApiService from "../../services/employee-api.service"; -import {MatIcon} from "@angular/material/icon"; + MatDialogTitle, +} from '@angular/material/dialog'; +import QualificationService from '../../services/qualification.service'; +import { Qualification } from '../Qualification'; +import { AsyncPipe } from '@angular/common'; +import { MatButton } from '@angular/material/button'; +import { DetailsComponent as EmployeeDetailsComponent } from '../../employee/details/details.component'; +import EmployeeApiService from '../../services/employee-api.service'; +import { MatIcon } from '@angular/material/icon'; @Component({ selector: 'app-details', @@ -23,10 +23,10 @@ import {MatIcon} from "@angular/material/icon"; MatDialogTitle, MatDialogActions, MatButton, - MatIcon + MatIcon, ], templateUrl: './details.component.html', - styleUrl: './details.component.css' + styleUrl: './details.component.css', }) export class DetailsComponent { private qualificationService = inject(QualificationService); @@ -35,7 +35,9 @@ export class DetailsComponent { private dialog: MatDialog = inject(MatDialog); public qualification: Qualification = inject(MAT_DIALOG_DATA); - public employees$ = this.qualificationService.findEmployees(this.qualification.id); + public employees$ = this.qualificationService.findEmployees( + this.qualification.id, + ); closeModal() { this.dialogRef.close(); @@ -43,12 +45,12 @@ export class DetailsComponent { openEmployeeDetailsModal(id: number | undefined) { if (!id) { - throw new Error("ID must not be undefined"); + throw new Error('ID must not be undefined'); } - this.employeeService.getById(id).subscribe(employee => { + this.employeeService.getById(id).subscribe((employee) => { this.dialog.open(EmployeeDetailsComponent, { - data: employee + data: employee, }); }); } diff --git a/src/app/qualification/edit/edit.component.html b/src/app/qualification/edit/edit.component.html index 7786908..0342f5c 100644 --- a/src/app/qualification/edit/edit.component.html +++ b/src/app/qualification/edit/edit.component.html @@ -1,15 +1,30 @@ -

Edit Qualification

+

+ Edit Qualification +

- +
@if (apiErrorMessage) {
- error + error
-

There was an error editing the qualification.

-

{{ apiErrorMessage }}

+

+ There was an error editing the qualification. +

+

+ {{ apiErrorMessage }} +

@@ -18,27 +33,36 @@
Skill - + required + /> Enter the skill name - {{ getErrorMessage('skill') }} + {{ getErrorMessage("skill") }}
- - - diff --git a/src/app/qualification/edit/edit.component.ts b/src/app/qualification/edit/edit.component.ts index 975824f..387eaa6 100644 --- a/src/app/qualification/edit/edit.component.ts +++ b/src/app/qualification/edit/edit.component.ts @@ -1,58 +1,70 @@ -import {Component, inject} from '@angular/core'; -import {FormBuilder, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; -import QualificationService from "../../services/qualification.service"; +import { Component, inject } from '@angular/core'; import { - MAT_DIALOG_DATA, - MatDialogActions, MatDialogClose, - MatDialogContent, - MatDialogRef, - MatDialogTitle -} from "@angular/material/dialog"; -import {MatButton} from "@angular/material/button"; -import {MatError, MatFormField, MatHint, MatLabel} from "@angular/material/form-field"; -import {MatInput} from "@angular/material/input"; -import {NgIf} from "@angular/common"; -import {Qualification} from "../Qualification"; -import {MatIcon} from "@angular/material/icon"; + FormBuilder, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import QualificationService from '../../services/qualification.service'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { MatButton } from '@angular/material/button'; +import { + MatError, + MatFormField, + MatHint, + MatLabel, +} from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { NgIf } from '@angular/common'; +import { Qualification } from '../Qualification'; +import { MatIcon } from '@angular/material/icon'; @Component({ selector: 'app-edit-qualification', - imports: [ - FormsModule, - MatButton, - MatDialogActions, - MatDialogContent, - MatDialogTitle, - MatError, - MatFormField, - MatInput, - MatLabel, - NgIf, - ReactiveFormsModule, - MatDialogClose, - MatHint, - MatIcon - ], + imports: [ + FormsModule, + MatButton, + MatDialogActions, + MatDialogContent, + MatDialogTitle, + MatError, + MatFormField, + MatInput, + MatLabel, + NgIf, + ReactiveFormsModule, + MatDialogClose, + MatHint, + MatIcon, + ], templateUrl: './edit.component.html', - styleUrl: './edit.component.css' + styleUrl: './edit.component.css', }) export class EditComponent { - public apiErrorMessage: string = ''; + public apiErrorMessage = ''; public qualification: Qualification = inject(MAT_DIALOG_DATA); private formBuilder: FormBuilder = inject(FormBuilder); - private qualificationService: QualificationService = inject(QualificationService); + private qualificationService: QualificationService = + inject(QualificationService); private dialogRef: MatDialogRef = inject(MatDialogRef); qualificationForm = this.formBuilder.group({ - 'skill': [this.qualification.skill, Validators.required], + skill: [this.qualification.skill, Validators.required], }); isFieldInvalid(fieldName: string): boolean { const field = this.qualificationForm.get(fieldName); if (!field) { - throw new Error('Form field does not exist: ' + fieldName) + throw new Error('Form field does not exist: ' + fieldName); } return field.invalid && (field.dirty || field.touched); @@ -74,15 +86,20 @@ export class EditComponent { return; } - this.qualificationService.edit(this.qualification.id, this.qualificationForm.value).subscribe({ - next: (editedQualification) => { - this.dialogRef.close(editedQualification); - }, - error: (error) => { - console.error('Error creating qualification:', error); + const qualification = { + skill: this.qualificationForm.value.skill || '', + }; - this.apiErrorMessage = 'API Error'; - } - }); + this.qualificationService + .update(this.qualification.id, qualification) + .subscribe({ + next: (editedQualification) => { + this.dialogRef.close(editedQualification); + }, + error: (error) => { + console.error('Error updating qualification:', error); + this.apiErrorMessage = 'API Error'; + }, + }); } } diff --git a/src/app/qualification/table/table.component.html b/src/app/qualification/table/table.component.html index f45ab0a..fb56cb9 100644 --- a/src/app/qualification/table/table.component.html +++ b/src/app/qualification/table/table.component.html @@ -1,118 +1,170 @@
- @defer { - @if (qualifications$ | async; as qualifications) { -
-
-
-

Qualifications

- - search - -
- -
-
-
- -
- - @if (qualifications) { -
- - - - - - - - - - - - - -
Skill -
-
- - {{ qualification.skill[0]?.toUpperCase() }} - -
- -
-
Actions -
- - -
-
-
- } @else { - - - school -

No qualifications found

-
-
- } -
- } - } @placeholder { -
-
-
-
-
-
-
-
-
- @for (i of [1, 2, 3]; track i) { -
- } -
-
-
-
- } @error { -
-
- error -
-

There was an error loading the qualifications.

-

Please try refreshing the page.

+ @defer { + @if (qualifications$ | async; as qualifications) { +
+
+
+

+ Qualifications +

+ + search + +
+
-
+
- } @loading { -
- +
+ + @if (qualifications) { +
+ + + + + + + + + + + + + +
+ Skill + +
+
+ + {{ qualification.skill[0]?.toUpperCase() }} + +
+
+ +
+
+
+ Actions + +
+ + +
+
+
+ } @else { + + + school +

No qualifications found

+
+
+ } +
} + } @placeholder { +
+
+
+
+
+
+
+
+
+ @for (i of [1, 2, 3]; track i) { +
+ } +
+
+
+
+ } @error { +
+
+ error +
+

+ There was an error loading the qualifications. +

+

+ Please try refreshing the page. +

+
+
+
+ } @loading { +
+ +
+ }
diff --git a/src/app/qualification/table/table.component.ts b/src/app/qualification/table/table.component.ts index 4c68005..eccc63e 100644 --- a/src/app/qualification/table/table.component.ts +++ b/src/app/qualification/table/table.component.ts @@ -1,143 +1,155 @@ -import {Component, inject, OnInit} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {catchError, debounceTime, distinctUntilChanged, Observable, of, retry, Subject} from 'rxjs'; -import {HttpErrorResponse} from '@angular/common/http'; -import {Qualification} from "../Qualification"; -import {MatDialog} from "@angular/material/dialog"; -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 {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 {MatSnackBarModule} from '@angular/material/snack-bar'; -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"; -import {DetailsComponent} from "../details/details.component"; -import {ErrorHandlerService} from "../../services/error.handler.service"; +import { Component, inject, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + catchError, + debounceTime, + distinctUntilChanged, + Observable, + of, + retry, + Subject, +} from 'rxjs'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Qualification } from '../Qualification'; +import { MatDialog } from '@angular/material/dialog'; +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 { 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 { MatSnackBarModule } from '@angular/material/snack-bar'; +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'; +import { DetailsComponent } from '../details/details.component'; +import { ErrorHandlerService } from '../../services/error.handler.service'; @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' + 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 { - private readonly qualificationService: QualificationService = inject(QualificationService); - private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService); - private readonly dialog: MatDialog = inject(MatDialog); + private readonly qualificationService: QualificationService = + inject(QualificationService); + private readonly errorHandlerService: ErrorHandlerService = + inject(ErrorHandlerService); + private readonly dialog: MatDialog = inject(MatDialog); - private static readonly MAX_RETRIES = 3; + private static readonly MAX_RETRIES = 3; - private allQualifications: Qualification[] = []; - private searchSubject = new Subject(); - public qualifications$: Observable = of([]); - public isSearching = false; - public readonly displayedColumns: string[] = ['skill', 'actions']; + private allQualifications: Qualification[] = []; + private searchSubject = new Subject(); + public qualifications$: Observable = of([]); + public isSearching = false; + public readonly displayedColumns: string[] = ['skill', 'actions']; - ngOnInit() { - this.loadQualifications(); - this.setupSearch(); - } + ngOnInit() { + this.loadQualifications(); + this.setupSearch(); + } - private loadQualifications(): void { - this.fetchQualifications().subscribe(qualifications => { - this.allQualifications = qualifications; - this.qualifications$ = of(qualifications); - }); - } + private loadQualifications(): void { + this.fetchQualifications().subscribe((qualifications) => { + this.allQualifications = qualifications; + this.qualifications$ = of(qualifications); + }); + } - private setupSearch(): void { - this.searchSubject.pipe( - debounceTime(300), - distinctUntilChanged() - ).subscribe(searchTerm => { - this.isSearching = true; - setTimeout(() => { - const filteredQualifications = this.allQualifications.filter(qualification => - qualification.skill?.toLowerCase().includes(searchTerm) - ); - this.qualifications$ = of(filteredQualifications); - this.isSearching = false; - }, 150); - }); - } + private setupSearch(): void { + this.searchSubject + .pipe(debounceTime(300), distinctUntilChanged()) + .subscribe((searchTerm) => { + this.isSearching = true; + setTimeout(() => { + const filteredQualifications = this.allQualifications.filter( + (qualification) => + qualification.skill?.toLowerCase().includes(searchTerm), + ); + this.qualifications$ = of(filteredQualifications); + this.isSearching = false; + }, 150); + }); + } - private fetchQualifications(): Observable { - return this.qualificationService.getAll().pipe( - retry(QualificationsComponent.MAX_RETRIES), - catchError((error: HttpErrorResponse) => { - console.error('Error fetching qualifications:', error); - this.errorHandlerService.showErrorMessage('Failed to load qualifications. Please try again.'); - return of([]); - }) + private fetchQualifications(): Observable { + return this.qualificationService.getAll().pipe( + retry(QualificationsComponent.MAX_RETRIES), + catchError((error: HttpErrorResponse) => { + console.error('Error fetching qualifications:', error); + this.errorHandlerService.showErrorMessage( + 'Failed to load qualifications. Please try again.', ); - } + return of([]); + }), + ); + } - protected filterQualifications(event: Event): void { - const searchTerm = (event.target as HTMLInputElement).value.toLowerCase(); - this.searchSubject.next(searchTerm); - } + protected filterQualifications(event: Event): void { + const searchTerm = (event.target as HTMLInputElement).value.toLowerCase(); + this.searchSubject.next(searchTerm); + } - openCreateModal() { - const dialogRef = this.dialog.open(CreateComponent); + openCreateModal() { + const dialogRef = this.dialog.open(CreateComponent); - dialogRef.afterClosed().subscribe((success: boolean) => { - if (success) { - this.loadQualifications(); - } - }); - } + dialogRef.afterClosed().subscribe((success: boolean) => { + if (success) { + this.loadQualifications(); + } + }); + } - openEditModal(qualification: Qualification) { - const dialogRef = this.dialog.open(EditComponent, { - data: qualification - }); + openEditModal(qualification: Qualification) { + const dialogRef = this.dialog.open(EditComponent, { + data: qualification, + }); - dialogRef.afterClosed().subscribe((success: boolean) => { - if (success) { - this.loadQualifications(); - } - }); - } + dialogRef.afterClosed().subscribe((success: boolean) => { + if (success) { + this.loadQualifications(); + } + }); + } - openDeleteModal(id: number) { - const dialogRef = this.dialog.open(DeleteComponent, { - data: id - }); + openDeleteModal(id: number) { + const dialogRef = this.dialog.open(DeleteComponent, { + data: id, + }); - dialogRef.afterClosed().subscribe((success: boolean) => { - if (success) { - this.loadQualifications(); - } - }); - } + dialogRef.afterClosed().subscribe((success: boolean) => { + if (success) { + this.loadQualifications(); + } + }); + } - openDetailsModal(qualification: Qualification) { - this.dialog.open(DetailsComponent, { - data: qualification - }); - } + openDetailsModal(qualification: Qualification) { + this.dialog.open(DetailsComponent, { + data: qualification, + }); + } } diff --git a/src/app/services/auth-guard.service.ts b/src/app/services/auth-guard.service.ts index eded0c4..1e0ddc0 100644 --- a/src/app/services/auth-guard.service.ts +++ b/src/app/services/auth-guard.service.ts @@ -1,14 +1,15 @@ -import {Injectable} from '@angular/core'; -import {AuthService} from "./auth.service"; -import {Router} from "@angular/router"; -import {KeycloakService} from "keycloak-angular"; +import { Injectable } from '@angular/core'; +import { AuthService } from './auth.service'; +import { Router } from '@angular/router'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AuthGuardService { - constructor(public auth: AuthService, public router: Router) { - } + constructor( + public auth: AuthService, + public router: Router, + ) {} canActivate(): boolean { if (!this.auth.isAuthenticated()) { diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 2a56cef..d6fa206 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,8 +1,8 @@ -import {inject, Injectable} from '@angular/core'; -import {KeycloakService} from "keycloak-angular"; +import { inject, Injectable } from '@angular/core'; +import { KeycloakService } from 'keycloak-angular'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AuthService { private keycloakService = inject(KeycloakService); diff --git a/src/app/services/employee-api.service.ts b/src/app/services/employee-api.service.ts index 993f05f..c60be55 100644 --- a/src/app/services/employee-api.service.ts +++ b/src/app/services/employee-api.service.ts @@ -1,11 +1,10 @@ -import {inject, Injectable} from "@angular/core"; -import {HttpClient} from "@angular/common/http"; -import {Observable} from "rxjs"; -import {Employee} from "../employee/Employee"; - +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Employee } from '../employee/Employee'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export default class EmployeeApiService { private http: HttpClient = inject(HttpClient); @@ -13,22 +12,30 @@ export default class EmployeeApiService { private static readonly BASE_URL = '/backend'; public getById(id: number): Observable { - return this.http.get(`${EmployeeApiService.BASE_URL}/employees/${id}`) + return this.http.get(`${EmployeeApiService.BASE_URL}/employees/${id}`); } public deleteById(id: number): Observable { - return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`) + return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`); } public getAll(): Observable { - return this.http.get(`${EmployeeApiService.BASE_URL}/employees`) + return this.http.get( + `${EmployeeApiService.BASE_URL}/employees`, + ); } public create(employee: Employee) { - return this.http.post(`${EmployeeApiService.BASE_URL}/employees`, employee) + return this.http.post( + `${EmployeeApiService.BASE_URL}/employees`, + employee, + ); } public update(employee: Employee, id: number) { - return this.http.put(`${EmployeeApiService.BASE_URL}/employees/${id}`, employee) + return this.http.put( + `${EmployeeApiService.BASE_URL}/employees/${id}`, + employee, + ); } } diff --git a/src/app/services/error.handler.service.ts b/src/app/services/error.handler.service.ts index eb362de..b3eb1c7 100644 --- a/src/app/services/error.handler.service.ts +++ b/src/app/services/error.handler.service.ts @@ -1,8 +1,8 @@ -import {inject, Injectable} from '@angular/core'; -import {MatSnackBar} from "@angular/material/snack-bar"; +import { inject, Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ErrorHandlerService { private readonly snackBar: MatSnackBar = inject(MatSnackBar); @@ -12,7 +12,7 @@ export class ErrorHandlerService { duration: 5000, horizontalPosition: 'end', verticalPosition: 'bottom', - panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100'] + panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100'], }); } } diff --git a/src/app/services/qualification.service.ts b/src/app/services/qualification.service.ts index 711f52c..b280219 100644 --- a/src/app/services/qualification.service.ts +++ b/src/app/services/qualification.service.ts @@ -1,40 +1,53 @@ -import {inject, Injectable} from "@angular/core"; -import {HttpClient} from "@angular/common/http"; -import {map, Observable} from "rxjs"; -import {Qualification} from "../qualification/Qualification"; -import {Employee} from "../employee/Employee"; - +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Qualification } from '../qualification/Qualification'; +import { Employee } from '../employee/Employee'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export default class QualificationService { - private http: HttpClient = inject(HttpClient); - private static readonly BASE_URL = '/backend'; - public getAll(): Observable { - return this.http.get(`${QualificationService.BASE_URL}/qualifications`).pipe( - map(qualifications => qualifications.sort((a, b) => a.id - b.id)) - ) - } + private readonly apiUrl = `${QualificationService.BASE_URL}/qualifications`; - public create(data: any) { - return this.http.post(`${QualificationService.BASE_URL}/qualifications`, data) - } + constructor(private http: HttpClient) {} - public edit(id: number, data: any) { - return this.http.put(`${QualificationService.BASE_URL}/qualifications/${id}`, data) - } - - public delete(id: number) { - return this.http.delete(`${QualificationService.BASE_URL}/qualifications/${id}`) - } - - public findEmployees(id: number): Observable { - return this.http.get(`${QualificationService.BASE_URL}/qualifications/${id}/employees`) + getAll(): Observable { + return this.http + .get(this.apiUrl) .pipe( - map(response => response.employees) + map((qualifications) => qualifications.sort((a, b) => a.id - b.id)), ); } + + getById(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`); + } + + create(qualification: Omit): Observable { + return this.http.post(this.apiUrl, qualification); + } + + update( + id: number, + qualification: Partial, + ): Observable { + return this.http.put(`${this.apiUrl}/${id}`, qualification); + } + + delete(id: number): Observable { + return this.http.delete(`${this.apiUrl}/${id}`); + } + + findEmployees(id: number): Observable { + interface EmployeeResponse { + employees: Employee[]; + } + return this.http + .get(`${this.apiUrl}/${id}/employees`) + .pipe(map((response) => response.employees)); + } } diff --git a/src/index.html b/src/index.html index 89ee835..db6e104 100644 --- a/src/index.html +++ b/src/index.html @@ -1,15 +1,21 @@ - - - Lf10StarterNew - - - - - - - - - + + + Lf10StarterNew + + + + + + + + + diff --git a/src/main.ts b/src/main.ts index 35b00f3..8882c45 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,5 +2,6 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; -bootstrapApplication(AppComponent, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err), +); diff --git a/src/styles.css b/src/styles.css index 6df3509..105bc67 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2,5 +2,11 @@ @tailwind components; @tailwind utilities; -html, body { height: 100%; } -body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } +html, +body { + height: 100%; +} +body { + margin: 0; + font-family: Roboto, "Helvetica Neue", sans-serif; +} diff --git a/tailwind.config.js b/tailwind.config.js index 03231e9..de5d7d0 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,11 +1,8 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ - './src/**/*.{html,ts,css,scss,sass,less,styl}', - ], + content: ["./src/**/*.{html,ts,css,scss,sass,less,styl}"], theme: { extend: {}, }, plugins: [], -} - +}; diff --git a/tsconfig.app.json b/tsconfig.app.json index 3775b37..8886e90 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -6,10 +6,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] } diff --git a/tsconfig.json b/tsconfig.json index a8bb65b..25afa53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,10 +19,7 @@ "importHelpers": true, "target": "ES2022", "module": "ES2022", - "lib": [ - "ES2022", - "dom" - ] + "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 5fb748d..e00e30e 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -4,12 +4,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["jasmine"] }, - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] }