format code and improve readability across files #47

Merged
jleibl merged 1 commits from task/fix-linter-errors into main 2025-01-23 11:24:39 +00:00
48 changed files with 1300 additions and 952 deletions

View File

@ -1,11 +1,9 @@
# Starter für das LF10 Projekt # Starter für das LF10 Projekt
## Requirements ## Requirements
* Docker https://docs.docker.com/get-docker/ - Docker https://docs.docker.com/get-docker/
* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/ - Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
### Abhängigkeiten starten (Postgres, EmployeeBackend) ### Abhängigkeiten starten (Postgres, EmployeeBackend)
@ -37,7 +35,7 @@ http://localhost:8089/swagger
# Postgres # Postgres
``` ````
### Intellij-Ansicht für Postgres Datenbank einrichten (geht nicht in Webstorm!) ### Intellij-Ansicht für Postgres Datenbank einrichten (geht nicht in Webstorm!)
@ -51,7 +49,7 @@ http://localhost:8089/swagger
7. Username lf10_starter und Passwort secret eintragen (siehe application.properties), mit Apply 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 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 # 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. 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 ```typescript
import {APP_INITIALIZER, ApplicationConfig} from '@angular/core'; import { APP_INITIALIZER, ApplicationConfig } from "@angular/core";
import { provideRouter } from '@angular/router'; import { provideRouter } from "@angular/router";
import { routes } from './app.routes'; import { routes } from "./app.routes";
import { KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService } from "keycloak-angular"; import { KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService } from "keycloak-angular";
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http";
export const initializeKeycloak = (keycloak: KeycloakService) => async () => export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
keycloak.init({ keycloak.init({
config: { config: {
url: 'KEYCLOAK_URL', url: "KEYCLOAK_URL",
realm: 'REALM', realm: "REALM",
clientId: 'CLIENT_ID', clientId: "CLIENT_ID",
}, },
loadUserProfileAtStartUp: true, loadUserProfileAtStartUp: true,
initOptions: { initOptions: {
onLoad: 'check-sso', onLoad: "check-sso",
silentCheckSsoRedirectUri: silentCheckSsoRedirectUri: window.location.origin + "/silent-check-sso.html",
window.location.origin + '/silent-check-sso.html',
checkLoginIframe: false, checkLoginIframe: false,
redirectUri: 'http://localhost:4200', redirectUri: "http://localhost:4200",
}, },
}); });
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> { function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
return () => initializeKeycloak(keycloak)(); return () => initializeKeycloak(keycloak)();
} }
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), providers: [
provideRouter(routes),
KeycloakAngularModule, KeycloakAngularModule,
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initializeApp, useFactory: initializeApp,
multi: true, multi: true,
deps: [KeycloakService] deps: [KeycloakService],
}, },
KeycloakService, KeycloakService,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor, useClass: KeycloakBearerInterceptor,
multi: true 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. 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: Des Weiteren ist der Client mit der Bezeichnung employee-management-service-frontend wie folgt konfiguriert:
@ -141,4 +136,3 @@ Des Weiteren ist der Client mit der Bezeichnung employee-management-service-fron
# Bugs # 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.

View File

@ -16,9 +16,7 @@
"outputPath": "dist/lf10-starter2024", "outputPath": "dist/lf10-starter2024",
"index": "src/index.html", "index": "src/index.html",
"browser": "src/main.ts", "browser": "src/main.ts",
"polyfills": [ "polyfills": ["zone.js"],
"zone.js"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"assets": [ "assets": [
{ {
@ -74,10 +72,7 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"polyfills": [ "polyfills": ["zone.js", "zone.js/testing"],
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"assets": [ "assets": [
{ {
@ -95,10 +90,7 @@
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": [ "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
"src/**/*.ts",
"src/**/*.html"
]
} }
} }
} }
@ -106,8 +98,6 @@
}, },
"cli": { "cli": {
"analytics": "33c8483f-3876-4eb5-9c9b-1001cab9b273", "analytics": "33c8483f-3876-4eb5-9c9b-1001cab9b273",
"schematicCollections": [ "schematicCollections": ["angular-eslint"]
"angular-eslint"
]
} }
} }

View File

@ -1,4 +1,3 @@
volumes: volumes:
employee_postgres_data: employee_postgres_data:
driver: local driver: local

View File

@ -39,5 +39,5 @@ module.exports = tseslint.config(
...angular.configs.templateAccessibility, ...angular.configs.templateAccessibility,
], ],
rules: {}, rules: {},
} },
); );

View File

@ -2,4 +2,3 @@
<h1 class="text-3xl font-bold text-gray-900 mb-8">{{ title }}</h1> <h1 class="text-3xl font-bold text-gray-900 mb-8">{{ title }}</h1>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>

View File

@ -7,7 +7,7 @@ import {RouterOutlet} from '@angular/router';
imports: [CommonModule, RouterOutlet], imports: [CommonModule, RouterOutlet],
templateUrl: './app.component.html', templateUrl: './app.component.html',
standalone: true, standalone: true,
styleUrl: './app.component.css' styleUrl: './app.component.css',
}) })
export class AppComponent { export class AppComponent {
title = 'Employee Management System'; title = 'Employee Management System';

View File

@ -1,8 +1,16 @@
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core'; import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import {KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular"; import {
KeycloakAngularModule,
KeycloakBearerInterceptor,
KeycloakService,
} from 'keycloak-angular';
import { routes } from './app.routes'; import { routes } from './app.routes';
import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http"; import {
HTTP_INTERCEPTORS,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const initializeKeycloak = (keycloak: KeycloakService) => async () => export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
@ -22,7 +30,6 @@ export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
}, },
}); });
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> { function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
return () => initializeKeycloak(keycloak)(); return () => initializeKeycloak(keycloak)();
} }
@ -37,14 +44,14 @@ export const appConfig: ApplicationConfig = {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initializeApp, useFactory: initializeApp,
multi: true, multi: true,
deps: [KeycloakService] deps: [KeycloakService],
}, },
KeycloakService, KeycloakService,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor, useClass: KeycloakBearerInterceptor,
multi: true multi: true,
} },
] ],
}; };

View File

@ -1,10 +1,10 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import {LoginComponent} from "./login/login.component"; import { LoginComponent } from './login/login.component';
import {AuthGuardService} from "./services/auth-guard.service"; import { AuthGuardService } from './services/auth-guard.service';
import {HomeComponent} from "./home/home.component"; import { HomeComponent } from './home/home.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },
{ path: '', component: HomeComponent, canActivate: [AuthGuardService] }, { path: '', component: HomeComponent, canActivate: [AuthGuardService] },
{path: '**', redirectTo: ''} { path: '**', redirectTo: '' },
]; ];

View File

@ -1,4 +1,4 @@
import {Qualification} from "../qualification/Qualification"; import { Qualification } from '../qualification/Qualification';
export class Employee { export class Employee {
constructor( constructor(
@ -9,7 +9,6 @@ export class Employee {
public postcode?: string, public postcode?: string,
public city?: string, public city?: string,
public phone?: string, public phone?: string,
public skillSet?: Qualification[] public skillSet?: Qualification[],
) { ) {}
}
} }

View File

@ -5,74 +5,98 @@
<div class="flex gap-x-4"> <div class="flex gap-x-4">
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>First Name</mat-label> <mat-label>First Name</mat-label>
<input matInput formControlName="firstName" required> <input matInput formControlName="firstName" required />
<mat-hint>Enter the first name</mat-hint> <mat-hint>Enter the first name</mat-hint>
<mat-error *ngIf="errors['firstName']">{{errors['firstName']}}</mat-error> <mat-error *ngIf="errors['firstName']">{{
errors["firstName"]
}}</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Last Name</mat-label> <mat-label>Last Name</mat-label>
<input matInput formControlName="lastName" required> <input matInput formControlName="lastName" required />
<mat-hint>Enter the last name</mat-hint> <mat-hint>Enter the last name</mat-hint>
<mat-error *ngIf="errors['lastName']">{{errors['lastName']}}</mat-error> <mat-error *ngIf="errors['lastName']">{{
errors["lastName"]
}}</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Street</mat-label> <mat-label>Street</mat-label>
<input matInput formControlName="street" required> <input matInput formControlName="street" required />
<mat-hint>Enter the street address</mat-hint> <mat-hint>Enter the street address</mat-hint>
<mat-error *ngIf="errors['street']">{{errors['street']}}</mat-error> <mat-error *ngIf="errors['street']">{{ errors["street"] }}</mat-error>
</mat-form-field> </mat-form-field>
<div class="flex gap-x-4"> <div class="flex gap-x-4">
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>City</mat-label> <mat-label>City</mat-label>
<input matInput formControlName="city" required> <input matInput formControlName="city" required />
<mat-hint>Enter the city</mat-hint> <mat-hint>Enter the city</mat-hint>
<mat-error *ngIf="errors['city']">{{errors['city']}}</mat-error> <mat-error *ngIf="errors['city']">{{ errors["city"] }}</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="!w-1/2"> <mat-form-field class="!w-1/2">
<mat-label>Postcode</mat-label> <mat-label>Postcode</mat-label>
<input matInput formControlName="postcode" minlength="5" maxlength="5" type="number" required> <input
matInput
formControlName="postcode"
minlength="5"
maxlength="5"
type="number"
required
/>
<mat-hint>Enter postcode</mat-hint> <mat-hint>Enter postcode</mat-hint>
<mat-error *ngIf="errors['postcode']">{{errors['postcode']}}</mat-error> <mat-error *ngIf="errors['postcode']">{{
errors["postcode"]
}}</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Phone</mat-label> <mat-label>Phone</mat-label>
<input matInput formControlName="phone" required> <input matInput formControlName="phone" required />
<mat-hint>Enter the phone number</mat-hint> <mat-hint>Enter the phone number</mat-hint>
<mat-error *ngIf="errors['phone']">{{errors['phone']}}</mat-error> <mat-error *ngIf="errors['phone']">{{ errors["phone"] }}</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Qualifications</mat-label> <mat-label>Qualifications</mat-label>
<mat-hint>Select the qualifications</mat-hint> <mat-hint>Select the qualifications</mat-hint>
<mat-select formControlName="qualifications" multiple> <mat-select formControlName="qualifications" multiple>
<mat-option *ngFor="let qualification of qualifications" [value]="qualification.id"> <mat-option
*ngFor="let qualification of qualifications"
[value]="qualification.id"
>
{{ qualification.skill }} {{ qualification.skill }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="errors['qualifications']">{{errors['qualifications']}}</mat-error> <mat-error *ngIf="errors['qualifications']">{{
errors["qualifications"]
}}</mat-error>
</mat-form-field> </mat-form-field>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"> <mat-dialog-actions
<button mat-button align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
mat-dialog-close mat-dialog-close
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"
>
Cancel Cancel
</button> </button>
<button mat-flat-button <button
mat-flat-button
color="primary" color="primary"
type="submit" type="submit"
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"> class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
>
Submit Submit
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>
</div> </div>
</form> </form>
</mat-dialog-content> </mat-dialog-content>

View File

@ -1,23 +1,29 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import {AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; import {
import {MatFormField, MatHint, MatLabel} from "@angular/material/form-field"; AbstractControl,
import {MatError, MatInput} from "@angular/material/input"; FormBuilder,
import {MatButton} from "@angular/material/button"; 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 { import {
MatDialogActions, MatDialogActions,
MatDialogClose, MatDialogClose,
MatDialogContent, MatDialogContent,
MatDialogRef, MatDialogRef,
MatDialogTitle MatDialogTitle,
} from "@angular/material/dialog"; } from '@angular/material/dialog';
import {Employee} from "../Employee"; import { Employee } from '../Employee';
import EmployeeApiService from "../../services/employee-api.service"; import EmployeeApiService from '../../services/employee-api.service';
import {NgForOf, NgIf} from "@angular/common"; import { NgForOf, NgIf } from '@angular/common';
import {MatOption} from "@angular/material/core"; import { MatOption } from '@angular/material/core';
import {MatSelect} from "@angular/material/select"; import { MatSelect } from '@angular/material/select';
import QualificationService from "../../services/qualification.service"; import QualificationService from '../../services/qualification.service';
import {Qualification} from "../../qualification/Qualification"; import { Qualification } from '../../qualification/Qualification';
import {debounceTime} from "rxjs"; import { debounceTime } from 'rxjs';
@Component({ @Component({
selector: 'app-create-employee', selector: 'app-create-employee',
@ -36,11 +42,11 @@ import {debounceTime} from "rxjs";
MatSelect, MatSelect,
NgForOf, NgForOf,
MatError, MatError,
MatHint MatHint,
], ],
templateUrl: './create.component.html', templateUrl: './create.component.html',
standalone: true, standalone: true,
styleUrl: './create.component.css' styleUrl: './create.component.css',
}) })
export class CreateComponent implements OnInit { export class CreateComponent implements OnInit {
employeeForm!: FormGroup; employeeForm!: FormGroup;
@ -49,17 +55,17 @@ export class CreateComponent implements OnInit {
dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef); dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef);
qualificationService: QualificationService = inject(QualificationService); qualificationService: QualificationService = inject(QualificationService);
qualifications: Qualification[] = []; qualifications: Qualification[] = [];
errorMsgs: { [key: string]: string } = { errorMsgs: Record<string, string> = {
firstName: 'First name is required', firstName: 'First name is required',
lastName: 'Last name is required', lastName: 'Last name is required',
street: 'Street is required', street: 'Street is required',
postcode: 'Postcode must be 5 characters long', postcode: 'Postcode must be 5 characters long',
city: 'City is required', city: 'City is required',
phone: 'Phone is required', phone: 'Phone is required',
qualifications: 'Qualifications are required' qualifications: 'Qualifications are required',
} };
errors: { [key: string]: string } = {} errors: Record<string, string> = {};
ngOnInit(): void { ngOnInit(): void {
this.loadQualifications(); this.loadQualifications();
@ -70,7 +76,7 @@ export class CreateComponent implements OnInit {
postcode: ['', [Validators.required, this.validatePostcode]], postcode: ['', [Validators.required, this.validatePostcode]],
city: ['', Validators.required], city: ['', Validators.required],
phone: ['', Validators.required], phone: ['', Validators.required],
qualifications: [[]] qualifications: [[]],
}); });
Object.keys(this.employeeForm.controls).forEach((controlName: string) => { Object.keys(this.employeeForm.controls).forEach((controlName: string) => {
@ -82,9 +88,9 @@ export class CreateComponent implements OnInit {
} }
loadQualifications() { loadQualifications() {
this.qualificationService.getAll().subscribe( this.qualificationService
qualifications => this.qualifications = qualifications .getAll()
); .subscribe((qualifications) => (this.qualifications = qualifications));
} }
submit() { submit() {
@ -95,7 +101,7 @@ export class CreateComponent implements OnInit {
const formValue = this.employeeForm.value; const formValue = this.employeeForm.value;
const employeeData = { const employeeData = {
...formValue, ...formValue,
skillSet: formValue.qualifications skillSet: formValue.qualifications,
}; };
this.employeeService.create(employeeData as Employee).subscribe(); this.employeeService.create(employeeData as Employee).subscribe();

View File

@ -1,31 +1,48 @@
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Delete Employee</h2> <h2
mat-dialog-title
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
>
Delete Employee
</h2>
<mat-dialog-content class="!px-3 md:!px-6"> <mat-dialog-content class="!px-3 md:!px-6">
<div class="w-full min-w-[280px] md:min-w-[400px] space-y-4 md:space-y-6"> <div class="w-full min-w-[280px] md:min-w-[400px] space-y-4 md:space-y-6">
<div class="bg-amber-50 p-3 md:p-4 rounded-lg border border-amber-200"> <div class="bg-amber-50 p-3 md:p-4 rounded-lg border border-amber-200">
<div class="flex items-start space-x-2 md:space-x-3"> <div class="flex items-start space-x-2 md:space-x-3">
<mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8">warning</mat-icon> <mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8"
>warning</mat-icon
>
<div> <div>
<p class="text-gray-800 font-medium text-sm md:text-base"> <p class="text-gray-800 font-medium text-sm md:text-base">
Are you sure you want to delete {{employee.firstName}} {{employee.lastName}}? Are you sure you want to delete {{ employee.firstName }}
{{ employee.lastName }}?
</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">
This action cannot be undone.
</p> </p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">This action cannot be undone.</p>
</div> </div>
</div> </div>
</div> </div>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"> <mat-dialog-actions
<button mat-button align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
[mat-dialog-close]="false" [mat-dialog-close]="false"
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"
>
Cancel Cancel
</button> </button>
<button mat-flat-button <button
mat-flat-button
color="warn" color="warn"
(click)="deleteEmployee(employee.id ?? 0)" (click)="deleteEmployee(employee.id ?? 0)"
mat-dialog-close mat-dialog-close
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1" class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
cdkFocusInitial> cdkFocusInitial
>
Delete Delete
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@ -1,15 +1,16 @@
import {Component, Inject, inject} from '@angular/core'; import { Component, inject } from '@angular/core';
import {Employee} from "../Employee"; import { Employee } from '../Employee';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogActions, MatDialogActions,
MatDialogClose, MatDialogClose,
MatDialogContent, MatDialogRef, MatDialogContent,
MatDialogTitle MatDialogRef,
} from "@angular/material/dialog"; MatDialogTitle,
import {MatButton} from "@angular/material/button"; } from '@angular/material/dialog';
import {MatIcon} from "@angular/material/icon"; import { MatButton } from '@angular/material/button';
import EmployeeApiService from "../../services/employee-api.service"; import { MatIcon } from '@angular/material/icon';
import EmployeeApiService from '../../services/employee-api.service';
@Component({ @Component({
selector: 'app-delete-employee', selector: 'app-delete-employee',
@ -19,11 +20,11 @@ import EmployeeApiService from "../../services/employee-api.service";
MatDialogActions, MatDialogActions,
MatButton, MatButton,
MatDialogClose, MatDialogClose,
MatIcon MatIcon,
], ],
templateUrl: './delete.component.html', templateUrl: './delete.component.html',
standalone: true, standalone: true,
styleUrl: './delete.component.css' styleUrl: './delete.component.css',
}) })
export class DeleteComponent { export class DeleteComponent {
private apiService: EmployeeApiService = inject(EmployeeApiService); private apiService: EmployeeApiService = inject(EmployeeApiService);

View File

@ -1,21 +1,32 @@
<h2 mat-dialog-title class="text-2xl font-semibold text-gray-800 mb-4">Employee Details</h2> <h2 mat-dialog-title class="text-2xl font-semibold text-gray-800 mb-4">
Employee Details
</h2>
<mat-dialog-content class="!px-4 sm:!px-6"> <mat-dialog-content class="!px-4 sm:!px-6">
<div class="w-full min-w-[280px] sm:min-w-[500px] space-y-6 sm:space-y-8"> <div class="w-full min-w-[280px] sm:min-w-[500px] space-y-6 sm:space-y-8">
<div class="flex flex-col sm:flex-row items-center sm:items-start text-center sm:text-left space-y-4 sm:space-y-0 sm:space-x-4"> <div
<div class="h-20 w-20 sm:h-16 sm:w-16 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0"> class="flex flex-col sm:flex-row items-center sm:items-start text-center sm:text-left space-y-4 sm:space-y-0 sm:space-x-4"
>
<div
class="h-20 w-20 sm:h-16 sm:w-16 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0"
>
<span class="text-blue-600 text-2xl sm:text-xl font-medium"> <span class="text-blue-600 text-2xl sm:text-xl font-medium">
{{ employee.firstName?.charAt(0) ?? '' }}{{ employee.lastName?.charAt(0) ?? '' }} {{ employee.firstName?.charAt(0) ?? ""
}}{{ employee.lastName?.charAt(0) ?? "" }}
</span> </span>
</div> </div>
<div> <div>
<h3 class="text-xl font-medium text-gray-900">{{ employee.firstName }} {{ employee.lastName }}</h3> <h3 class="text-xl font-medium text-gray-900">
{{ employee.firstName }} {{ employee.lastName }}
</h3>
<p class="text-gray-500">ID: {{ employee.id }}</p> <p class="text-gray-500">ID: {{ employee.id }}</p>
</div> </div>
</div> </div>
<div class="space-y-3 sm:space-y-4"> <div class="space-y-3 sm:space-y-4">
<h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">Contact Information</h4> <h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">
Contact Information
</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1"> <div class="space-y-1">
<p class="text-sm text-gray-500">Phone</p> <p class="text-sm text-gray-500">Phone</p>
@ -45,10 +56,14 @@
</div> </div>
<div class="space-y-3 sm:space-y-4"> <div class="space-y-3 sm:space-y-4">
<h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">Qualifications</h4> <h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">
Qualifications
</h4>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@for (skill of employee.skillSet; track skill.id) { @for (skill of employee.skillSet; track skill.id) {
<div class="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm font-medium"> <div
class="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm font-medium"
>
{{ skill.skill }} {{ skill.skill }}
</div> </div>
} @empty { } @empty {
@ -60,9 +75,11 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end" class="!px-4 sm:!px-6 !py-4 !mt-4 border-t"> <mat-dialog-actions align="end" class="!px-4 sm:!px-6 !py-4 !mt-4 border-t">
<button mat-button <button
mat-button
(click)="closeModal()" (click)="closeModal()"
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full"
>
Close Close
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@ -1,20 +1,20 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogTitle} from "@angular/material/dialog"; import {
import {Employee} from "../Employee"; MAT_DIALOG_DATA,
import {MatButton} from "@angular/material/button"; MatDialogActions,
import {DialogRef} from "@angular/cdk/dialog"; MatDialogContent,
MatDialogTitle,
} from '@angular/material/dialog';
import { Employee } from '../Employee';
import { MatButton } from '@angular/material/button';
import { DialogRef } from '@angular/cdk/dialog';
@Component({ @Component({
selector: 'app-details', selector: 'app-details',
imports: [ imports: [MatDialogTitle, MatDialogContent, MatButton, MatDialogActions],
MatDialogTitle,
MatDialogContent,
MatButton,
MatDialogActions
],
templateUrl: './details.component.html', templateUrl: './details.component.html',
standalone: true, standalone: true,
styleUrl: './details.component.css' styleUrl: './details.component.css',
}) })
export class DetailsComponent { export class DetailsComponent {
employee: Employee = inject(MAT_DIALOG_DATA); employee: Employee = inject(MAT_DIALOG_DATA);

View File

@ -5,74 +5,98 @@
<div class="flex gap-x-4"> <div class="flex gap-x-4">
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>First Name</mat-label> <mat-label>First Name</mat-label>
<input matInput formControlName="firstName" required> <input matInput formControlName="firstName" required />
<mat-hint>Enter the first name</mat-hint> <mat-hint>Enter the first name</mat-hint>
<mat-error *ngIf="errors['firstName']">{{errors['firstName']}}</mat-error> <mat-error *ngIf="errors['firstName']">{{
errors["firstName"]
}}</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Last Name</mat-label> <mat-label>Last Name</mat-label>
<input matInput formControlName="lastName" required> <input matInput formControlName="lastName" required />
<mat-hint>Enter the last name</mat-hint> <mat-hint>Enter the last name</mat-hint>
<mat-error *ngIf="errors['lastName']">{{errors['lastName']}}</mat-error> <mat-error *ngIf="errors['lastName']">{{
errors["lastName"]
}}</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Street</mat-label> <mat-label>Street</mat-label>
<input matInput formControlName="street" required> <input matInput formControlName="street" required />
<mat-hint>Enter the street address</mat-hint> <mat-hint>Enter the street address</mat-hint>
<mat-error *ngIf="errors['street']">{{errors['street']}}</mat-error> <mat-error *ngIf="errors['street']">{{ errors["street"] }}</mat-error>
</mat-form-field> </mat-form-field>
<div class="flex gap-x-4"> <div class="flex gap-x-4">
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>City</mat-label> <mat-label>City</mat-label>
<input matInput formControlName="city" required> <input matInput formControlName="city" required />
<mat-hint>Enter the city</mat-hint> <mat-hint>Enter the city</mat-hint>
<mat-error *ngIf="errors['city']">{{errors['city']}}</mat-error> <mat-error *ngIf="errors['city']">{{ errors["city"] }}</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="!w-1/2"> <mat-form-field class="!w-1/2">
<mat-label>Postcode</mat-label> <mat-label>Postcode</mat-label>
<input matInput formControlName="postcode" minlength="5" maxlength="5" type="number" required> <input
matInput
formControlName="postcode"
minlength="5"
maxlength="5"
type="number"
required
/>
<mat-hint>Enter postcode</mat-hint> <mat-hint>Enter postcode</mat-hint>
<mat-error *ngIf="errors['postcode']">{{errors['postcode']}}</mat-error> <mat-error *ngIf="errors['postcode']">{{
errors["postcode"]
}}</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Phone</mat-label> <mat-label>Phone</mat-label>
<input matInput formControlName="phone" required> <input matInput formControlName="phone" required />
<mat-hint>Enter phone number</mat-hint> <mat-hint>Enter phone number</mat-hint>
<mat-error *ngIf="errors['phone']">{{errors['phone']}}</mat-error> <mat-error *ngIf="errors['phone']">{{ errors["phone"] }}</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="!w-full"> <mat-form-field class="!w-full">
<mat-label>Qualifications</mat-label> <mat-label>Qualifications</mat-label>
<mat-select formControlName="qualifications" multiple> <mat-select formControlName="qualifications" multiple>
<mat-option *ngFor="let qualification of qualifications" [value]="qualification.id"> <mat-option
*ngFor="let qualification of qualifications"
[value]="qualification.id"
>
{{ qualification.skill }} {{ qualification.skill }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-hint>Select qualifications</mat-hint> <mat-hint>Select qualifications</mat-hint>
<mat-error *ngIf="errors['qualifications']">{{errors['qualifications']}}</mat-error> <mat-error *ngIf="errors['qualifications']">{{
errors["qualifications"]
}}</mat-error>
</mat-form-field> </mat-form-field>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"> <mat-dialog-actions
<button mat-button align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
mat-dialog-close mat-dialog-close
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"
>
Cancel Cancel
</button> </button>
<button mat-flat-button <button
mat-flat-button
color="primary" color="primary"
type="submit" type="submit"
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"> class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
>
Submit Submit
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>
</div> </div>
</form> </form>
</mat-dialog-content> </mat-dialog-content>

View File

@ -1,23 +1,29 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import {AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; import {
AbstractControl,
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogActions, MatDialogActions,
MatDialogClose, MatDialogClose,
MatDialogContent, MatDialogContent,
MatDialogRef, MatDialogRef,
MatDialogTitle MatDialogTitle,
} from "@angular/material/dialog"; } from '@angular/material/dialog';
import {NgForOf, NgIf} from "@angular/common"; import { NgForOf, NgIf } from '@angular/common';
import {MatFormField, MatHint} from "@angular/material/form-field"; import { MatFormField, MatHint } from '@angular/material/form-field';
import {MatError, MatInput, MatLabel} from "@angular/material/input"; import { MatError, MatInput, MatLabel } from '@angular/material/input';
import {MatButton} from "@angular/material/button"; import { MatButton } from '@angular/material/button';
import {Employee} from "../Employee"; import { Employee } from '../Employee';
import EmployeeApiService from "../../services/employee-api.service"; import EmployeeApiService from '../../services/employee-api.service';
import QualificationService from "../../services/qualification.service"; import QualificationService from '../../services/qualification.service';
import {Qualification} from "../../qualification/Qualification"; import { Qualification } from '../../qualification/Qualification';
import {MatOption, MatSelect} from "@angular/material/select"; import { MatOption, MatSelect } from '@angular/material/select';
import {debounceTime} from "rxjs"; import { debounceTime } from 'rxjs';
@Component({ @Component({
selector: 'app-edit', selector: 'app-edit',
@ -40,7 +46,7 @@ import {debounceTime} from "rxjs";
], ],
templateUrl: './edit.component.html', templateUrl: './edit.component.html',
standalone: true, standalone: true,
styleUrl: './edit.component.css' styleUrl: './edit.component.css',
}) })
export class EditComponent implements OnInit { export class EditComponent implements OnInit {
employeeForm!: FormGroup; employeeForm!: FormGroup;
@ -50,17 +56,17 @@ export class EditComponent implements OnInit {
dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef); dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef);
employee: Employee = inject(MAT_DIALOG_DATA); employee: Employee = inject(MAT_DIALOG_DATA);
qualifications: Qualification[] = []; qualifications: Qualification[] = [];
errorMsgs: { [key: string]: string } = { errorMsgs: Record<string, string> = {
firstName: 'First name is required', firstName: 'First name is required',
lastName: 'Last name is required', lastName: 'Last name is required',
street: 'Street is required', street: 'Street is required',
postcode: 'Postcode must be 5 characters long', postcode: 'Postcode must be 5 characters long',
city: 'City is required', city: 'City is required',
phone: 'Phone is required', phone: 'Phone is required',
qualifications: 'Qualifications are required' qualifications: 'Qualifications are required',
} };
errors: { [key: string]: string } = {} errors: Record<string, string> = {};
ngOnInit(): void { ngOnInit(): void {
this.loadQualifications(); this.loadQualifications();
@ -68,10 +74,13 @@ export class EditComponent implements OnInit {
firstName: [this.employee.firstName, Validators.required], firstName: [this.employee.firstName, Validators.required],
lastName: [this.employee.lastName, Validators.required], lastName: [this.employee.lastName, Validators.required],
street: [this.employee.street, 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], city: [this.employee.city, Validators.required],
phone: [this.employee.phone, 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) => { Object.keys(this.employeeForm.controls).forEach((controlName: string) => {
@ -83,9 +92,9 @@ export class EditComponent implements OnInit {
} }
loadQualifications() { loadQualifications() {
this.qualificationService.getAll().subscribe( this.qualificationService
qualifications => this.qualifications = qualifications .getAll()
); .subscribe((qualifications) => (this.qualifications = qualifications));
} }
submit() { submit() {
@ -102,10 +111,12 @@ export class EditComponent implements OnInit {
const formValue = this.employeeForm.value; const formValue = this.employeeForm.value;
const employeeData = { const employeeData = {
...formValue, ...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); this.dialogRef.close(true);
} }

View File

@ -4,23 +4,37 @@
<div class="!space-y-6"> <div class="!space-y-6">
<div class="!flex !justify-between !items-center"> <div class="!flex !justify-between !items-center">
<div class="!flex !items-center !gap-4"> <div class="!flex !items-center !gap-4">
<h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">Employee Directory</h2> <h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">
Employee Directory
</h2>
<mat-form-field class="!m-0" subscriptSizing="dynamic"> <mat-form-field class="!m-0" subscriptSizing="dynamic">
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon> <mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<input matInput <input
matInput
type="text" type="text"
placeholder="Search employees..." placeholder="Search employees..."
(keyup)="filterEmployees($event)"> (keyup)="filterEmployees($event)"
<div matSuffix class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center"> />
<mat-progress-spinner [diameter]="20" mode="indeterminate" <div
matSuffix
class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center"
>
<mat-progress-spinner
[diameter]="20"
mode="indeterminate"
[class.!opacity-0]="!isSearching" [class.!opacity-0]="!isSearching"
[class.!opacity-100]="isSearching" [class.!opacity-100]="isSearching"
class="!transition-opacity"></mat-progress-spinner> class="!transition-opacity"
></mat-progress-spinner>
</div> </div>
</mat-form-field> </mat-form-field>
</div> </div>
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white !shrink-0" <button
(click)="showCreateEmployeeModal()"> mat-flat-button
color="primary"
class="!bg-blue-600 !text-white !shrink-0"
(click)="showCreateEmployeeModal()"
>
<mat-icon class="!mr-2">add</mat-icon> <mat-icon class="!mr-2">add</mat-icon>
Add Employee Add Employee
</button> </button>
@ -30,34 +44,64 @@
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4"> <div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
<table mat-table [dataSource]="employees" matSort class="!w-full"> <table mat-table [dataSource]="employees" matSort class="!w-full">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef class="!text-left !w-full"> Name</th> <th
mat-header-cell
*matHeaderCellDef
class="!text-left !w-full"
>
Name
</th>
<td mat-cell *matCellDef="let employee" class="!py-4"> <td mat-cell *matCellDef="let employee" class="!py-4">
<div class="!flex !items-center"> <div class="!flex !items-center">
<div class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3"> <div
class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3"
>
<span class="!text-blue-600 !font-medium"> <span class="!text-blue-600 !font-medium">
{{ employee.firstName[0] }}{{ employee.lastName[0] }} {{ employee.firstName[0] }}{{ employee.lastName[0] }}
</span> </span>
</div> </div>
<div> <div>
<a class="!text-blue-600 hover:!underline cursor-pointer" <button
[matTooltip]="'Click to view Employee details'" (click)="openDetailModal(employee)"> class="text-blue-600 hover:underline cursor-pointer"
[matTooltip]="'View Employee details'"
(click)="openDetailModal(employee)"
(keydown.enter)="openDetailModal(employee)"
>
{{ employee.lastName }}, {{ employee.firstName }} {{ employee.lastName }}, {{ employee.firstName }}
</a> </button>
</div> </div>
</div> </div>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="!text-right !w-[120px]"> Actions</th> <th
<td mat-cell *matCellDef="let employee" class="!text-right !py-4 !whitespace-nowrap"> mat-header-cell
*matHeaderCellDef
class="!text-right !w-[120px]"
>
Actions
</th>
<td
mat-cell
*matCellDef="let employee"
class="!text-right !py-4 !whitespace-nowrap"
>
<div class="!flex !justify-end !items-center !gap-1"> <div class="!flex !justify-end !items-center !gap-1">
<button mat-icon-button color="primary" [matTooltip]="'Edit employee'" <button
(click)="showEditEmployeeModal(employee)"> mat-icon-button
color="primary"
[matTooltip]="'Edit employee'"
(click)="showEditEmployeeModal(employee)"
>
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button color="warn" [matTooltip]="'Delete employee'" <button
(click)="openDeleteDialogue(employee)"> mat-icon-button
color="warn"
[matTooltip]="'Delete employee'"
(click)="openDeleteDialogue(employee)"
>
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</div> </div>
@ -65,7 +109,7 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table> </table>
</div> </div>
} @else { } @else {
@ -98,10 +142,16 @@
} @error { } @error {
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200"> <div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
<div class="flex items-start space-x-2 md:space-x-3"> <div class="flex items-start space-x-2 md:space-x-3">
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon> <mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
>error</mat-icon
>
<div> <div>
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error loading the employees.</p> <p class="text-gray-800 font-medium text-sm md:text-base">
<p class="text-gray-600 mt-1 text-xs md:text-sm">Please try refreshing the page.</p> There was an error loading the employees.
</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">
Please try refreshing the page.
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,14 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {catchError, debounceTime, distinctUntilChanged, Observable, of, retry, Subject} from 'rxjs'; import {
catchError,
debounceTime,
distinctUntilChanged,
Observable,
of,
retry,
Subject,
} from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Employee } from '../Employee'; import { Employee } from '../Employee';
@ -14,15 +22,15 @@ import {MatTooltipModule} from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import {MatDialog} from "@angular/material/dialog"; import { MatDialog } from '@angular/material/dialog';
import {DeleteComponent} from "../delete/delete.component"; import { DeleteComponent } from '../delete/delete.component';
import EmployeeApiService from "../../services/employee-api.service"; import EmployeeApiService from '../../services/employee-api.service';
import {CreateComponent} from "../create/create.component"; import { CreateComponent } from '../create/create.component';
import {EditComponent} from "../edit/edit.component"; import { EditComponent } from '../edit/edit.component';
import {DetailsComponent} from "../details/details.component"; import { DetailsComponent } from '../details/details.component';
import {MatFormFieldModule} from "@angular/material/form-field"; import { MatFormFieldModule } from '@angular/material/form-field';
import {MatInputModule} from "@angular/material/input"; import { MatInputModule } from '@angular/material/input';
import {ErrorHandlerService} from "../../services/error.handler.service"; import { ErrorHandlerService } from '../../services/error.handler.service';
@Component({ @Component({
selector: 'app-employee-list', selector: 'app-employee-list',
@ -43,12 +51,13 @@ import {ErrorHandlerService} from "../../services/error.handler.service";
MatInputModule, MatInputModule,
], ],
templateUrl: './table.component.html', templateUrl: './table.component.html',
styleUrl: './table.component.css' styleUrl: './table.component.css',
}) })
export class TableComponent implements OnInit { export class TableComponent implements OnInit {
private readonly apiService: EmployeeApiService = inject(EmployeeApiService); private readonly apiService: EmployeeApiService = inject(EmployeeApiService);
private readonly matDialog: MatDialog = inject(MatDialog); private readonly matDialog: MatDialog = inject(MatDialog);
private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService); private readonly errorHandlerService: ErrorHandlerService =
inject(ErrorHandlerService);
private static readonly MAX_RETRIES = 3; private static readonly MAX_RETRIES = 3;
@ -64,22 +73,22 @@ export class TableComponent implements OnInit {
} }
private loadEmployees(): void { private loadEmployees(): void {
this.fetchEmployees().subscribe(employees => { this.fetchEmployees().subscribe((employees) => {
this.allEmployees = employees; this.allEmployees = employees;
this.employees$ = of(employees); this.employees$ = of(employees);
}); });
} }
private setupSearch(): void { private setupSearch(): void {
this.searchSubject.pipe( this.searchSubject
debounceTime(300), .pipe(debounceTime(300), distinctUntilChanged())
distinctUntilChanged() .subscribe((searchTerm) => {
).subscribe(searchTerm => {
this.isSearching = true; this.isSearching = true;
setTimeout(() => { setTimeout(() => {
const filteredEmployees = this.allEmployees.filter(employee => const filteredEmployees = this.allEmployees.filter(
(employee) =>
employee.firstName?.toLowerCase().includes(searchTerm) || employee.firstName?.toLowerCase().includes(searchTerm) ||
employee.lastName?.toLowerCase().includes(searchTerm) employee.lastName?.toLowerCase().includes(searchTerm),
); );
this.employees$ = of(filteredEmployees); this.employees$ = of(filteredEmployees);
this.isSearching = false; this.isSearching = false;
@ -92,14 +101,17 @@ export class TableComponent implements OnInit {
retry(TableComponent.MAX_RETRIES), retry(TableComponent.MAX_RETRIES),
catchError((error: HttpErrorResponse) => { catchError((error: HttpErrorResponse) => {
console.error('Error fetching employees:', error); 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([]); return of([]);
}) }),
); );
} }
protected openDeleteDialogue(employee: Employee): void { protected openDeleteDialogue(employee: Employee): void {
this.matDialog.open(DeleteComponent, {data: employee}) this.matDialog
.open(DeleteComponent, { data: employee })
.afterClosed() .afterClosed()
.subscribe((deleted: boolean) => { .subscribe((deleted: boolean) => {
if (deleted) { if (deleted) {
@ -109,7 +121,8 @@ export class TableComponent implements OnInit {
} }
protected showCreateEmployeeModal() { protected showCreateEmployeeModal() {
this.matDialog.open(CreateComponent) this.matDialog
.open(CreateComponent)
.afterClosed() .afterClosed()
.subscribe((created: boolean) => { .subscribe((created: boolean) => {
if (created) { if (created) {
@ -119,7 +132,8 @@ export class TableComponent implements OnInit {
} }
protected showEditEmployeeModal(employee: Employee) { protected showEditEmployeeModal(employee: Employee) {
this.matDialog.open(EditComponent, {data: employee}) this.matDialog
.open(EditComponent, { data: employee })
.afterClosed() .afterClosed()
.subscribe((edited: boolean) => { .subscribe((edited: boolean) => {
if (edited) { if (edited) {

View File

@ -1,16 +1,11 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {TableComponent} from "../employee/table/table.component"; import { TableComponent } from '../employee/table/table.component';
import {QualificationsComponent} from "../qualification/table/table.component"; import { QualificationsComponent } from '../qualification/table/table.component';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
imports: [ imports: [TableComponent, QualificationsComponent],
TableComponent,
QualificationsComponent
],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.css' styleUrl: './home.component.css',
}) })
export class HomeComponent { export class HomeComponent {}
}

View File

@ -1 +1,3 @@
<div class="dot-loader">Logging in<span>{{ dots }}</span></div> <div class="dot-loader">
Logging in<span>{{ dots }}</span>
</div>

View File

@ -1,16 +1,16 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import {interval, Subscription} from "rxjs"; import { interval, Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
imports: [], imports: [],
templateUrl: './login.component.html', templateUrl: './login.component.html',
standalone: true, standalone: true,
styleUrl: './login.component.css' styleUrl: './login.component.css',
}) })
export class LoginComponent implements OnInit, OnDestroy { export class LoginComponent implements OnInit, OnDestroy {
dots: string = ''; dots = '';
private maxDots: number = 4; // Maximum number of dots private maxDots = 4; // Maximum number of dots
private intervalSub!: Subscription; private intervalSub!: Subscription;
ngOnInit(): void { ngOnInit(): void {

View File

@ -1,4 +1,6 @@
export class Qualification { export class Qualification {
constructor(public id: number, public skill?: string) { constructor(
} public id: number,
public skill?: string,
) {}
} }

View File

@ -1,15 +1,30 @@
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Create Qualification</h2> <h2
mat-dialog-title
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
>
Create Qualification
</h2>
<mat-dialog-content class="!px-3 md:!px-6"> <mat-dialog-content class="!px-3 md:!px-6">
<form [formGroup]="qualificationForm" (ngSubmit)="create()" class="w-full min-w-[280px] md:min-w-[400px]"> <form
[formGroup]="qualificationForm"
(ngSubmit)="create()"
class="w-full min-w-[280px] md:min-w-[400px]"
>
<div class="space-y-4 md:space-y-6"> <div class="space-y-4 md:space-y-6">
@if (apiErrorMessage) { @if (apiErrorMessage) {
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200"> <div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
<div class="flex items-start space-x-2 md:space-x-3"> <div class="flex items-start space-x-2 md:space-x-3">
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon> <mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
>error</mat-icon
>
<div> <div>
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error creating the qualification.</p> <p class="text-gray-800 font-medium text-sm md:text-base">
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiErrorMessage }}</p> There was an error creating the qualification.
</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">
{{ apiErrorMessage }}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -18,27 +33,36 @@
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3"> <div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3">
<mat-form-field class="w-full"> <mat-form-field class="w-full">
<mat-label>Skill</mat-label> <mat-label>Skill</mat-label>
<input matInput <input
matInput
formControlName="skill" formControlName="skill"
placeholder="Enter skill name" placeholder="Enter skill name"
required> required
/>
<mat-hint class="text-sm">Enter the skill name</mat-hint> <mat-hint class="text-sm">Enter the skill name</mat-hint>
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm"> <mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
{{ getErrorMessage('skill') }} {{ getErrorMessage("skill") }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"> <mat-dialog-actions
<button mat-button align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
mat-dialog-close mat-dialog-close
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
>
Cancel Cancel
</button> </button>
<button mat-flat-button <button
mat-flat-button
color="primary" color="primary"
type="submit" type="submit"
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"> class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
>
Create Create
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@ -1,19 +1,23 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import {FormBuilder, ReactiveFormsModule, Validators} from "@angular/forms"; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import QualificationService from "../../services/qualification.service"; import QualificationService from '../../services/qualification.service';
import { import {
MatDialogActions, MatDialogActions,
MatDialogClose, MatDialogClose,
MatDialogContent, MatDialogContent,
MatDialogRef, MatDialogRef,
MatDialogTitle MatDialogTitle,
} from "@angular/material/dialog"; } from '@angular/material/dialog';
import {NgIf} from "@angular/common"; import { NgIf } from '@angular/common';
import {MatError, MatFormField, MatHint, MatLabel} from "@angular/material/form-field"; import {
import {MatButton} from "@angular/material/button"; MatError,
import {MatInput} from "@angular/material/input"; MatFormField,
import {MatIcon} from "@angular/material/icon"; MatHint,
import {filter} from "rxjs"; 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({ @Component({
selector: 'app-create-qualification', selector: 'app-create-qualification',
@ -30,27 +34,28 @@ import {filter} from "rxjs";
MatInput, MatInput,
MatDialogClose, MatDialogClose,
MatHint, MatHint,
MatIcon MatIcon,
], ],
templateUrl: './create.component.html', templateUrl: './create.component.html',
styleUrl: './create.component.css' styleUrl: './create.component.css',
}) })
export class CreateComponent { export class CreateComponent {
private formBuilder: FormBuilder = inject(FormBuilder); private formBuilder: FormBuilder = inject(FormBuilder);
private qualificationService: QualificationService = inject(QualificationService); private qualificationService: QualificationService =
inject(QualificationService);
private dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef); private dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef);
public apiErrorMessage: string = ''; public apiErrorMessage = '';
qualificationForm = this.formBuilder.group({ qualificationForm = this.formBuilder.group({
'skill': ['', Validators.required], skill: ['', Validators.required],
}); });
isFieldInvalid(fieldName: string): boolean { isFieldInvalid(fieldName: string): boolean {
const field = this.qualificationForm.get(fieldName); const field = this.qualificationForm.get(fieldName);
if (!field) { 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); return field.invalid && (field.dirty || field.touched);
@ -72,15 +77,18 @@ export class CreateComponent {
return; return;
} }
this.qualificationService.create(this.qualificationForm.value).subscribe({ const qualification = {
skill: this.qualificationForm.value.skill || '',
};
this.qualificationService.create(qualification).subscribe({
next: (createdQualification) => { next: (createdQualification) => {
this.dialogRef.close(createdQualification); this.dialogRef.close(createdQualification);
}, },
error: (error) => { error: (error) => {
console.error('Error creating qualification:', error); console.error('Error creating qualification:', error);
this.apiErrorMessage = 'API Error'; this.apiErrorMessage = 'API Error';
} },
}); });
} }
} }

View File

@ -1,13 +1,22 @@
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Delete Qualification</h2> <h2
mat-dialog-title
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
>
Delete Qualification
</h2>
<mat-dialog-content class="!px-3 md:!px-6"> <mat-dialog-content class="!px-3 md:!px-6">
<div class="w-full min-w-[280px] md:min-w-[400px] space-y-4 md:space-y-6"> <div class="w-full min-w-[280px] md:min-w-[400px] space-y-4 md:space-y-6">
@if (apiError) { @if (apiError) {
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200"> <div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
<div class="flex items-start space-x-2 md:space-x-3"> <div class="flex items-start space-x-2 md:space-x-3">
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon> <mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
>error</mat-icon
>
<div> <div>
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error deleting the qualification.</p> <p class="text-gray-800 font-medium text-sm md:text-base">
There was an error deleting the qualification.
</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiError }}</p> <p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiError }}</p>
</div> </div>
</div> </div>
@ -16,26 +25,38 @@
<div class="bg-amber-50 p-3 md:p-4 rounded-lg border border-amber-200"> <div class="bg-amber-50 p-3 md:p-4 rounded-lg border border-amber-200">
<div class="flex items-start space-x-2 md:space-x-3"> <div class="flex items-start space-x-2 md:space-x-3">
<mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8">warning</mat-icon> <mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8"
>warning</mat-icon
>
<div> <div>
<p class="text-gray-800 font-medium text-sm md:text-base">Are you sure you want to delete this qualification?</p> <p class="text-gray-800 font-medium text-sm md:text-base">
<p class="text-gray-600 mt-1 text-xs md:text-sm">This action cannot be undone.</p> Are you sure you want to delete this qualification?
</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">
This action cannot be undone.
</p>
</div> </div>
</div> </div>
</div> </div>
<mat-dialog-actions
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"> align="end"
<button mat-button class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
(click)="closeModal()" (click)="closeModal()"
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
>
Cancel Cancel
</button> </button>
<button mat-flat-button <button
mat-flat-button
color="warn" color="warn"
(click)="delete()" (click)="delete()"
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1" class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
cdkFocusInitial> cdkFocusInitial
>
Delete Delete
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@ -4,14 +4,13 @@ import {
MatDialogActions, MatDialogActions,
MatDialogContent, MatDialogContent,
MatDialogRef, MatDialogRef,
MatDialogTitle MatDialogTitle,
} from "@angular/material/dialog"; } from '@angular/material/dialog';
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import QualificationService from "../../services/qualification.service"; import QualificationService from '../../services/qualification.service';
import {MatButton} from "@angular/material/button"; import { MatButton } from '@angular/material/button';
import {HttpErrorResponse} from "@angular/common/http"; import { HttpErrorResponse } from '@angular/common/http';
import { MatError } from '@angular/material/form-field' import { MatIcon } from '@angular/material/icon';
import {MatIcon} from "@angular/material/icon";
@Component({ @Component({
selector: 'app-delete-qualification', selector: 'app-delete-qualification',
@ -22,17 +21,18 @@ import {MatIcon} from "@angular/material/icon";
ReactiveFormsModule, ReactiveFormsModule,
MatDialogActions, MatDialogActions,
MatButton, MatButton,
MatIcon MatIcon,
], ],
templateUrl: './delete.component.html', templateUrl: './delete.component.html',
standalone: true, standalone: true,
styleUrl: './delete.component.css' styleUrl: './delete.component.css',
}) })
export class DeleteComponent { export class DeleteComponent {
public id: number = inject(MAT_DIALOG_DATA); public id: number = inject(MAT_DIALOG_DATA);
public apiError: string | null = null; public apiError: string | null = null;
private qualificationService: QualificationService = inject(QualificationService); private qualificationService: QualificationService =
inject(QualificationService);
private dialogRef: MatDialogRef<DeleteComponent> = inject(MatDialogRef); private dialogRef: MatDialogRef<DeleteComponent> = inject(MatDialogRef);
delete() { delete() {
@ -45,11 +45,12 @@ export class DeleteComponent {
if (error.error.message.includes('SQL')) { if (error.error.message.includes('SQL')) {
// The API message is undescriptive but this is the most common // 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 { } else {
this.apiError = 'API Error'; this.apiError = 'API Error';
} }
} },
}); });
} }

View File

@ -1,4 +1,7 @@
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"> <h2
mat-dialog-title
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
>
{{ qualification.skill }} Developers {{ qualification.skill }} Developers
</h2> </h2>
@ -7,25 +10,37 @@
@if (employees$ | async; as employees) { @if (employees$ | async; as employees) {
@if (employees.length === 0) { @if (employees.length === 0) {
<div class="bg-gray-50 p-3 md:p-4 rounded-lg text-center"> <div class="bg-gray-50 p-3 md:p-4 rounded-lg text-center">
<mat-icon class="text-gray-400 text-xl md:text-2xl mb-2 !w-8 !h-8">person_off</mat-icon> <mat-icon class="text-gray-400 text-xl md:text-2xl mb-2 !w-8 !h-8"
<p class="text-gray-600 text-sm md:text-base">No employees found with this qualification.</p> >person_off</mat-icon
>
<p class="text-gray-600 text-sm md:text-base">
No employees found with this qualification.
</p>
</div> </div>
} @else { } @else {
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-2"> <div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-2">
@for (employee of employees; track employee.id) { @for (employee of employees; track employee.id) {
<a class="block w-full p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors cursor-pointer border border-gray-100 hover:border-blue-100" <button
(click)="openEmployeeDetailsModal(employee.id)"> class="block w-full p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors cursor-pointer border border-gray-100 hover:border-blue-100 text-left"
(click)="openEmployeeDetailsModal(employee.id)"
(keydown.enter)="openEmployeeDetailsModal(employee.id)"
>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="h-8 w-8 md:h-10 md:w-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0"> <div
class="h-8 w-8 md:h-10 md:w-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0"
>
<span class="text-blue-600 font-medium text-sm md:text-base"> <span class="text-blue-600 font-medium text-sm md:text-base">
{{ employee.firstName?.charAt(0) }}{{ employee.lastName?.charAt(0) }} {{ employee.firstName?.charAt(0)
}}{{ employee.lastName?.charAt(0) }}
</span> </span>
</div> </div>
<div> <div>
<span class="font-medium text-gray-900 text-sm md:text-base">{{ employee.firstName }} {{ employee.lastName }}</span> <span class="font-medium text-gray-900 text-sm md:text-base"
>{{ employee.firstName }} {{ employee.lastName }}</span
>
</div> </div>
</div> </div>
</a> </button>
} }
</div> </div>
} }
@ -34,9 +49,11 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end" class="!px-3 md:!px-6 !py-4 !mt-4 border-t"> <mat-dialog-actions align="end" class="!px-3 md:!px-6 !py-4 !mt-4 border-t">
<button mat-button <button
mat-button
(click)="closeModal()" (click)="closeModal()"
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full"
>
Close Close
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@ -5,15 +5,15 @@ import {
MatDialogActions, MatDialogActions,
MatDialogContent, MatDialogContent,
MatDialogRef, MatDialogRef,
MatDialogTitle MatDialogTitle,
} from "@angular/material/dialog"; } from '@angular/material/dialog';
import QualificationService from "../../services/qualification.service"; import QualificationService from '../../services/qualification.service';
import {Qualification} from "../Qualification"; import { Qualification } from '../Qualification';
import {AsyncPipe} from "@angular/common"; import { AsyncPipe } from '@angular/common';
import {MatButton} from "@angular/material/button"; import { MatButton } from '@angular/material/button';
import {DetailsComponent as EmployeeDetailsComponent} from "../../employee/details/details.component"; import { DetailsComponent as EmployeeDetailsComponent } from '../../employee/details/details.component';
import EmployeeApiService from "../../services/employee-api.service"; import EmployeeApiService from '../../services/employee-api.service';
import {MatIcon} from "@angular/material/icon"; import { MatIcon } from '@angular/material/icon';
@Component({ @Component({
selector: 'app-details', selector: 'app-details',
@ -23,10 +23,10 @@ import {MatIcon} from "@angular/material/icon";
MatDialogTitle, MatDialogTitle,
MatDialogActions, MatDialogActions,
MatButton, MatButton,
MatIcon MatIcon,
], ],
templateUrl: './details.component.html', templateUrl: './details.component.html',
styleUrl: './details.component.css' styleUrl: './details.component.css',
}) })
export class DetailsComponent { export class DetailsComponent {
private qualificationService = inject(QualificationService); private qualificationService = inject(QualificationService);
@ -35,7 +35,9 @@ export class DetailsComponent {
private dialog: MatDialog = inject(MatDialog); private dialog: MatDialog = inject(MatDialog);
public qualification: Qualification = inject(MAT_DIALOG_DATA); public qualification: Qualification = inject(MAT_DIALOG_DATA);
public employees$ = this.qualificationService.findEmployees(this.qualification.id); public employees$ = this.qualificationService.findEmployees(
this.qualification.id,
);
closeModal() { closeModal() {
this.dialogRef.close(); this.dialogRef.close();
@ -43,12 +45,12 @@ export class DetailsComponent {
openEmployeeDetailsModal(id: number | undefined) { openEmployeeDetailsModal(id: number | undefined) {
if (!id) { 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, { this.dialog.open(EmployeeDetailsComponent, {
data: employee data: employee,
}); });
}); });
} }

View File

@ -1,15 +1,30 @@
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Edit Qualification</h2> <h2
mat-dialog-title
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
>
Edit Qualification
</h2>
<mat-dialog-content class="!px-3 md:!px-6"> <mat-dialog-content class="!px-3 md:!px-6">
<form [formGroup]="qualificationForm" (ngSubmit)="edit()" class="w-full min-w-[280px] md:min-w-[400px]"> <form
[formGroup]="qualificationForm"
(ngSubmit)="edit()"
class="w-full min-w-[280px] md:min-w-[400px]"
>
<div class="space-y-4 md:space-y-6"> <div class="space-y-4 md:space-y-6">
@if (apiErrorMessage) { @if (apiErrorMessage) {
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200"> <div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
<div class="flex items-start space-x-2 md:space-x-3"> <div class="flex items-start space-x-2 md:space-x-3">
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon> <mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
>error</mat-icon
>
<div> <div>
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error editing the qualification.</p> <p class="text-gray-800 font-medium text-sm md:text-base">
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiErrorMessage }}</p> There was an error editing the qualification.
</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">
{{ apiErrorMessage }}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -18,27 +33,36 @@
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3"> <div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3">
<mat-form-field class="w-full"> <mat-form-field class="w-full">
<mat-label>Skill</mat-label> <mat-label>Skill</mat-label>
<input matInput <input
matInput
formControlName="skill" formControlName="skill"
placeholder="Enter skill name" placeholder="Enter skill name"
required> required
/>
<mat-hint class="text-sm">Enter the skill name</mat-hint> <mat-hint class="text-sm">Enter the skill name</mat-hint>
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm"> <mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
{{ getErrorMessage('skill') }} {{ getErrorMessage("skill") }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"> <mat-dialog-actions
<button mat-button align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
mat-dialog-close mat-dialog-close
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"> class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
>
Cancel Cancel
</button> </button>
<button mat-flat-button <button
mat-flat-button
color="primary" color="primary"
type="submit" type="submit"
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"> class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
>
Save Changes Save Changes
</button> </button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@ -1,19 +1,30 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import {FormBuilder, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; import {
import QualificationService from "../../services/qualification.service"; FormBuilder,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import QualificationService from '../../services/qualification.service';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogActions, MatDialogClose, MatDialogActions,
MatDialogClose,
MatDialogContent, MatDialogContent,
MatDialogRef, MatDialogRef,
MatDialogTitle MatDialogTitle,
} from "@angular/material/dialog"; } from '@angular/material/dialog';
import {MatButton} from "@angular/material/button"; import { MatButton } from '@angular/material/button';
import {MatError, MatFormField, MatHint, MatLabel} from "@angular/material/form-field"; import {
import {MatInput} from "@angular/material/input"; MatError,
import {NgIf} from "@angular/common"; MatFormField,
import {Qualification} from "../Qualification"; MatHint,
import {MatIcon} from "@angular/material/icon"; 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({ @Component({
selector: 'app-edit-qualification', selector: 'app-edit-qualification',
@ -31,28 +42,29 @@ import {MatIcon} from "@angular/material/icon";
ReactiveFormsModule, ReactiveFormsModule,
MatDialogClose, MatDialogClose,
MatHint, MatHint,
MatIcon MatIcon,
], ],
templateUrl: './edit.component.html', templateUrl: './edit.component.html',
styleUrl: './edit.component.css' styleUrl: './edit.component.css',
}) })
export class EditComponent { export class EditComponent {
public apiErrorMessage: string = ''; public apiErrorMessage = '';
public qualification: Qualification = inject(MAT_DIALOG_DATA); public qualification: Qualification = inject(MAT_DIALOG_DATA);
private formBuilder: FormBuilder = inject(FormBuilder); private formBuilder: FormBuilder = inject(FormBuilder);
private qualificationService: QualificationService = inject(QualificationService); private qualificationService: QualificationService =
inject(QualificationService);
private dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef); private dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef);
qualificationForm = this.formBuilder.group({ qualificationForm = this.formBuilder.group({
'skill': [this.qualification.skill, Validators.required], skill: [this.qualification.skill, Validators.required],
}); });
isFieldInvalid(fieldName: string): boolean { isFieldInvalid(fieldName: string): boolean {
const field = this.qualificationForm.get(fieldName); const field = this.qualificationForm.get(fieldName);
if (!field) { 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); return field.invalid && (field.dirty || field.touched);
@ -74,15 +86,20 @@ export class EditComponent {
return; return;
} }
this.qualificationService.edit(this.qualification.id, this.qualificationForm.value).subscribe({ const qualification = {
skill: this.qualificationForm.value.skill || '',
};
this.qualificationService
.update(this.qualification.id, qualification)
.subscribe({
next: (editedQualification) => { next: (editedQualification) => {
this.dialogRef.close(editedQualification); this.dialogRef.close(editedQualification);
}, },
error: (error) => { error: (error) => {
console.error('Error creating qualification:', error); console.error('Error updating qualification:', error);
this.apiErrorMessage = 'API Error'; this.apiErrorMessage = 'API Error';
} },
}); });
} }
} }

View File

@ -4,23 +4,37 @@
<div class="!space-y-6"> <div class="!space-y-6">
<div class="!flex !justify-between !items-center"> <div class="!flex !justify-between !items-center">
<div class="!flex !items-center !gap-4"> <div class="!flex !items-center !gap-4">
<h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">Qualifications</h2> <h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">
Qualifications
</h2>
<mat-form-field class="!m-0" subscriptSizing="dynamic"> <mat-form-field class="!m-0" subscriptSizing="dynamic">
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon> <mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<input matInput <input
matInput
type="text" type="text"
placeholder="Search qualifications..." placeholder="Search qualifications..."
(keyup)="filterQualifications($event)"> (keyup)="filterQualifications($event)"
<div matSuffix class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center"> />
<mat-progress-spinner [diameter]="20" mode="indeterminate" <div
matSuffix
class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center"
>
<mat-progress-spinner
[diameter]="20"
mode="indeterminate"
[class.!opacity-0]="!isSearching" [class.!opacity-0]="!isSearching"
[class.!opacity-100]="isSearching" [class.!opacity-100]="isSearching"
class="!transition-opacity"></mat-progress-spinner> class="!transition-opacity"
></mat-progress-spinner>
</div> </div>
</mat-form-field> </mat-form-field>
</div> </div>
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white !shrink-0" <button
(click)="openCreateModal()"> mat-flat-button
color="primary"
class="!bg-blue-600 !text-white !shrink-0"
(click)="openCreateModal()"
>
<mat-icon class="!mr-2">add</mat-icon> <mat-icon class="!mr-2">add</mat-icon>
Add Qualification Add Qualification
</button> </button>
@ -28,41 +42,73 @@
@if (qualifications) { @if (qualifications) {
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4"> <div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
<table mat-table [dataSource]="qualifications" matSort class="!w-full"> <table
mat-table
[dataSource]="qualifications"
matSort
class="!w-full"
>
<ng-container matColumnDef="skill"> <ng-container matColumnDef="skill">
<th mat-header-cell *matHeaderCellDef class="!text-left !w-full">Skill</th> <th
mat-header-cell
*matHeaderCellDef
class="!text-left !w-full"
>
Skill
</th>
<td mat-cell *matCellDef="let qualification" class="!py-4"> <td mat-cell *matCellDef="let qualification" class="!py-4">
<div class="!flex !items-center"> <div class="!flex !items-center">
<div class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3"> <div
class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3"
>
<span class="!text-blue-600 !font-medium"> <span class="!text-blue-600 !font-medium">
{{ qualification.skill[0]?.toUpperCase() }} {{ qualification.skill[0]?.toUpperCase() }}
</span> </span>
</div> </div>
<div> <div>
<a class="!text-blue-600 hover:!underline cursor-pointer" <button
[matTooltip]="'Click to view qualification details'" class="text-blue-600 hover:underline cursor-pointer"
(click)="openDetailsModal(qualification)"> [matTooltip]="'View qualification details'"
(click)="openDetailsModal(qualification)"
(keydown.enter)="openDetailsModal(qualification)"
>
{{ qualification.skill }} {{ qualification.skill }}
</a> </button>
</div> </div>
</div> </div>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="!text-right !w-[120px]">Actions</th> <th
<td mat-cell *matCellDef="let qualification" class="!text-right !py-4 !whitespace-nowrap"> mat-header-cell
*matHeaderCellDef
class="!text-right !w-[120px]"
>
Actions
</th>
<td
mat-cell
*matCellDef="let qualification"
class="!text-right !py-4 !whitespace-nowrap"
>
<div class="!flex !justify-end !items-center !gap-1"> <div class="!flex !justify-end !items-center !gap-1">
<button mat-icon-button <button
mat-icon-button
color="primary" color="primary"
[matTooltip]="'Edit qualification'" [matTooltip]="'Edit qualification'"
(click)="openEditModal(qualification)"> (click)="openEditModal(qualification)"
(keydown.enter)="openEditModal(qualification)"
>
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button <button
mat-icon-button
color="warn" color="warn"
[matTooltip]="'Delete qualification'" [matTooltip]="'Delete qualification'"
(click)="openDeleteModal(qualification.id)"> (click)="openDeleteModal(qualification.id)"
(keydown.enter)="openDeleteModal(qualification.id)"
>
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</div> </div>
@ -70,7 +116,7 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table> </table>
</div> </div>
} @else { } @else {
@ -103,10 +149,16 @@
} @error { } @error {
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200"> <div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
<div class="flex items-start space-x-2 md:space-x-3"> <div class="flex items-start space-x-2 md:space-x-3">
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon> <mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
>error</mat-icon
>
<div> <div>
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error loading the qualifications.</p> <p class="text-gray-800 font-medium text-sm md:text-base">
<p class="text-gray-600 mt-1 text-xs md:text-sm">Please try refreshing the page.</p> There was an error loading the qualifications.
</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">
Please try refreshing the page.
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,13 +1,21 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {catchError, debounceTime, distinctUntilChanged, Observable, of, retry, Subject} from 'rxjs'; import {
catchError,
debounceTime,
distinctUntilChanged,
Observable,
of,
retry,
Subject,
} from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import {Qualification} from "../Qualification"; import { Qualification } from '../Qualification';
import {MatDialog} from "@angular/material/dialog"; import { MatDialog } from '@angular/material/dialog';
import QualificationService from "../../services/qualification.service"; import QualificationService from '../../services/qualification.service';
import {CreateComponent} from "../create/create.component"; import { CreateComponent } from '../create/create.component';
import {EditComponent} from "../edit/edit.component"; import { EditComponent } from '../edit/edit.component';
import {DeleteComponent} from "../delete/delete.component"; import { DeleteComponent } from '../delete/delete.component';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@ -18,10 +26,10 @@ import {MatTooltipModule} from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import {MatFormFieldModule} from "@angular/material/form-field"; import { MatFormFieldModule } from '@angular/material/form-field';
import {MatInputModule} from "@angular/material/input"; import { MatInputModule } from '@angular/material/input';
import {DetailsComponent} from "../details/details.component"; import { DetailsComponent } from '../details/details.component';
import {ErrorHandlerService} from "../../services/error.handler.service"; import { ErrorHandlerService } from '../../services/error.handler.service';
@Component({ @Component({
selector: 'app-qualifications', selector: 'app-qualifications',
@ -42,11 +50,13 @@ import {ErrorHandlerService} from "../../services/error.handler.service";
MatInputModule, MatInputModule,
], ],
templateUrl: './table.component.html', templateUrl: './table.component.html',
styleUrl: './table.component.css' styleUrl: './table.component.css',
}) })
export class QualificationsComponent implements OnInit { export class QualificationsComponent implements OnInit {
private readonly qualificationService: QualificationService = inject(QualificationService); private readonly qualificationService: QualificationService =
private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService); inject(QualificationService);
private readonly errorHandlerService: ErrorHandlerService =
inject(ErrorHandlerService);
private readonly dialog: MatDialog = inject(MatDialog); private readonly dialog: MatDialog = inject(MatDialog);
private static readonly MAX_RETRIES = 3; private static readonly MAX_RETRIES = 3;
@ -63,21 +73,21 @@ export class QualificationsComponent implements OnInit {
} }
private loadQualifications(): void { private loadQualifications(): void {
this.fetchQualifications().subscribe(qualifications => { this.fetchQualifications().subscribe((qualifications) => {
this.allQualifications = qualifications; this.allQualifications = qualifications;
this.qualifications$ = of(qualifications); this.qualifications$ = of(qualifications);
}); });
} }
private setupSearch(): void { private setupSearch(): void {
this.searchSubject.pipe( this.searchSubject
debounceTime(300), .pipe(debounceTime(300), distinctUntilChanged())
distinctUntilChanged() .subscribe((searchTerm) => {
).subscribe(searchTerm => {
this.isSearching = true; this.isSearching = true;
setTimeout(() => { setTimeout(() => {
const filteredQualifications = this.allQualifications.filter(qualification => const filteredQualifications = this.allQualifications.filter(
qualification.skill?.toLowerCase().includes(searchTerm) (qualification) =>
qualification.skill?.toLowerCase().includes(searchTerm),
); );
this.qualifications$ = of(filteredQualifications); this.qualifications$ = of(filteredQualifications);
this.isSearching = false; this.isSearching = false;
@ -90,9 +100,11 @@ export class QualificationsComponent implements OnInit {
retry(QualificationsComponent.MAX_RETRIES), retry(QualificationsComponent.MAX_RETRIES),
catchError((error: HttpErrorResponse) => { catchError((error: HttpErrorResponse) => {
console.error('Error fetching qualifications:', error); console.error('Error fetching qualifications:', error);
this.errorHandlerService.showErrorMessage('Failed to load qualifications. Please try again.'); this.errorHandlerService.showErrorMessage(
'Failed to load qualifications. Please try again.',
);
return of([]); return of([]);
}) }),
); );
} }
@ -113,7 +125,7 @@ export class QualificationsComponent implements OnInit {
openEditModal(qualification: Qualification) { openEditModal(qualification: Qualification) {
const dialogRef = this.dialog.open(EditComponent, { const dialogRef = this.dialog.open(EditComponent, {
data: qualification data: qualification,
}); });
dialogRef.afterClosed().subscribe((success: boolean) => { dialogRef.afterClosed().subscribe((success: boolean) => {
@ -125,7 +137,7 @@ export class QualificationsComponent implements OnInit {
openDeleteModal(id: number) { openDeleteModal(id: number) {
const dialogRef = this.dialog.open(DeleteComponent, { const dialogRef = this.dialog.open(DeleteComponent, {
data: id data: id,
}); });
dialogRef.afterClosed().subscribe((success: boolean) => { dialogRef.afterClosed().subscribe((success: boolean) => {
@ -137,7 +149,7 @@ export class QualificationsComponent implements OnInit {
openDetailsModal(qualification: Qualification) { openDetailsModal(qualification: Qualification) {
this.dialog.open(DetailsComponent, { this.dialog.open(DetailsComponent, {
data: qualification data: qualification,
}); });
} }
} }

View File

@ -1,14 +1,15 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {AuthService} from "./auth.service"; import { AuthService } from './auth.service';
import {Router} from "@angular/router"; import { Router } from '@angular/router';
import {KeycloakService} from "keycloak-angular";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class AuthGuardService { export class AuthGuardService {
constructor(public auth: AuthService, public router: Router) { constructor(
} public auth: AuthService,
public router: Router,
) {}
canActivate(): boolean { canActivate(): boolean {
if (!this.auth.isAuthenticated()) { if (!this.auth.isAuthenticated()) {

View File

@ -1,8 +1,8 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import {KeycloakService} from "keycloak-angular"; import { KeycloakService } from 'keycloak-angular';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class AuthService { export class AuthService {
private keycloakService = inject(KeycloakService); private keycloakService = inject(KeycloakService);

View File

@ -1,11 +1,10 @@
import {inject, Injectable} from "@angular/core"; import { inject, Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import {Observable} from "rxjs"; import { Observable } from 'rxjs';
import {Employee} from "../employee/Employee"; import { Employee } from '../employee/Employee';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export default class EmployeeApiService { export default class EmployeeApiService {
private http: HttpClient = inject(HttpClient); private http: HttpClient = inject(HttpClient);
@ -13,22 +12,30 @@ export default class EmployeeApiService {
private static readonly BASE_URL = '/backend'; private static readonly BASE_URL = '/backend';
public getById(id: number): Observable<Employee> { public getById(id: number): Observable<Employee> {
return this.http.get(`${EmployeeApiService.BASE_URL}/employees/${id}`) return this.http.get(`${EmployeeApiService.BASE_URL}/employees/${id}`);
} }
public deleteById(id: number): Observable<Employee> { public deleteById(id: number): Observable<Employee> {
return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`) return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`);
} }
public getAll(): Observable<Employee[]> { public getAll(): Observable<Employee[]> {
return this.http.get<Employee[]>(`${EmployeeApiService.BASE_URL}/employees`) return this.http.get<Employee[]>(
`${EmployeeApiService.BASE_URL}/employees`,
);
} }
public create(employee: Employee) { public create(employee: Employee) {
return this.http.post<Employee>(`${EmployeeApiService.BASE_URL}/employees`, employee) return this.http.post<Employee>(
`${EmployeeApiService.BASE_URL}/employees`,
employee,
);
} }
public update(employee: Employee, id: number) { 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,
);
} }
} }

View File

@ -1,8 +1,8 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import {MatSnackBar} from "@angular/material/snack-bar"; import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class ErrorHandlerService { export class ErrorHandlerService {
private readonly snackBar: MatSnackBar = inject(MatSnackBar); private readonly snackBar: MatSnackBar = inject(MatSnackBar);
@ -12,7 +12,7 @@ export class ErrorHandlerService {
duration: 5000, duration: 5000,
horizontalPosition: 'end', horizontalPosition: 'end',
verticalPosition: 'bottom', verticalPosition: 'bottom',
panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100'] panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100'],
}); });
} }
} }

View File

@ -1,40 +1,53 @@
import {inject, Injectable} from "@angular/core"; import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http"; import { HttpClient } from '@angular/common/http';
import {map, Observable} from "rxjs"; import { Observable } from 'rxjs';
import {Qualification} from "../qualification/Qualification"; import { map } from 'rxjs/operators';
import {Employee} from "../employee/Employee"; import { Qualification } from '../qualification/Qualification';
import { Employee } from '../employee/Employee';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export default class QualificationService { export default class QualificationService {
private http: HttpClient = inject(HttpClient);
private static readonly BASE_URL = '/backend'; private static readonly BASE_URL = '/backend';
public getAll(): Observable<Qualification[]> { private readonly apiUrl = `${QualificationService.BASE_URL}/qualifications`;
return this.http.get<Qualification[]>(`${QualificationService.BASE_URL}/qualifications`).pipe(
map(qualifications => qualifications.sort((a, b) => a.id - b.id))
)
}
public create(data: any) { constructor(private http: HttpClient) {}
return this.http.post(`${QualificationService.BASE_URL}/qualifications`, data)
}
public edit(id: number, data: any) { getAll(): Observable<Qualification[]> {
return this.http.put(`${QualificationService.BASE_URL}/qualifications/${id}`, data) return this.http
} .get<Qualification[]>(this.apiUrl)
public delete(id: number) {
return this.http.delete(`${QualificationService.BASE_URL}/qualifications/${id}`)
}
public findEmployees(id: number): Observable<Employee[]> {
return this.http.get<any>(`${QualificationService.BASE_URL}/qualifications/${id}/employees`)
.pipe( .pipe(
map(response => response.employees) map((qualifications) => qualifications.sort((a, b) => a.id - b.id)),
); );
} }
getById(id: number): Observable<Qualification> {
return this.http.get<Qualification>(`${this.apiUrl}/${id}`);
}
create(qualification: Omit<Qualification, 'id'>): Observable<Qualification> {
return this.http.post<Qualification>(this.apiUrl, qualification);
}
update(
id: number,
qualification: Partial<Qualification>,
): Observable<Qualification> {
return this.http.put<Qualification>(`${this.apiUrl}/${id}`, qualification);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
findEmployees(id: number): Observable<Employee[]> {
interface EmployeeResponse {
employees: Employee[];
}
return this.http
.get<EmployeeResponse>(`${this.apiUrl}/${id}/employees`)
.pipe(map((response) => response.employees));
}
} }

View File

@ -1,13 +1,19 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>Lf10StarterNew</title> <title>Lf10StarterNew</title>
<base href="/"> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
</head> </head>
<body class="mat-typography bg-white"> <body class="mat-typography bg-white">
<app-root></app-root> <app-root></app-root>

View File

@ -2,5 +2,6 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig) bootstrapApplication(AppComponent, appConfig).catch((err) =>
.catch((err) => console.error(err)); console.error(err),
);

View File

@ -2,5 +2,11 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html, body { height: 100%; } html,
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}

View File

@ -1,11 +1,8 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: [ content: ["./src/**/*.{html,ts,css,scss,sass,less,styl}"],
'./src/**/*.{html,ts,css,scss,sass,less,styl}',
],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
} };

View File

@ -6,10 +6,6 @@
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [] "types": []
}, },
"files": [ "files": ["src/main.ts"],
"src/main.ts" "include": ["src/**/*.d.ts"]
],
"include": [
"src/**/*.d.ts"
]
} }

View File

@ -19,10 +19,7 @@
"importHelpers": true, "importHelpers": true,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "ES2022",
"lib": [ "lib": ["ES2022", "dom"]
"ES2022",
"dom"
]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,

View File

@ -4,12 +4,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": ["jasmine"]
"jasmine"
]
}, },
"include": [ "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
} }