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
Showing only changes of commit 9ba377b0c8 - Show all commits

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),
KeycloakAngularModule,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
multi: true,
deps: [KeycloakService]
},
KeycloakService,
provideHttpClient(withInterceptorsFromDi()),
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true
}
]
};
providers: [
provideRouter(routes),
KeycloakAngularModule,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
multi: true,
deps: [KeycloakService],
},
KeycloakService,
provideHttpClient(withInterceptorsFromDi()),
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
},
],
};
```
Der Benutzer, mit dem ihr eure Integration testen könnt, hat den Benutzernamen user und das Passwort test. Die einzige Rolle heißt user.
Des Weiteren ist der Client mit der Bezeichnung employee-management-service-frontend wie folgt konfiguriert:
@ -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>
parent.postMessage(location.href, location.origin);
</script>
</body>
<body>
<script>
parent.postMessage(location.href, location.origin);
</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-close
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1">
<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"
>
Cancel
</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">
<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"
>
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-close]="false"
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1">
<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"
>
Cancel
</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>
<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
>
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
(click)="closeModal()"
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full">
<button
mat-button
(click)="closeModal()"
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-close
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1">
<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"
>
Cancel
</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">
<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"
>
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

@ -1,113 +1,163 @@
<section class="!space-y-6 mb-6">
@defer {
@if (employees$ | async; as employees) {
<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>
<mat-form-field class="!m-0" subscriptSizing="dynamic">
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<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"
[class.!opacity-0]="!isSearching"
[class.!opacity-100]="isSearching"
class="!transition-opacity"></mat-progress-spinner>
@if (employees$ | async; as employees) {
<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>
<mat-form-field class="!m-0" subscriptSizing="dynamic">
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<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"
[class.!opacity-0]="!isSearching"
[class.!opacity-100]="isSearching"
class="!transition-opacity"
></mat-progress-spinner>
</div>
</mat-form-field>
</div>
</mat-form-field>
<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>
</div>
@if (employees) {
<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>
<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"
>
<span class="!text-blue-600 !font-medium">
{{ employee.firstName[0] }}{{ employee.lastName[0] }}
</span>
</div>
<div>
<button
class="text-blue-600 hover:underline cursor-pointer"
[matTooltip]="'View Employee details'"
(click)="openDetailModal(employee)"
(keydown.enter)="openDetailModal(employee)"
>
{{ employee.lastName }}, {{ employee.firstName }}
</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"
>
<div class="!flex !justify-end !items-center !gap-1">
<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)"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
} @else {
<mat-card class="!text-center !py-8">
<mat-card-content>
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">people</mat-icon>
<p class="!text-gray-600">No employees found</p>
</mat-card-content>
</mat-card>
}
</div>
<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>
</div>
@if (employees) {
<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>
<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">
<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)">
{{ employee.lastName }}, {{ employee.firstName }}
</a>
</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">
<div class="!flex !justify-end !items-center !gap-1">
<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)">
<mat-icon>delete</mat-icon>
</button>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
} @else {
<mat-card class="!text-center !py-8">
<mat-card-content>
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">people</mat-icon>
<p class="!text-gray-600">No employees found</p>
</mat-card-content>
</mat-card>
}
</div>
}
} @placeholder {
<div class="!space-y-6">
<div class="!animate-pulse">
<div class="!flex !justify-between !items-center !mb-8">
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
</div>
<div class="!bg-gray-50 !p-4 !rounded-lg">
<div class="!space-y-4">
<div class="!h-10 !bg-gray-200 !rounded"></div>
@for (i of [1, 2, 3]; track i) {
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
}
<div class="!space-y-6">
<div class="!animate-pulse">
<div class="!flex !justify-between !items-center !mb-8">
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
</div>
<div class="!bg-gray-50 !p-4 !rounded-lg">
<div class="!space-y-4">
<div class="!h-10 !bg-gray-200 !rounded"></div>
@for (i of [1, 2, 3]; track i) {
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
}
</div>
</div>
</div>
</div>
</div>
} @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>
<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>
<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
>
<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>
</div>
</div>
</div>
</div>
} @loading {
<div class="!flex !justify-center !items-center !py-12">
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
</div>
<div class="!flex !justify-center !items-center !py-12">
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
</div>
}
</section>

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,27 +73,27 @@ export class TableComponent implements OnInit {
}
private loadEmployees(): void {
this.fetchEmployees().subscribe(employees => {
this.fetchEmployees().subscribe((employees) => {
this.allEmployees = employees;
this.employees$ = of(employees);
});
}
private setupSearch(): void {
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(searchTerm => {
this.isSearching = true;
setTimeout(() => {
const filteredEmployees = this.allEmployees.filter(employee =>
employee.firstName?.toLowerCase().includes(searchTerm) ||
employee.lastName?.toLowerCase().includes(searchTerm)
);
this.employees$ = of(filteredEmployees);
this.isSearching = false;
}, 150);
});
this.searchSubject
.pipe(debounceTime(300), distinctUntilChanged())
.subscribe((searchTerm) => {
this.isSearching = true;
setTimeout(() => {
const filteredEmployees = this.allEmployees.filter(
(employee) =>
employee.firstName?.toLowerCase().includes(searchTerm) ||
employee.lastName?.toLowerCase().includes(searchTerm),
);
this.employees$ = of(filteredEmployees);
this.isSearching = false;
}, 150);
});
}
private fetchEmployees(): Observable<Employee[]> {
@ -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
formControlName="skill"
placeholder="Enter skill name"
required>
<input
matInput
formControlName="skill"
placeholder="Enter skill name"
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-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">
<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"
>
Cancel
</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">
<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"
>
Create
</button>
</mat-dialog-actions>

View File

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

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
(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">
<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"
>
Cancel
</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>
<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
>
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
(click)="closeModal()"
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full">
<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"
>
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-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">
<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"
>
Cancel
</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">
<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"
>
Save Changes
</button>
</mat-dialog-actions>

View File

@ -1,58 +1,70 @@
import {Component, inject} from '@angular/core';
import {FormBuilder, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
import QualificationService from "../../services/qualification.service";
import { Component, inject } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions, MatDialogClose,
MatDialogContent,
MatDialogRef,
MatDialogTitle
} from "@angular/material/dialog";
import {MatButton} from "@angular/material/button";
import {MatError, MatFormField, MatHint, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {NgIf} from "@angular/common";
import {Qualification} from "../Qualification";
import {MatIcon} from "@angular/material/icon";
FormBuilder,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import QualificationService from '../../services/qualification.service';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { MatButton } from '@angular/material/button';
import {
MatError,
MatFormField,
MatHint,
MatLabel,
} from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { NgIf } from '@angular/common';
import { Qualification } from '../Qualification';
import { MatIcon } from '@angular/material/icon';
@Component({
selector: 'app-edit-qualification',
imports: [
FormsModule,
MatButton,
MatDialogActions,
MatDialogContent,
MatDialogTitle,
MatError,
MatFormField,
MatInput,
MatLabel,
NgIf,
ReactiveFormsModule,
MatDialogClose,
MatHint,
MatIcon
],
imports: [
FormsModule,
MatButton,
MatDialogActions,
MatDialogContent,
MatDialogTitle,
MatError,
MatFormField,
MatInput,
MatLabel,
NgIf,
ReactiveFormsModule,
MatDialogClose,
MatHint,
MatIcon,
],
templateUrl: './edit.component.html',
styleUrl: './edit.component.css'
styleUrl: './edit.component.css',
})
export class EditComponent {
public apiErrorMessage: string = '';
public apiErrorMessage = '';
public qualification: Qualification = inject(MAT_DIALOG_DATA);
private formBuilder: FormBuilder = inject(FormBuilder);
private qualificationService: QualificationService = inject(QualificationService);
private qualificationService: QualificationService =
inject(QualificationService);
private dialogRef: MatDialogRef<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({
next: (editedQualification) => {
this.dialogRef.close(editedQualification);
},
error: (error) => {
console.error('Error creating qualification:', error);
const qualification = {
skill: this.qualificationForm.value.skill || '',
};
this.apiErrorMessage = 'API Error';
}
});
this.qualificationService
.update(this.qualification.id, qualification)
.subscribe({
next: (editedQualification) => {
this.dialogRef.close(editedQualification);
},
error: (error) => {
console.error('Error updating qualification:', error);
this.apiErrorMessage = 'API Error';
},
});
}
}

View File

@ -1,118 +1,170 @@
<section class="!space-y-6 mb-6">
@defer {
@if (qualifications$ | async; as qualifications) {
<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>
<mat-form-field class="!m-0" subscriptSizing="dynamic">
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<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"
[class.!opacity-0]="!isSearching"
[class.!opacity-100]="isSearching"
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()">
<mat-icon class="!mr-2">add</mat-icon>
Add Qualification
</button>
</div>
@if (qualifications) {
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
<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>
<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">
<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)">
{{ qualification.skill }}
</a>
</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">
<div class="!flex !justify-end !items-center !gap-1">
<button mat-icon-button
color="primary"
[matTooltip]="'Edit qualification'"
(click)="openEditModal(qualification)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button
color="warn"
[matTooltip]="'Delete qualification'"
(click)="openDeleteModal(qualification.id)">
<mat-icon>delete</mat-icon>
</button>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
} @else {
<mat-card class="!text-center !py-8">
<mat-card-content>
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">school</mat-icon>
<p class="!text-gray-600">No qualifications found</p>
</mat-card-content>
</mat-card>
}
</div>
}
} @placeholder {
<div class="!space-y-6">
<div class="!animate-pulse">
<div class="!flex !justify-between !items-center !mb-8">
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
</div>
<div class="!bg-gray-50 !p-4 !rounded-lg">
<div class="!space-y-4">
<div class="!h-10 !bg-gray-200 !rounded"></div>
@for (i of [1, 2, 3]; track i) {
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
}
</div>
</div>
</div>
</div>
} @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>
<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>
@defer {
@if (qualifications$ | async; as qualifications) {
<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>
<mat-form-field class="!m-0" subscriptSizing="dynamic">
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
<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"
[class.!opacity-0]="!isSearching"
[class.!opacity-100]="isSearching"
class="!transition-opacity"
></mat-progress-spinner>
</div>
</div>
</mat-form-field>
</div>
} @loading {
<div class="!flex !justify-center !items-center !py-12">
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
<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>
</div>
@if (qualifications) {
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
<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>
<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"
>
<span class="!text-blue-600 !font-medium">
{{ qualification.skill[0]?.toUpperCase() }}
</span>
</div>
<div>
<button
class="text-blue-600 hover:underline cursor-pointer"
[matTooltip]="'View qualification details'"
(click)="openDetailsModal(qualification)"
(keydown.enter)="openDetailsModal(qualification)"
>
{{ qualification.skill }}
</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"
>
<div class="!flex !justify-end !items-center !gap-1">
<button
mat-icon-button
color="primary"
[matTooltip]="'Edit qualification'"
(click)="openEditModal(qualification)"
(keydown.enter)="openEditModal(qualification)"
>
<mat-icon>edit</mat-icon>
</button>
<button
mat-icon-button
color="warn"
[matTooltip]="'Delete qualification'"
(click)="openDeleteModal(qualification.id)"
(keydown.enter)="openDeleteModal(qualification.id)"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
} @else {
<mat-card class="!text-center !py-8">
<mat-card-content>
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">school</mat-icon>
<p class="!text-gray-600">No qualifications found</p>
</mat-card-content>
</mat-card>
}
</div>
}
} @placeholder {
<div class="!space-y-6">
<div class="!animate-pulse">
<div class="!flex !justify-between !items-center !mb-8">
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
</div>
<div class="!bg-gray-50 !p-4 !rounded-lg">
<div class="!space-y-4">
<div class="!h-10 !bg-gray-200 !rounded"></div>
@for (i of [1, 2, 3]; track i) {
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
}
</div>
</div>
</div>
</div>
} @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
>
<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>
</div>
</div>
</div>
} @loading {
<div class="!flex !justify-center !items-center !py-12">
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
</div>
}
</section>

View File

@ -1,143 +1,155 @@
import {Component, inject, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common';
import {catchError, debounceTime, distinctUntilChanged, Observable, of, retry, Subject} from 'rxjs';
import {HttpErrorResponse} from '@angular/common/http';
import {Qualification} from "../Qualification";
import {MatDialog} from "@angular/material/dialog";
import QualificationService from "../../services/qualification.service";
import {CreateComponent} from "../create/create.component";
import {EditComponent} from "../edit/edit.component";
import {DeleteComponent} from "../delete/delete.component";
import {MatCardModule} from '@angular/material/card';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatDividerModule} from '@angular/material/divider';
import {MatTooltipModule} from '@angular/material/tooltip';
import {MatMenuModule} from '@angular/material/menu';
import {MatTableModule} from '@angular/material/table';
import {MatSortModule} from '@angular/material/sort';
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {DetailsComponent} from "../details/details.component";
import {ErrorHandlerService} from "../../services/error.handler.service";
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
catchError,
debounceTime,
distinctUntilChanged,
Observable,
of,
retry,
Subject,
} from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { Qualification } from '../Qualification';
import { MatDialog } from '@angular/material/dialog';
import QualificationService from '../../services/qualification.service';
import { CreateComponent } from '../create/create.component';
import { EditComponent } from '../edit/edit.component';
import { DeleteComponent } from '../delete/delete.component';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDividerModule } from '@angular/material/divider';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { DetailsComponent } from '../details/details.component';
import { ErrorHandlerService } from '../../services/error.handler.service';
@Component({
selector: 'app-qualifications',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDividerModule,
MatTooltipModule,
MatMenuModule,
MatTableModule,
MatSortModule,
MatFormFieldModule,
MatInputModule,
],
templateUrl: './table.component.html',
styleUrl: './table.component.css'
selector: 'app-qualifications',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatDividerModule,
MatTooltipModule,
MatMenuModule,
MatTableModule,
MatSortModule,
MatFormFieldModule,
MatInputModule,
],
templateUrl: './table.component.html',
styleUrl: './table.component.css',
})
export class QualificationsComponent implements OnInit {
private readonly qualificationService: QualificationService = inject(QualificationService);
private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService);
private readonly dialog: MatDialog = inject(MatDialog);
private readonly qualificationService: QualificationService =
inject(QualificationService);
private readonly errorHandlerService: ErrorHandlerService =
inject(ErrorHandlerService);
private readonly dialog: MatDialog = inject(MatDialog);
private static readonly MAX_RETRIES = 3;
private static readonly MAX_RETRIES = 3;
private allQualifications: Qualification[] = [];
private searchSubject = new Subject<string>();
public qualifications$: Observable<Qualification[]> = of([]);
public isSearching = false;
public readonly displayedColumns: string[] = ['skill', 'actions'];
private allQualifications: Qualification[] = [];
private searchSubject = new Subject<string>();
public qualifications$: Observable<Qualification[]> = of([]);
public isSearching = false;
public readonly displayedColumns: string[] = ['skill', 'actions'];
ngOnInit() {
this.loadQualifications();
this.setupSearch();
}
ngOnInit() {
this.loadQualifications();
this.setupSearch();
}
private loadQualifications(): void {
this.fetchQualifications().subscribe(qualifications => {
this.allQualifications = qualifications;
this.qualifications$ = of(qualifications);
});
}
private loadQualifications(): void {
this.fetchQualifications().subscribe((qualifications) => {
this.allQualifications = qualifications;
this.qualifications$ = of(qualifications);
});
}
private setupSearch(): void {
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(searchTerm => {
this.isSearching = true;
setTimeout(() => {
const filteredQualifications = this.allQualifications.filter(qualification =>
qualification.skill?.toLowerCase().includes(searchTerm)
);
this.qualifications$ = of(filteredQualifications);
this.isSearching = false;
}, 150);
});
}
private setupSearch(): void {
this.searchSubject
.pipe(debounceTime(300), distinctUntilChanged())
.subscribe((searchTerm) => {
this.isSearching = true;
setTimeout(() => {
const filteredQualifications = this.allQualifications.filter(
(qualification) =>
qualification.skill?.toLowerCase().includes(searchTerm),
);
this.qualifications$ = of(filteredQualifications);
this.isSearching = false;
}, 150);
});
}
private fetchQualifications(): Observable<Qualification[]> {
return this.qualificationService.getAll().pipe(
retry(QualificationsComponent.MAX_RETRIES),
catchError((error: HttpErrorResponse) => {
console.error('Error fetching qualifications:', error);
this.errorHandlerService.showErrorMessage('Failed to load qualifications. Please try again.');
return of([]);
})
private fetchQualifications(): Observable<Qualification[]> {
return this.qualificationService.getAll().pipe(
retry(QualificationsComponent.MAX_RETRIES),
catchError((error: HttpErrorResponse) => {
console.error('Error fetching qualifications:', error);
this.errorHandlerService.showErrorMessage(
'Failed to load qualifications. Please try again.',
);
}
return of([]);
}),
);
}
protected filterQualifications(event: Event): void {
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase();
this.searchSubject.next(searchTerm);
}
protected filterQualifications(event: Event): void {
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase();
this.searchSubject.next(searchTerm);
}
openCreateModal() {
const dialogRef = this.dialog.open(CreateComponent);
openCreateModal() {
const dialogRef = this.dialog.open(CreateComponent);
dialogRef.afterClosed().subscribe((success: boolean) => {
if (success) {
this.loadQualifications();
}
});
}
dialogRef.afterClosed().subscribe((success: boolean) => {
if (success) {
this.loadQualifications();
}
});
}
openEditModal(qualification: Qualification) {
const dialogRef = this.dialog.open(EditComponent, {
data: qualification
});
openEditModal(qualification: Qualification) {
const dialogRef = this.dialog.open(EditComponent, {
data: qualification,
});
dialogRef.afterClosed().subscribe((success: boolean) => {
if (success) {
this.loadQualifications();
}
});
}
dialogRef.afterClosed().subscribe((success: boolean) => {
if (success) {
this.loadQualifications();
}
});
}
openDeleteModal(id: number) {
const dialogRef = this.dialog.open(DeleteComponent, {
data: id
});
openDeleteModal(id: number) {
const dialogRef = this.dialog.open(DeleteComponent, {
data: id,
});
dialogRef.afterClosed().subscribe((success: boolean) => {
if (success) {
this.loadQualifications();
}
});
}
dialogRef.afterClosed().subscribe((success: boolean) => {
if (success) {
this.loadQualifications();
}
});
}
openDetailsModal(qualification: Qualification) {
this.dialog.open(DetailsComponent, {
data: qualification
});
}
openDetailsModal(qualification: Qualification) {
this.dialog.open(DetailsComponent, {
data: qualification,
});
}
}

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">
<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">
<app-root></app-root>
</body>
<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">
<app-root></app-root>
</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"]
}