format code and improve readability across files (#47)

Reviewed-on: http://git.simonis.lol/angular/ems-frontend/pulls/47
Co-authored-by: Jan-Marlon Leibl <jleibl@proton.me>
Co-committed-by: Jan-Marlon Leibl <jleibl@proton.me>
This commit is contained in:
Hop In, I Have Puppies AND WiFi 2025-01-23 11:24:38 +00:00 committed by Hop In, I Have Puppies AND WiFi
parent 26ba6b1054
commit 0be39d98ec
48 changed files with 1300 additions and 952 deletions

View File

@ -1,11 +1,9 @@
# Starter für das LF10 Projekt
## Requirements
* Docker https://docs.docker.com/get-docker/
* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
- Docker https://docs.docker.com/get-docker/
- Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
### Abhängigkeiten starten (Postgres, EmployeeBackend)
@ -37,7 +35,7 @@ http://localhost:8089/swagger
# Postgres
```
````
### Intellij-Ansicht für Postgres Datenbank einrichten (geht nicht in Webstorm!)
@ -51,7 +49,7 @@ http://localhost:8089/swagger
7. Username lf10_starter und Passwort secret eintragen (siehe application.properties), mit Apply bestätigen
8. im Reiter Schemas alle Häkchen entfernen und lediglich vor lf10_starter_db und public Häkchen setzen
9. mit Apply und ok bestätigen
```
````
# Keycloak
@ -80,57 +78,54 @@ die ClientId deines Angular Frontends lautet: employee-management-service-fronte
Hier ein Beispiel einer app.config.ts mit der Konfiguration für Keycloak. Mit dem KeycloakService, der hier definiert wird, kannst du in einem AuthGuard z.B. feststellen, ob der Benutzer eingeloggt ist oder nicht oder ihn mit keycloakService.login() zum Login weiterleiten.
```typescript
import {APP_INITIALIZER, ApplicationConfig} from '@angular/core';
import { provideRouter } from '@angular/router';
import { APP_INITIALIZER, ApplicationConfig } from "@angular/core";
import { provideRouter } from "@angular/router";
import { routes } from './app.routes';
import {KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular";
import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http";
import { routes } from "./app.routes";
import { KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService } from "keycloak-angular";
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http";
export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
keycloak.init({
config: {
url: 'KEYCLOAK_URL',
realm: 'REALM',
clientId: 'CLIENT_ID',
url: "KEYCLOAK_URL",
realm: "REALM",
clientId: "CLIENT_ID",
},
loadUserProfileAtStartUp: true,
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/silent-check-sso.html',
onLoad: "check-sso",
silentCheckSsoRedirectUri: window.location.origin + "/silent-check-sso.html",
checkLoginIframe: false,
redirectUri: 'http://localhost:4200',
redirectUri: "http://localhost:4200",
},
});
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
return () => initializeKeycloak(keycloak)();
}
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes),
providers: [
provideRouter(routes),
KeycloakAngularModule,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
multi: true,
deps: [KeycloakService]
deps: [KeycloakService],
},
KeycloakService,
provideHttpClient(withInterceptorsFromDi()),
{
provide: HTTP_INTERCEPTORS,
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.
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
Trage hier die Features ein, die nicht funktionieren. Beschreibe den jeweiligen Fehler.

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<html>
<body>
<script>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</script>
</body>
</html>

View File

@ -1,5 +1,4 @@
<main class="container mx-auto px-4 py-8 max-w-6xl">
<h1 class="text-3xl font-bold text-gray-900 mb-8">{{ title }}</h1>
<router-outlet></router-outlet>
<router-outlet></router-outlet>
</main>

View File

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

View File

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

View File

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

View File

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

View File

@ -5,74 +5,98 @@
<div class="flex gap-x-4">
<mat-form-field class="!w-full">
<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-error *ngIf="errors['firstName']">{{errors['firstName']}}</mat-error>
<mat-error *ngIf="errors['firstName']">{{
errors["firstName"]
}}</mat-error>
</mat-form-field>
<mat-form-field class="!w-full">
<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-error *ngIf="errors['lastName']">{{errors['lastName']}}</mat-error>
<mat-error *ngIf="errors['lastName']">{{
errors["lastName"]
}}</mat-error>
</mat-form-field>
</div>
<mat-form-field class="!w-full">
<mat-label>Street</mat-label>
<input matInput formControlName="street" required>
<input matInput formControlName="street" required />
<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>
<div class="flex gap-x-4">
<mat-form-field class="!w-full">
<mat-label>City</mat-label>
<input matInput formControlName="city" required>
<input matInput formControlName="city" required />
<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 class="!w-1/2">
<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-error *ngIf="errors['postcode']">{{errors['postcode']}}</mat-error>
<mat-error *ngIf="errors['postcode']">{{
errors["postcode"]
}}</mat-error>
</mat-form-field>
</div>
<mat-form-field class="!w-full">
<mat-label>Phone</mat-label>
<input matInput formControlName="phone" required>
<input matInput formControlName="phone" required />
<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 class="!w-full">
<mat-label>Qualifications</mat-label>
<mat-hint>Select the qualifications</mat-hint>
<mat-select formControlName="qualifications" multiple>
<mat-option *ngFor="let qualification of qualifications" [value]="qualification.id">
{{qualification.skill}}
<mat-option
*ngFor="let qualification of qualifications"
[value]="qualification.id"
>
{{ qualification.skill }}
</mat-option>
</mat-select>
<mat-error *ngIf="errors['qualifications']">{{errors['qualifications']}}</mat-error>
<mat-error *ngIf="errors['qualifications']">{{
errors["qualifications"]
}}</mat-error>
</mat-form-field>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
<button mat-button
<mat-dialog-actions
align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
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
</button>
<button mat-flat-button
<button
mat-flat-button
color="primary"
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
</button>
</mat-dialog-actions>
</div>
</form>
</mat-dialog-content>

View File

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

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">
<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="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>
<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 class="text-gray-600 mt-1 text-xs md:text-sm">This action cannot be undone.</p>
</div>
</div>
</div>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
<button mat-button
<mat-dialog-actions
align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
[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
</button>
<button mat-flat-button
<button
mat-flat-button
color="warn"
(click)="deleteEmployee(employee.id ?? 0)"
mat-dialog-close
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
cdkFocusInitial>
cdkFocusInitial
>
Delete
</button>
</mat-dialog-actions>

View File

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

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">
<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 class="h-20 w-20 sm:h-16 sm:w-16 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
<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
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">
{{ employee.firstName?.charAt(0) ?? '' }}{{ employee.lastName?.charAt(0) ?? '' }}
{{ employee.firstName?.charAt(0) ?? ""
}}{{ employee.lastName?.charAt(0) ?? "" }}
</span>
</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>
</div>
</div>
<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="space-y-1">
<p class="text-sm text-gray-500">Phone</p>
@ -45,11 +56,15 @@
</div>
<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">
@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">
{{skill.skill}}
<div
class="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm font-medium"
>
{{ skill.skill }}
</div>
} @empty {
<p class="text-gray-500 italic">No qualifications added</p>
@ -60,9 +75,11 @@
</mat-dialog-content>
<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()"
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
</button>
</mat-dialog-actions>

View File

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

View File

@ -5,74 +5,98 @@
<div class="flex gap-x-4">
<mat-form-field class="!w-full">
<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-error *ngIf="errors['firstName']">{{errors['firstName']}}</mat-error>
<mat-error *ngIf="errors['firstName']">{{
errors["firstName"]
}}</mat-error>
</mat-form-field>
<mat-form-field class="!w-full">
<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-error *ngIf="errors['lastName']">{{errors['lastName']}}</mat-error>
<mat-error *ngIf="errors['lastName']">{{
errors["lastName"]
}}</mat-error>
</mat-form-field>
</div>
<mat-form-field class="!w-full">
<mat-label>Street</mat-label>
<input matInput formControlName="street" required>
<input matInput formControlName="street" required />
<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>
<div class="flex gap-x-4">
<mat-form-field class="!w-full">
<mat-label>City</mat-label>
<input matInput formControlName="city" required>
<input matInput formControlName="city" required />
<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 class="!w-1/2">
<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-error *ngIf="errors['postcode']">{{errors['postcode']}}</mat-error>
<mat-error *ngIf="errors['postcode']">{{
errors["postcode"]
}}</mat-error>
</mat-form-field>
</div>
<mat-form-field class="!w-full">
<mat-label>Phone</mat-label>
<input matInput formControlName="phone" required>
<input matInput formControlName="phone" required />
<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 class="!w-full">
<mat-label>Qualifications</mat-label>
<mat-select formControlName="qualifications" multiple>
<mat-option *ngFor="let qualification of qualifications" [value]="qualification.id">
{{qualification.skill}}
<mat-option
*ngFor="let qualification of qualifications"
[value]="qualification.id"
>
{{ qualification.skill }}
</mat-option>
</mat-select>
<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-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
<button mat-button
<mat-dialog-actions
align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
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
</button>
<button mat-flat-button
<button
mat-flat-button
color="primary"
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
</button>
</mat-dialog-actions>
</div>
</form>
</mat-dialog-content>

View File

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

View File

@ -4,23 +4,37 @@
<div class="!space-y-6">
<div class="!flex !justify-between !items-center">
<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-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<input matInput
<input
matInput
type="text"
placeholder="Search employees..."
(keyup)="filterEmployees($event)">
<div matSuffix class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center">
<mat-progress-spinner [diameter]="20" mode="indeterminate"
(keyup)="filterEmployees($event)"
/>
<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-100]="isSearching"
class="!transition-opacity"></mat-progress-spinner>
class="!transition-opacity"
></mat-progress-spinner>
</div>
</mat-form-field>
</div>
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white !shrink-0"
(click)="showCreateEmployeeModal()">
<button
mat-flat-button
color="primary"
class="!bg-blue-600 !text-white !shrink-0"
(click)="showCreateEmployeeModal()"
>
<mat-icon class="!mr-2">add</mat-icon>
Add Employee
</button>
@ -30,34 +44,64 @@
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
<table mat-table [dataSource]="employees" matSort class="!w-full">
<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">
<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">
{{ employee.firstName[0] }}{{ employee.lastName[0] }}
</span>
</div>
<div>
<a class="!text-blue-600 hover:!underline cursor-pointer"
[matTooltip]="'Click to view Employee details'" (click)="openDetailModal(employee)">
<button
class="text-blue-600 hover:underline cursor-pointer"
[matTooltip]="'View Employee details'"
(click)="openDetailModal(employee)"
(keydown.enter)="openDetailModal(employee)"
>
{{ employee.lastName }}, {{ employee.firstName }}
</a>
</button>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="!text-right !w-[120px]"> Actions</th>
<td mat-cell *matCellDef="let employee" class="!text-right !py-4 !whitespace-nowrap">
<th
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">
<button mat-icon-button color="primary" [matTooltip]="'Edit employee'"
(click)="showEditEmployeeModal(employee)">
<button
mat-icon-button
color="primary"
[matTooltip]="'Edit employee'"
(click)="showEditEmployeeModal(employee)"
>
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" [matTooltip]="'Delete employee'"
(click)="openDeleteDialogue(employee)">
<button
mat-icon-button
color="warn"
[matTooltip]="'Delete employee'"
(click)="openDeleteDialogue(employee)"
>
<mat-icon>delete</mat-icon>
</button>
</div>
@ -65,7 +109,7 @@
</ng-container>
<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>
</div>
} @else {
@ -98,10 +142,16 @@
} @error {
<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">
<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>
<p class="text-gray-800 font-medium text-sm md:text-base">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>
<p class="text-gray-800 font-medium text-sm md:text-base">
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>

View File

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

View File

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

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

View File

@ -1,4 +1,6 @@
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">
<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">
@if (apiErrorMessage) {
<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">
<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>
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error creating the qualification.</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiErrorMessage }}</p>
<p class="text-gray-800 font-medium text-sm md:text-base">
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>
@ -18,27 +33,36 @@
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3">
<mat-form-field class="w-full">
<mat-label>Skill</mat-label>
<input matInput
<input
matInput
formControlName="skill"
placeholder="Enter skill name"
required>
required
/>
<mat-hint class="text-sm">Enter the skill name</mat-hint>
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
{{ getErrorMessage('skill') }}
{{ getErrorMessage("skill") }}
</mat-error>
</mat-form-field>
</div>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
<button mat-button
<mat-dialog-actions
align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
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
</button>
<button mat-flat-button
<button
mat-flat-button
color="primary"
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
</button>
</mat-dialog-actions>

View File

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

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">
<div class="w-full min-w-[280px] md:min-w-[400px] space-y-4 md:space-y-6">
@if (apiError) {
<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">
<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>
<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>
</div>
</div>
@ -16,26 +25,38 @@
<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">
<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>
<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-600 mt-1 text-xs md:text-sm">This action cannot be undone.</p>
<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-600 mt-1 text-xs md:text-sm">
This action cannot be undone.
</p>
</div>
</div>
</div>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
<button mat-button
<mat-dialog-actions
align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
(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
</button>
<button mat-flat-button
<button
mat-flat-button
color="warn"
(click)="delete()"
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
</button>
</mat-dialog-actions>

View File

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

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
</h2>
@ -7,25 +10,37 @@
@if (employees$ | async; as employees) {
@if (employees.length === 0) {
<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>
<p class="text-gray-600 text-sm md:text-base">No employees found with this qualification.</p>
<mat-icon class="text-gray-400 text-xl md:text-2xl mb-2 !w-8 !h-8"
>person_off</mat-icon
>
<p class="text-gray-600 text-sm md:text-base">
No employees found with this qualification.
</p>
</div>
} @else {
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-2">
@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"
(click)="openEmployeeDetailsModal(employee.id)">
<button
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="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">
{{ employee.firstName?.charAt(0) }}{{ employee.lastName?.charAt(0) }}
{{ employee.firstName?.charAt(0)
}}{{ employee.lastName?.charAt(0) }}
</span>
</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>
</a>
</button>
}
</div>
}
@ -34,9 +49,11 @@
</mat-dialog-content>
<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()"
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
</button>
</mat-dialog-actions>

View File

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

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">
<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">
@if (apiErrorMessage) {
<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">
<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>
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error editing the qualification.</p>
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiErrorMessage }}</p>
<p class="text-gray-800 font-medium text-sm md:text-base">
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>
@ -18,27 +33,36 @@
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3">
<mat-form-field class="w-full">
<mat-label>Skill</mat-label>
<input matInput
<input
matInput
formControlName="skill"
placeholder="Enter skill name"
required>
required
/>
<mat-hint class="text-sm">Enter the skill name</mat-hint>
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
{{ getErrorMessage('skill') }}
{{ getErrorMessage("skill") }}
</mat-error>
</mat-form-field>
</div>
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
<button mat-button
<mat-dialog-actions
align="end"
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
>
<button
mat-button
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
</button>
<button mat-flat-button
<button
mat-flat-button
color="primary"
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
</button>
</mat-dialog-actions>

View File

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

View File

@ -4,23 +4,37 @@
<div class="!space-y-6">
<div class="!flex !justify-between !items-center">
<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-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<input matInput
<input
matInput
type="text"
placeholder="Search qualifications..."
(keyup)="filterQualifications($event)">
<div matSuffix class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center">
<mat-progress-spinner [diameter]="20" mode="indeterminate"
(keyup)="filterQualifications($event)"
/>
<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-100]="isSearching"
class="!transition-opacity"></mat-progress-spinner>
class="!transition-opacity"
></mat-progress-spinner>
</div>
</mat-form-field>
</div>
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white !shrink-0"
(click)="openCreateModal()">
<button
mat-flat-button
color="primary"
class="!bg-blue-600 !text-white !shrink-0"
(click)="openCreateModal()"
>
<mat-icon class="!mr-2">add</mat-icon>
Add Qualification
</button>
@ -28,41 +42,73 @@
@if (qualifications) {
<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">
<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">
<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">
{{ qualification.skill[0]?.toUpperCase() }}
</span>
</div>
<div>
<a class="!text-blue-600 hover:!underline cursor-pointer"
[matTooltip]="'Click to view qualification details'"
(click)="openDetailsModal(qualification)">
<button
class="text-blue-600 hover:underline cursor-pointer"
[matTooltip]="'View qualification details'"
(click)="openDetailsModal(qualification)"
(keydown.enter)="openDetailsModal(qualification)"
>
{{ qualification.skill }}
</a>
</button>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="!text-right !w-[120px]">Actions</th>
<td mat-cell *matCellDef="let qualification" class="!text-right !py-4 !whitespace-nowrap">
<th
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">
<button mat-icon-button
<button
mat-icon-button
color="primary"
[matTooltip]="'Edit qualification'"
(click)="openEditModal(qualification)">
(click)="openEditModal(qualification)"
(keydown.enter)="openEditModal(qualification)"
>
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button
<button
mat-icon-button
color="warn"
[matTooltip]="'Delete qualification'"
(click)="openDeleteModal(qualification.id)">
(click)="openDeleteModal(qualification.id)"
(keydown.enter)="openDeleteModal(qualification.id)"
>
<mat-icon>delete</mat-icon>
</button>
</div>
@ -70,7 +116,7 @@
</ng-container>
<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>
</div>
} @else {
@ -103,10 +149,16 @@
} @error {
<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">
<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>
<p class="text-gray-800 font-medium text-sm md:text-base">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>
<p class="text-gray-800 font-medium text-sm md:text-base">
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>

View File

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

View File

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

View File

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

View File

@ -1,11 +1,10 @@
import {inject, Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {Employee} from "../employee/Employee";
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Employee } from '../employee/Employee';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export default class EmployeeApiService {
private http: HttpClient = inject(HttpClient);
@ -13,22 +12,30 @@ export default class EmployeeApiService {
private static readonly BASE_URL = '/backend';
public getById(id: number): Observable<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> {
return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`)
return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`);
}
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) {
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) {
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 {MatSnackBar} from "@angular/material/snack-bar";
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ErrorHandlerService {
private readonly snackBar: MatSnackBar = inject(MatSnackBar);
@ -12,7 +12,7 @@ export class ErrorHandlerService {
duration: 5000,
horizontalPosition: 'end',
verticalPosition: 'bottom',
panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100']
panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100'],
});
}
}

View File

@ -1,40 +1,53 @@
import {inject, Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {map, Observable} from "rxjs";
import {Qualification} from "../qualification/Qualification";
import {Employee} from "../employee/Employee";
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Qualification } from '../qualification/Qualification';
import { Employee } from '../employee/Employee';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export default class QualificationService {
private http: HttpClient = inject(HttpClient);
private static readonly BASE_URL = '/backend';
public getAll(): Observable<Qualification[]> {
return this.http.get<Qualification[]>(`${QualificationService.BASE_URL}/qualifications`).pipe(
map(qualifications => qualifications.sort((a, b) => a.id - b.id))
)
}
private readonly apiUrl = `${QualificationService.BASE_URL}/qualifications`;
public create(data: any) {
return this.http.post(`${QualificationService.BASE_URL}/qualifications`, data)
}
constructor(private http: HttpClient) {}
public edit(id: number, data: any) {
return this.http.put(`${QualificationService.BASE_URL}/qualifications/${id}`, data)
}
public delete(id: number) {
return this.http.delete(`${QualificationService.BASE_URL}/qualifications/${id}`)
}
public findEmployees(id: number): Observable<Employee[]> {
return this.http.get<any>(`${QualificationService.BASE_URL}/qualifications/${id}/employees`)
getAll(): Observable<Qualification[]> {
return this.http
.get<Qualification[]>(this.apiUrl)
.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,15 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<head>
<meta charset="utf-8" />
<title>Lf10StarterNew</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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 href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography bg-white">
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
</head>
<body class="mat-typography bg-white">
<app-root></app-root>
</body>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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