Extract snackbar error message implementation to service #43
@ -5,7 +5,7 @@ volumes:
|
||||
|
||||
services:
|
||||
postgres-employee:
|
||||
container_name: postgres_employee
|
||||
container_name: ems-db
|
||||
image: postgres:13.3
|
||||
volumes:
|
||||
- employee_postgres_data:/var/lib/postgresql/data
|
||||
@ -17,9 +17,8 @@ services:
|
||||
- "5432:5432"
|
||||
|
||||
employee:
|
||||
container_name: employee
|
||||
container_name: ems-api
|
||||
image: berndheidemann/employee-management-service:1.1.3
|
||||
# image: berndheidemann/employee-management-service_without_keycloak:1.1
|
||||
environment:
|
||||
spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db
|
||||
spring.datasource.username: employee
|
||||
|
@ -20,8 +20,10 @@
|
||||
"@angular/platform-browser": "^19.0.4",
|
||||
"@angular/platform-browser-dynamic": "^19.0.4",
|
||||
"@angular/router": "^19.0.4",
|
||||
"keycloak-angular": "^16.1.0",
|
||||
"rxjs": "~7.8.1",
|
||||
"tailwind": "4.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
@ -30,12 +32,14 @@
|
||||
"@angular/cli": "^19.0.5",
|
||||
"@angular/compiler-cli": "^19.0.4",
|
||||
"@types/jasmine": "~5.1.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"jasmine-core": "~5.2.0",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"postcss": "^8.4.49",
|
||||
"typescript": "~5.5.4"
|
||||
}
|
||||
}
|
7
public/silent-check-sso.html
Normal file
7
public/silent-check-sso.html
Normal file
@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,10 +0,0 @@
|
||||
export class Employee {
|
||||
constructor(public id?: number,
|
||||
public lastName?: string,
|
||||
public firstName?: string,
|
||||
public street?: string,
|
||||
public postcode?: string,
|
||||
public city?: string,
|
||||
public phone?: string) {
|
||||
}
|
||||
}
|
@ -1,2 +1,5 @@
|
||||
<app-employee-list></app-employee-list>
|
||||
<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>
|
||||
</main>
|
||||
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have the 'lf10StarterNew' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('lf10StarterNew');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, lf10StarterNew');
|
||||
});
|
||||
});
|
@ -1,14 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import {EmployeeListComponent} from "./employee-list/employee-list.component";
|
||||
import {Component} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [CommonModule, EmployeeListComponent],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.css'
|
||||
selector: 'app-root',
|
||||
imports: [CommonModule, RouterOutlet],
|
||||
templateUrl: './app.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './app.component.css'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'lf10StarterNew';
|
||||
title = 'Employee Management System';
|
||||
}
|
||||
|
@ -1,10 +1,50 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
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 { routes } from './app.routes';
|
||||
import {provideHttpClient, withInterceptorsFromDi} from "@angular/common/http";
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
|
||||
keycloak.init({
|
||||
config: {
|
||||
url: 'https://keycloak.szut.dev/auth',
|
||||
realm: 'szut',
|
||||
clientId: 'employee-management-service-frontend',
|
||||
},
|
||||
loadUserProfileAtStartUp: true,
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri:
|
||||
window.location.origin + '/silent-check-sso.html',
|
||||
checkLoginIframe: false,
|
||||
redirectUri: 'http://localhost:4200',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
|
||||
return () => initializeKeycloak(keycloak)();
|
||||
}
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes), provideHttpClient(withInterceptorsFromDi()), provideAnimationsAsync()]
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideAnimationsAsync(),
|
||||
KeycloakAngularModule,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeApp,
|
||||
multi: true,
|
||||
deps: [KeycloakService]
|
||||
},
|
||||
KeycloakService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -1,3 +1,10 @@
|
||||
import { Routes } from '@angular/router';
|
||||
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 = [];
|
||||
export const routes: Routes = [
|
||||
{path: 'login', component: LoginComponent},
|
||||
{path: '', component: HomeComponent, canActivate: [AuthGuardService]},
|
||||
{path: '**', redirectTo: ''}
|
||||
];
|
||||
|
@ -1,9 +0,0 @@
|
||||
<h1>LF10-Starter</h1>
|
||||
Wenn Sie in der EmployeeListComponent.ts ein gültiges Bearer-Token eintragen, sollten hier die Namen der in der Datenbank gespeicherten Mitarbeiter angezeigt werden!
|
||||
<ul>
|
||||
@for(e of employees$ | async; track e.id) {
|
||||
<li>
|
||||
{{e.lastName }}, {{e.firstName}}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
@ -1,23 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EmployeeListComponent } from './employee-list.component';
|
||||
|
||||
describe('EmployeeListComponent', () => {
|
||||
let component: EmployeeListComponent;
|
||||
let fixture: ComponentFixture<EmployeeListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EmployeeListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EmployeeListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,29 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {Observable, of} from "rxjs";
|
||||
import {HttpClient, HttpHeaders} from "@angular/common/http";
|
||||
import {Employee} from "../Employee";
|
||||
|
||||
@Component({
|
||||
selector: 'app-employee-list',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './employee-list.component.html',
|
||||
styleUrl: './employee-list.component.css'
|
||||
})
|
||||
export class EmployeeListComponent {
|
||||
bearer = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzUFQ0dldiNno5MnlQWk1EWnBqT1U0RjFVN0lwNi1ELUlqQWVGczJPbGU0In0.eyJleHAiOjE3MzM5MTQ5MjgsImlhdCI6MTczMzkxMTMyOCwianRpIjoiMjNhYzMwMmUtYmYxNS00OTRmLWJhYTItNjIzODllYWZkMmZhIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5zenV0LmRldi9hdXRoL3JlYWxtcy9zenV0IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU1NDZjZDIxLTk4NTQtNDMyZi1hNDY3LTRkZTNlZWRmNTg4OSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImVtcGxveWVlLW1hbmFnZW1lbnQtc2VydmljZSIsInNlc3Npb25fc3RhdGUiOiI2ODdiMTEwYS00NTRjLTQwMzgtYjBkMS1kZDAzZGQ1N2JiNjEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NDIwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsicHJvZHVjdF9vd25lciIsIm9mZmxpbmVfYWNjZXNzIiwiZGVmYXVsdC1yb2xlcy1zenV0IiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIifQ.E5ir1Z-POpUU_jvTh8CzoMYO74qo_7uQXw7QQBUvXB2_37pT3_tutAq6sM4V5cNBu--fWar5bltlNcOAWd_7Kdb66Qc23i0RR9vPneoSduJAzoD8gtFbx8c7ltNR4pG-c6tdnkGhLLqM621DShaSlH8Shp-Z0-y4Iq3GFdQrAFH1CrRVYlW0qFv1EZsE9BmhW3hJwrR1S2IPiEN6MwhehLflLa_ZgLcF417ocIfK-6gbbRNAwXA-JajFVOZAEVXs-52Ta9Kb_EEQFpRsjXorfflmbizQmgrbhBUB7MTiPYIcRruZSYdfmjcE008PHnut52cTcVYEuOrUCUqY4VmhoQ';
|
||||
employees$: Observable<Employee[]>;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.employees$ = of([]);
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
this.employees$ = this.http.get<Employee[]>('/backend/employees', {
|
||||
headers: new HttpHeaders()
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Authorization', `Bearer ${this.bearer}`)
|
||||
});
|
||||
}
|
||||
}
|
15
src/app/employee/Employee.ts
Normal file
15
src/app/employee/Employee.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {Qualification} from "../qualification/Qualification";
|
||||
|
||||
export class Employee {
|
||||
constructor(
|
||||
public id?: number,
|
||||
public lastName?: string,
|
||||
public firstName?: string,
|
||||
public street?: string,
|
||||
public postcode?: string,
|
||||
public city?: string,
|
||||
public phone?: string,
|
||||
public skillSet?: Qualification[]
|
||||
) {
|
||||
}
|
||||
}
|
78
src/app/employee/create/create.component.html
Normal file
78
src/app/employee/create/create.component.html
Normal file
@ -0,0 +1,78 @@
|
||||
<h2 mat-dialog-title>Create Employee</h2>
|
||||
<mat-dialog-content>
|
||||
<form *ngIf="employeeForm" [formGroup]="employeeForm" (ngSubmit)="submit()">
|
||||
<div class="!space-y-4">
|
||||
<div class="flex gap-x-4">
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<mat-hint>Enter the first name</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter the last name</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter the street address</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter the city</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter postcode</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter the phone number</mat-hint>
|
||||
<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>
|
||||
</mat-select>
|
||||
<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">
|
||||
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">
|
||||
Submit
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</mat-dialog-content>
|
119
src/app/employee/create/create.component.ts
Normal file
119
src/app/employee/create/create.component.ts
Normal file
@ -0,0 +1,119 @@
|
||||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-employee',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatFormField,
|
||||
MatInput,
|
||||
MatButton,
|
||||
MatLabel,
|
||||
MatDialogContent,
|
||||
MatDialogTitle,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
NgIf,
|
||||
MatOption,
|
||||
MatSelect,
|
||||
NgForOf,
|
||||
MatError,
|
||||
MatHint
|
||||
],
|
||||
templateUrl: './create.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './create.component.css'
|
||||
})
|
||||
export class CreateComponent implements OnInit {
|
||||
employeeForm!: FormGroup;
|
||||
employeeService: EmployeeApiService = inject(EmployeeApiService);
|
||||
formBuilder: FormBuilder = inject(FormBuilder);
|
||||
dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef);
|
||||
qualificationService: QualificationService = inject(QualificationService);
|
||||
qualifications: Qualification[] = [];
|
||||
errorMsgs: { [key: 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'
|
||||
}
|
||||
|
||||
errors: { [key: string]: string } = {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadQualifications();
|
||||
this.employeeForm = this.formBuilder.group({
|
||||
firstName: ['', Validators.required],
|
||||
lastName: ['', Validators.required],
|
||||
street: ['', Validators.required],
|
||||
postcode: ['', [Validators.required, this.validatePostcode]],
|
||||
city: ['', Validators.required],
|
||||
phone: ['', Validators.required],
|
||||
qualifications: [[]]
|
||||
});
|
||||
|
||||
Object.keys(this.employeeForm.controls).forEach((controlName: string) => {
|
||||
const control = this.employeeForm.controls[controlName];
|
||||
control.valueChanges.pipe(debounceTime(10)).subscribe(() => {
|
||||
this.showErrorMsg(controlName, control);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadQualifications() {
|
||||
this.qualificationService.getAll().subscribe(
|
||||
qualifications => this.qualifications = qualifications
|
||||
);
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.employeeForm.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.employeeForm.value;
|
||||
const employeeData = {
|
||||
...formValue,
|
||||
skillSet: formValue.qualifications
|
||||
};
|
||||
|
||||
this.employeeService.create(employeeData as Employee).subscribe();
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
showErrorMsg(controlName: string, control: AbstractControl | undefined) {
|
||||
if (control?.errors) {
|
||||
this.errors[controlName] = this.errorMsgs[controlName];
|
||||
}
|
||||
}
|
||||
|
||||
validatePostcode(control: AbstractControl) {
|
||||
const postcode = control.value as number;
|
||||
if (postcode.toString().length !== 5) {
|
||||
return {invalidPostcode: true};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
0
src/app/employee/delete/delete.component.css
Normal file
0
src/app/employee/delete/delete.component.css
Normal file
33
src/app/employee/delete/delete.component.html
Normal file
33
src/app/employee/delete/delete.component.html
Normal file
@ -0,0 +1,33 @@
|
||||
<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>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
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>
|
||||
</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">
|
||||
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>
|
||||
Delete
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</mat-dialog-content>
|
38
src/app/employee/delete/delete.component.ts
Normal file
38
src/app/employee/delete/delete.component.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {Component, Inject, 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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-employee',
|
||||
imports: [
|
||||
MatDialogContent,
|
||||
MatDialogTitle,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatDialogClose,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './delete.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './delete.component.css'
|
||||
})
|
||||
export class DeleteComponent {
|
||||
private apiService: EmployeeApiService = inject(EmployeeApiService);
|
||||
private dialogRef: MatDialogRef<DeleteComponent> = inject(MatDialogRef);
|
||||
protected employee: Employee = inject(MAT_DIALOG_DATA);
|
||||
|
||||
deleteEmployee(id: number) {
|
||||
this.apiService.deleteById(id).subscribe();
|
||||
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
0
src/app/employee/details/details.component.css
Normal file
0
src/app/employee/details/details.component.css
Normal file
68
src/app/employee/details/details.component.html
Normal file
68
src/app/employee/details/details.component.html
Normal file
@ -0,0 +1,68 @@
|
||||
<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">
|
||||
<span class="text-blue-600 text-2xl sm:text-xl font-medium">
|
||||
{{ 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>
|
||||
<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>
|
||||
<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>
|
||||
<p class="text-gray-900 break-all">{{ employee.phone }}</p>
|
||||
</div>
|
||||
</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">Address</h4>
|
||||
<div class="bg-gray-50 p-3 sm:p-4 rounded-lg space-y-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm text-gray-500">Street</p>
|
||||
<p class="text-gray-900 break-words">{{ employee.street }}</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm text-gray-500">City</p>
|
||||
<p class="text-gray-900">{{ employee.city }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm text-gray-500">Postcode</p>
|
||||
<p class="text-gray-900">{{ employee.postcode }}</p>
|
||||
</div>
|
||||
</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">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>
|
||||
} @empty {
|
||||
<p class="text-gray-500 italic">No qualifications added</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
Close
|
||||
</button>
|
||||
</mat-dialog-actions>
|
26
src/app/employee/details/details.component.ts
Normal file
26
src/app/employee/details/details.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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
|
||||
],
|
||||
templateUrl: './details.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './details.component.css'
|
||||
})
|
||||
export class DetailsComponent {
|
||||
employee: Employee = inject(MAT_DIALOG_DATA);
|
||||
dialogRef: DialogRef = inject(DialogRef);
|
||||
|
||||
closeModal() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
0
src/app/employee/edit/edit.component.css
Normal file
0
src/app/employee/edit/edit.component.css
Normal file
78
src/app/employee/edit/edit.component.html
Normal file
78
src/app/employee/edit/edit.component.html
Normal file
@ -0,0 +1,78 @@
|
||||
<h2 mat-dialog-title>Edit Employee</h2>
|
||||
<mat-dialog-content>
|
||||
<form *ngIf="employeeForm" [formGroup]="employeeForm" (ngSubmit)="submit()">
|
||||
<div class="!space-y-4">
|
||||
<div class="flex gap-x-4">
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<mat-hint>Enter the first name</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter the last name</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter the street address</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter the city</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter postcode</mat-hint>
|
||||
<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>
|
||||
<mat-hint>Enter phone number</mat-hint>
|
||||
<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>
|
||||
</mat-select>
|
||||
<mat-hint>Select qualifications</mat-hint>
|
||||
<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">
|
||||
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">
|
||||
Submit
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</mat-dialog-content>
|
126
src/app/employee/edit/edit.component.ts
Normal file
126
src/app/employee/edit/edit.component.ts
Normal file
@ -0,0 +1,126 @@
|
||||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit',
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
MatFormField,
|
||||
MatInput,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatDialogClose,
|
||||
MatLabel,
|
||||
MatSelect,
|
||||
MatOption,
|
||||
NgForOf,
|
||||
MatHint,
|
||||
MatError,
|
||||
],
|
||||
templateUrl: './edit.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './edit.component.css'
|
||||
})
|
||||
export class EditComponent implements OnInit {
|
||||
employeeForm!: FormGroup;
|
||||
formBuilder: FormBuilder = inject(FormBuilder);
|
||||
employeeService: EmployeeApiService = inject(EmployeeApiService);
|
||||
qualificationService: QualificationService = inject(QualificationService);
|
||||
dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef);
|
||||
employee: Employee = inject(MAT_DIALOG_DATA);
|
||||
qualifications: Qualification[] = [];
|
||||
errorMsgs: { [key: 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'
|
||||
}
|
||||
|
||||
errors: { [key: string]: string } = {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadQualifications();
|
||||
this.employeeForm = this.formBuilder.group({
|
||||
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]],
|
||||
city: [this.employee.city, Validators.required],
|
||||
phone: [this.employee.phone, Validators.required],
|
||||
qualifications: [this.employee.skillSet?.map(skill => skill.id) ?? []]
|
||||
});
|
||||
|
||||
Object.keys(this.employeeForm.controls).forEach((controlName: string) => {
|
||||
const control = this.employeeForm.controls[controlName];
|
||||
control.valueChanges.pipe(debounceTime(10)).subscribe(() => {
|
||||
this.showErrorMsg(controlName, control);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadQualifications() {
|
||||
this.qualificationService.getAll().subscribe(
|
||||
qualifications => this.qualifications = qualifications
|
||||
);
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.employeeForm === null || !this.employeeForm.valid) {
|
||||
console.error('Form invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.employee.id === undefined) {
|
||||
console.error('Employee ID is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.employeeForm.value;
|
||||
const employeeData = {
|
||||
...formValue,
|
||||
skillSet: formValue.qualifications
|
||||
};
|
||||
|
||||
this.employeeService.update(employeeData as Employee, this.employee.id).subscribe();
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
showErrorMsg(controlName: string, control: AbstractControl | undefined) {
|
||||
if (control?.errors) {
|
||||
this.errors[controlName] = this.errorMsgs[controlName];
|
||||
}
|
||||
}
|
||||
|
||||
validatePostcode(control: AbstractControl) {
|
||||
const postcode = control.value as number;
|
||||
if (postcode.toString().length !== 5) {
|
||||
return {invalidPostcode: true};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
73
src/app/employee/table/table.component.css
Normal file
73
src/app/employee/table/table.component.css
Normal file
@ -0,0 +1,73 @@
|
||||
:host ::ng-deep {
|
||||
.mat-mdc-card {
|
||||
--mdc-elevated-card-container-color: transparent;
|
||||
@apply !shadow-none !rounded-xl;
|
||||
}
|
||||
|
||||
.mat-mdc-button-base {
|
||||
--mat-mdc-button-persistent-ripple-color: currentColor;
|
||||
@apply !rounded-lg;
|
||||
}
|
||||
|
||||
.mat-mdc-progress-spinner {
|
||||
--mdc-circular-progress-active-indicator-color: #2563eb;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell {
|
||||
@apply !text-gray-600 !font-semibold !text-sm !py-4 !px-6;
|
||||
}
|
||||
|
||||
.mat-mdc-table {
|
||||
@apply !bg-transparent !border-separate !border-spacing-y-2;
|
||||
|
||||
.mat-mdc-row {
|
||||
@apply !bg-white !rounded-xl !shadow-sm !transition-all !duration-200;
|
||||
|
||||
&:hover {
|
||||
@apply !bg-gray-50 !shadow-md !transform !scale-[1.01];
|
||||
}
|
||||
|
||||
.mat-mdc-cell {
|
||||
@apply !border-b-0 !py-4 !px-6 first:!rounded-l-xl last:!rounded-r-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-header-row {
|
||||
@apply !bg-transparent;
|
||||
|
||||
.mat-mdc-header-cell {
|
||||
@apply !border-b-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-menu-panel {
|
||||
@apply !rounded-xl !shadow-lg;
|
||||
}
|
||||
|
||||
.mat-mdc-menu-item {
|
||||
@apply !rounded-lg !mx-1 !my-0.5;
|
||||
}
|
||||
|
||||
.mdc-button {
|
||||
@apply !font-medium;
|
||||
|
||||
&.mat-primary {
|
||||
@apply !bg-blue-600 !text-white hover:!bg-blue-700;
|
||||
}
|
||||
|
||||
&.mat-warn {
|
||||
@apply !bg-red-600 !text-white hover:!bg-red-700;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-snack-bar-container {
|
||||
&.error-snackbar {
|
||||
@apply !rounded-xl;
|
||||
|
||||
.mdc-snackbar__surface {
|
||||
@apply !bg-red-50 !text-red-900 !border !border-red-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
src/app/employee/table/table.component.html
Normal file
113
src/app/employee/table/table.component.html
Normal file
@ -0,0 +1,113 @@
|
||||
<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>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
} @loading {
|
||||
<div class="!flex !justify-center !items-center !py-12">
|
||||
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||
</div>
|
||||
}
|
||||
</section>
|
139
src/app/employee/table/table.component.ts
Normal file
139
src/app/employee/table/table.component.ts
Normal file
@ -0,0 +1,139 @@
|
||||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-employee-list',
|
||||
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 TableComponent implements OnInit {
|
||||
private readonly apiService: EmployeeApiService = inject(EmployeeApiService);
|
||||
private readonly matDialog: MatDialog = inject(MatDialog);
|
||||
private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService);
|
||||
|
||||
private static readonly MAX_RETRIES = 3;
|
||||
|
||||
private allEmployees: Employee[] = [];
|
||||
private searchSubject = new Subject<string>();
|
||||
public employees$: Observable<Employee[]> = of([]);
|
||||
public isSearching = false;
|
||||
public readonly displayedColumns: string[] = ['name', 'actions'];
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.loadEmployees();
|
||||
this.setupSearch();
|
||||
}
|
||||
|
||||
private loadEmployees(): void {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
private fetchEmployees(): Observable<Employee[]> {
|
||||
return this.apiService.getAll().pipe(
|
||||
retry(TableComponent.MAX_RETRIES),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
console.error('Error fetching employees:', error);
|
||||
this.errorHandlerService.showErrorMessage('Failed to load employees. Please try again.');
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected openDeleteDialogue(employee: Employee): void {
|
||||
this.matDialog.open(DeleteComponent, {data: employee})
|
||||
.afterClosed()
|
||||
.subscribe((deleted: boolean) => {
|
||||
if (deleted) {
|
||||
this.employees$ = this.fetchEmployees();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected showCreateEmployeeModal() {
|
||||
this.matDialog.open(CreateComponent)
|
||||
.afterClosed()
|
||||
.subscribe((created: boolean) => {
|
||||
if (created) {
|
||||
this.employees$ = this.fetchEmployees();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected showEditEmployeeModal(employee: Employee) {
|
||||
this.matDialog.open(EditComponent, {data: employee})
|
||||
.afterClosed()
|
||||
.subscribe((edited: boolean) => {
|
||||
if (edited) {
|
||||
this.employees$ = this.fetchEmployees();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected openDetailModal(employee: Employee) {
|
||||
this.matDialog.open(DetailsComponent, {data: employee});
|
||||
}
|
||||
|
||||
protected filterEmployees(event: Event): void {
|
||||
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase();
|
||||
this.searchSubject.next(searchTerm);
|
||||
}
|
||||
}
|
0
src/app/home/home.component.css
Normal file
0
src/app/home/home.component.css
Normal file
2
src/app/home/home.component.html
Normal file
2
src/app/home/home.component.html
Normal file
@ -0,0 +1,2 @@
|
||||
<app-employee-list></app-employee-list>
|
||||
<app-qualifications></app-qualifications>
|
16
src/app/home/home.component.ts
Normal file
16
src/app/home/home.component.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {TableComponent} from "../employee/table/table.component";
|
||||
import {QualificationsComponent} from "../qualification/table/table.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [
|
||||
TableComponent,
|
||||
QualificationsComponent
|
||||
],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.css'
|
||||
})
|
||||
export class HomeComponent {
|
||||
|
||||
}
|
6
src/app/login/login.component.css
Normal file
6
src/app/login/login.component.css
Normal file
@ -0,0 +1,6 @@
|
||||
.dot-loader {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
}
|
1
src/app/login/login.component.html
Normal file
1
src/app/login/login.component.html
Normal file
@ -0,0 +1 @@
|
||||
<div class="dot-loader">Logging in<span>{{ dots }}</span></div>
|
27
src/app/login/login.component.ts
Normal file
27
src/app/login/login.component.ts
Normal file
@ -0,0 +1,27 @@
|
||||
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'
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy{
|
||||
dots: string = '';
|
||||
private maxDots: number = 4; // Maximum number of dots
|
||||
private intervalSub!: Subscription;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.intervalSub = interval(500).subscribe(() => {
|
||||
this.dots = this.dots.length < this.maxDots ? this.dots + '.' : '';
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.intervalSub) {
|
||||
this.intervalSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
4
src/app/qualification/Qualification.ts
Normal file
4
src/app/qualification/Qualification.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class Qualification {
|
||||
constructor(public id: number, public skill?: string) {
|
||||
}
|
||||
}
|
0
src/app/qualification/create/create.component.css
Normal file
0
src/app/qualification/create/create.component.css
Normal file
47
src/app/qualification/create/create.component.html
Normal file
47
src/app/qualification/create/create.component.html
Normal file
@ -0,0 +1,47 @@
|
||||
<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]">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<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>
|
||||
<mat-hint class="text-sm">Enter the skill name</mat-hint>
|
||||
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
|
||||
{{ 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">
|
||||
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">
|
||||
Create
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
86
src/app/qualification/create/create.component.ts
Normal file
86
src/app/qualification/create/create.component.ts
Normal file
@ -0,0 +1,86 @@
|
||||
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";
|
||||
|
||||
@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'
|
||||
})
|
||||
export class CreateComponent {
|
||||
private formBuilder: FormBuilder = inject(FormBuilder);
|
||||
private qualificationService: QualificationService = inject(QualificationService);
|
||||
private dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef);
|
||||
|
||||
public apiErrorMessage: string = '';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
0
src/app/qualification/delete/delete.component.css
Normal file
0
src/app/qualification/delete/delete.component.css
Normal file
43
src/app/qualification/delete/delete.component.html
Normal file
43
src/app/qualification/delete/delete.component.html
Normal file
@ -0,0 +1,43 @@
|
||||
<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>
|
||||
<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-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>
|
||||
<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>
|
||||
</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">
|
||||
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>
|
||||
Delete
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</mat-dialog-content>
|
59
src/app/qualification/delete/delete.component.ts
Normal file
59
src/app/qualification/delete/delete.component.ts
Normal file
@ -0,0 +1,59 @@
|
||||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-qualification',
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatDialogContent,
|
||||
MatDialogTitle,
|
||||
ReactiveFormsModule,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './delete.component.html',
|
||||
standalone: true,
|
||||
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 dialogRef: MatDialogRef<DeleteComponent> = inject(MatDialogRef);
|
||||
|
||||
delete() {
|
||||
this.qualificationService.delete(this.id).subscribe({
|
||||
next: () => {
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error: HttpErrorResponse) => {
|
||||
console.error('Error deleting qualification:', error);
|
||||
|
||||
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';
|
||||
} else {
|
||||
this.apiError = 'API Error';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
0
src/app/qualification/details/details.component.css
Normal file
0
src/app/qualification/details/details.component.css
Normal file
42
src/app/qualification/details/details.component.html
Normal file
42
src/app/qualification/details/details.component.html
Normal file
@ -0,0 +1,42 @@
|
||||
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">
|
||||
{{ qualification.skill }} Developers
|
||||
</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 (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>
|
||||
</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)">
|
||||
<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">
|
||||
<span class="text-blue-600 font-medium text-sm md:text-base">
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</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">
|
||||
Close
|
||||
</button>
|
||||
</mat-dialog-actions>
|
55
src/app/qualification/details/details.component.ts
Normal file
55
src/app/qualification/details/details.component.ts
Normal file
@ -0,0 +1,55 @@
|
||||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-details',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
MatDialogContent,
|
||||
MatDialogTitle,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './details.component.html',
|
||||
styleUrl: './details.component.css'
|
||||
})
|
||||
export class DetailsComponent {
|
||||
private qualificationService = inject(QualificationService);
|
||||
private employeeService = inject(EmployeeApiService);
|
||||
private dialogRef: MatDialogRef<DetailsComponent> = inject(MatDialogRef);
|
||||
private dialog: MatDialog = inject(MatDialog);
|
||||
|
||||
public qualification: Qualification = inject(MAT_DIALOG_DATA);
|
||||
public employees$ = this.qualificationService.findEmployees(this.qualification.id);
|
||||
|
||||
closeModal() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
openEmployeeDetailsModal(id: number | undefined) {
|
||||
if (!id) {
|
||||
throw new Error("ID must not be undefined");
|
||||
}
|
||||
|
||||
this.employeeService.getById(id).subscribe(employee => {
|
||||
this.dialog.open(EmployeeDetailsComponent, {
|
||||
data: employee
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
0
src/app/qualification/edit/edit.component.css
Normal file
0
src/app/qualification/edit/edit.component.css
Normal file
47
src/app/qualification/edit/edit.component.html
Normal file
47
src/app/qualification/edit/edit.component.html
Normal file
@ -0,0 +1,47 @@
|
||||
<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]">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<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>
|
||||
<mat-hint class="text-sm">Enter the skill name</mat-hint>
|
||||
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
|
||||
{{ 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">
|
||||
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">
|
||||
Save Changes
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
88
src/app/qualification/edit/edit.component.ts
Normal file
88
src/app/qualification/edit/edit.component.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {FormBuilder, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions, MatDialogClose,
|
||||
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
|
||||
],
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrl: './edit.component.css'
|
||||
})
|
||||
export class EditComponent {
|
||||
public apiErrorMessage: string = '';
|
||||
public qualification: Qualification = inject(MAT_DIALOG_DATA);
|
||||
|
||||
private formBuilder: FormBuilder = inject(FormBuilder);
|
||||
private qualificationService: QualificationService = inject(QualificationService);
|
||||
private dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef);
|
||||
|
||||
qualificationForm = this.formBuilder.group({
|
||||
'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)
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
edit() {
|
||||
if (!this.qualificationForm.valid) {
|
||||
console.error('Validation failed');
|
||||
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);
|
||||
|
||||
this.apiErrorMessage = 'API Error';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
0
src/app/qualification/table/table.component.css
Normal file
0
src/app/qualification/table/table.component.css
Normal file
118
src/app/qualification/table/table.component.html
Normal file
118
src/app/qualification/table/table.component.html
Normal file
@ -0,0 +1,118 @@
|
||||
<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>
|
||||
</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>
|
143
src/app/qualification/table/table.component.ts
Normal file
143
src/app/qualification/table/table.component.ts
Normal file
@ -0,0 +1,143 @@
|
||||
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'
|
||||
})
|
||||
export class QualificationsComponent implements OnInit {
|
||||
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 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();
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
openCreateModal() {
|
||||
const dialogRef = this.dialog.open(CreateComponent);
|
||||
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openEditModal(qualification: Qualification) {
|
||||
const dialogRef = this.dialog.open(EditComponent, {
|
||||
data: qualification
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openDeleteModal(id: number) {
|
||||
const dialogRef = this.dialog.open(DeleteComponent, {
|
||||
data: id
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openDetailsModal(qualification: Qualification) {
|
||||
this.dialog.open(DetailsComponent, {
|
||||
data: qualification
|
||||
});
|
||||
}
|
||||
}
|
22
src/app/services/auth-guard.service.ts
Normal file
22
src/app/services/auth-guard.service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {AuthService} from "./auth.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {KeycloakService} from "keycloak-angular";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuardService {
|
||||
constructor(public auth: AuthService, public router: Router) {
|
||||
}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
this.router.navigate(['login']);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
19
src/app/services/auth.service.ts
Normal file
19
src/app/services/auth.service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {KeycloakService} from "keycloak-angular";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private keycloakService = inject(KeycloakService);
|
||||
|
||||
public isAuthenticated(): boolean {
|
||||
if (this.keycloakService.isLoggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.keycloakService.login();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
34
src/app/services/employee-api.service.ts
Normal file
34
src/app/services/employee-api.service.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {inject, Injectable} from "@angular/core";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {Observable} from "rxjs";
|
||||
import {Employee} from "../employee/Employee";
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export default class EmployeeApiService {
|
||||
private http: HttpClient = inject(HttpClient);
|
||||
|
||||
private static readonly BASE_URL = 'http://localhost:8089';
|
||||
|
||||
public getById(id: number): Observable<Employee> {
|
||||
return this.http.get(`${EmployeeApiService.BASE_URL}/employees/${id}`)
|
||||
}
|
||||
|
||||
public deleteById(id: number): Observable<Employee> {
|
||||
return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`)
|
||||
}
|
||||
|
||||
public getAll(): Observable<Employee[]> {
|
||||
return this.http.get<Employee[]>(`${EmployeeApiService.BASE_URL}/employees`)
|
||||
}
|
||||
|
||||
public create(employee: 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)
|
||||
}
|
||||
}
|
18
src/app/services/error.handler.service.ts
Normal file
18
src/app/services/error.handler.service.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ErrorHandlerService {
|
||||
private readonly snackBar: MatSnackBar = inject(MatSnackBar);
|
||||
|
||||
public showErrorMessage(message: string): void {
|
||||
this.snackBar.open(message, 'Close', {
|
||||
duration: 5000,
|
||||
horizontalPosition: 'end',
|
||||
verticalPosition: 'bottom',
|
||||
panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100']
|
||||
});
|
||||
}
|
||||
}
|
40
src/app/services/qualification.service.ts
Normal file
40
src/app/services/qualification.service.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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";
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export default class QualificationService {
|
||||
private http: HttpClient = inject(HttpClient);
|
||||
|
||||
private static readonly BASE_URL = 'http://localhost:8089';
|
||||
|
||||
public getAll(): Observable<Qualification[]> {
|
||||
return this.http.get<Qualification[]>(`${QualificationService.BASE_URL}/qualifications`).pipe(
|
||||
map(qualifications => qualifications.sort((a, b) => a.id - b.id))
|
||||
)
|
||||
}
|
||||
|
||||
public create(data: any) {
|
||||
return this.http.post(`${QualificationService.BASE_URL}/qualifications`, data)
|
||||
}
|
||||
|
||||
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`)
|
||||
.pipe(
|
||||
map(response => response.employees)
|
||||
);
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
<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">
|
||||
<body class="mat-typography bg-white">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,4 +1,6 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
|
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{html,ts,css,scss,sass,less,styl}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
Reference in New Issue
Block a user