format code and improve readability across files (#47)

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

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>
</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,2 +1,2 @@
<app-employee-list></app-employee-list>
<app-qualifications></app-qualifications>
<app-employee-list></app-employee-list>
<app-qualifications></app-qualifications>

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,41 +1,62 @@
<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>
</div>
}
<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
formControlName="skill"
<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;
}