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:
committed by
Hop In, I Have Puppies AND WiFi

parent
88d9a1a534
commit
545c6194e4
@ -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>
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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: '' },
|
||||
];
|
||||
|
@ -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[],
|
||||
) {}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -1,2 +1,2 @@
|
||||
<app-employee-list></app-employee-list>
|
||||
<app-qualifications></app-qualifications>
|
||||
<app-employee-list></app-employee-list>
|
||||
<app-qualifications></app-qualifications>
|
||||
|
@ -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 {}
|
||||
|
@ -1 +1,3 @@
|
||||
<div class="dot-loader">Logging in<span>{{ dots }}</span></div>
|
||||
<div class="dot-loader">
|
||||
Logging in<span>{{ dots }}</span>
|
||||
</div>
|
||||
|
@ -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 {
|
||||
|
@ -1,4 +1,6 @@
|
||||
export class Qualification {
|
||||
constructor(public id: number, public skill?: string) {
|
||||
}
|
||||
constructor(
|
||||
public id: number,
|
||||
public skill?: string,
|
||||
) {}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user