WIP: style: adjust icon sizes in HTML templates #39
@ -5,7 +5,7 @@ volumes:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
postgres-employee:
|
postgres-employee:
|
||||||
container_name: postgres_employee
|
container_name: ems-db
|
||||||
image: postgres:13.3
|
image: postgres:13.3
|
||||||
volumes:
|
volumes:
|
||||||
- employee_postgres_data:/var/lib/postgresql/data
|
- employee_postgres_data:/var/lib/postgresql/data
|
||||||
@ -17,9 +17,8 @@ services:
|
|||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
|
||||||
employee:
|
employee:
|
||||||
container_name: employee
|
container_name: ems-api
|
||||||
image: berndheidemann/employee-management-service:1.1.3
|
image: berndheidemann/employee-management-service:1.1.3
|
||||||
# image: berndheidemann/employee-management-service_without_keycloak:1.1
|
|
||||||
environment:
|
environment:
|
||||||
spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db
|
spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db
|
||||||
spring.datasource.username: employee
|
spring.datasource.username: employee
|
||||||
|
@ -20,8 +20,10 @@
|
|||||||
"@angular/platform-browser": "^19.0.4",
|
"@angular/platform-browser": "^19.0.4",
|
||||||
"@angular/platform-browser-dynamic": "^19.0.4",
|
"@angular/platform-browser-dynamic": "^19.0.4",
|
||||||
"@angular/router": "^19.0.4",
|
"@angular/router": "^19.0.4",
|
||||||
|
"keycloak-angular": "^16.1.0",
|
||||||
"rxjs": "~7.8.1",
|
"rxjs": "~7.8.1",
|
||||||
"tailwind": "4.0.0",
|
"tailwind": "4.0.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
@ -30,12 +32,14 @@
|
|||||||
"@angular/cli": "^19.0.5",
|
"@angular/cli": "^19.0.5",
|
||||||
"@angular/compiler-cli": "^19.0.4",
|
"@angular/compiler-cli": "^19.0.4",
|
||||||
"@types/jasmine": "~5.1.5",
|
"@types/jasmine": "~5.1.5",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
"jasmine-core": "~5.2.0",
|
"jasmine-core": "~5.2.0",
|
||||||
"karma": "~6.4.4",
|
"karma": "~6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.1",
|
"karma-coverage": "~2.2.1",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
"typescript": "~5.5.4"
|
"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 {Component} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import {RouterOutlet} from '@angular/router';
|
||||||
import {EmployeeListComponent} from "./employee-list/employee-list.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [CommonModule, EmployeeListComponent],
|
imports: [CommonModule, RouterOutlet],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
|
standalone: true,
|
||||||
styleUrl: './app.component.css'
|
styleUrl: './app.component.css'
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'lf10StarterNew';
|
title = 'Employee Management System';
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,50 @@
|
|||||||
import { ApplicationConfig } from '@angular/core';
|
import {APP_INITIALIZER, ApplicationConfig} from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
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';
|
export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
|
||||||
import {provideHttpClient, withInterceptorsFromDi} from "@angular/common/http";
|
keycloak.init({
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
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 = {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
src/app/employee/table/table.component.html
Normal file
105
src/app/employee/table/table.component.html
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<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">
|
||||||
|
<h2 class="!text-2xl !font-semibold !text-gray-900">Employee Directory</h2>
|
||||||
|
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white" (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 {
|
||||||
|
<mat-card class="!bg-red-50 !border !border-red-100">
|
||||||
|
<mat-card-content class="!p-4">
|
||||||
|
<div class="!flex !items-center !gap-4 !text-red-800">
|
||||||
|
<mat-icon class="!text-red-500 !w-8 !h-8">error</mat-icon>
|
||||||
|
<div>
|
||||||
|
<h3 class="!font-medium !mb-1">Error loading employees</h3>
|
||||||
|
<p class="!text-sm !text-red-700">Please try refreshing the page.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
} @loading {
|
||||||
|
<div class="!flex !justify-center !items-center !py-12">
|
||||||
|
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
109
src/app/employee/table/table.component.ts
Normal file
109
src/app/employee/table/table.component.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import {Component, inject, OnInit} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {catchError, Observable, of, retry} 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 {MatSnackBar, 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";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-employee-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
MatDividerModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatSortModule
|
||||||
|
],
|
||||||
|
templateUrl: './table.component.html',
|
||||||
|
styleUrl: './table.component.css'
|
||||||
|
})
|
||||||
|
export class TableComponent implements OnInit {
|
||||||
|
private readonly apiService: EmployeeApiService = inject(EmployeeApiService);
|
||||||
|
private readonly snackBar: MatSnackBar = inject(MatSnackBar);
|
||||||
|
private readonly matDialog: MatDialog = inject(MatDialog);
|
||||||
|
|
||||||
|
private static readonly MAX_RETRIES = 3;
|
||||||
|
public employees$: Observable<Employee[]> = of([]);
|
||||||
|
public readonly displayedColumns: string[] = ['name', 'actions'];
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.employees$ = this.fetchEmployees();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchEmployees(): Observable<Employee[]> {
|
||||||
|
return this.apiService.getAll().pipe(
|
||||||
|
retry(TableComponent.MAX_RETRIES),
|
||||||
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
console.error('Error fetching employees:', error);
|
||||||
|
this.showErrorMessage('Failed to load employees. Please try again.');
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
}
|
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
44
src/app/qualification/create/create.component.html
Normal file
44
src/app/qualification/create/create.component.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<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">
|
||||||
|
<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-error class="text-sm md:text-base text-red-700">{{ apiErrorMessage }}</mat-error>
|
||||||
|
</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>
|
60
src/app/qualification/delete/delete.component.ts
Normal file
60
src/app/qualification/delete/delete.component.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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,
|
||||||
|
MatError,
|
||||||
|
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
44
src/app/qualification/edit/edit.component.html
Normal file
44
src/app/qualification/edit/edit.component.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<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">
|
||||||
|
<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-error class="text-sm md:text-base text-red-700">{{ apiErrorMessage }}</mat-error>
|
||||||
|
</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
105
src/app/qualification/table/table.component.html
Normal file
105
src/app/qualification/table/table.component.html
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<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">
|
||||||
|
<h2 class="!text-2xl !font-semibold !text-gray-900">Qualifications</h2>
|
||||||
|
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white"
|
||||||
|
(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 {
|
||||||
|
<mat-card class="!bg-red-50 !border !border-red-100">
|
||||||
|
<mat-card-content class="!p-4">
|
||||||
|
<div class="!flex !items-center !gap-4 !text-red-800">
|
||||||
|
<mat-icon class="!text-red-500 !w-8 !h-8">error</mat-icon>
|
||||||
|
<div>
|
||||||
|
<h3 class="!font-medium !mb-1">Error loading qualifications</h3>
|
||||||
|
<p class="!text-sm !text-red-700">Please try refreshing the page.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
} @loading {
|
||||||
|
<div class="!flex !justify-center !items-center !py-12">
|
||||||
|
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
107
src/app/qualification/table/table.component.ts
Normal file
107
src/app/qualification/table/table.component.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import {Component, inject, OnInit} from '@angular/core';
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
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 {AsyncPipe} from "@angular/common";
|
||||||
|
import {MatButton, MatIconButton} from "@angular/material/button";
|
||||||
|
import {
|
||||||
|
MatCell,
|
||||||
|
MatCellDef,
|
||||||
|
MatColumnDef,
|
||||||
|
MatHeaderCell,
|
||||||
|
MatHeaderCellDef,
|
||||||
|
MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
|
||||||
|
MatTable
|
||||||
|
} from "@angular/material/table";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
import {MatCard, MatCardContent} from "@angular/material/card";
|
||||||
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
|
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
||||||
|
import {DetailsComponent} from "../details/details.component";
|
||||||
|
import {MatSort} from "@angular/material/sort";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-qualifications',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
MatButton,
|
||||||
|
MatTable,
|
||||||
|
MatColumnDef,
|
||||||
|
MatHeaderCell,
|
||||||
|
MatCell,
|
||||||
|
MatHeaderCellDef,
|
||||||
|
MatCellDef,
|
||||||
|
MatIconButton,
|
||||||
|
MatIcon,
|
||||||
|
MatHeaderRow,
|
||||||
|
MatRow,
|
||||||
|
MatHeaderRowDef,
|
||||||
|
MatRowDef,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatTooltip,
|
||||||
|
MatProgressSpinner,
|
||||||
|
MatSort
|
||||||
|
],
|
||||||
|
templateUrl: './table.component.html',
|
||||||
|
styleUrl: './table.component.css'
|
||||||
|
})
|
||||||
|
export class QualificationsComponent implements OnInit {
|
||||||
|
public qualifications$!: Observable<Qualification[]>;
|
||||||
|
public readonly displayedColumns: string[] = ['skill', 'actions'];
|
||||||
|
|
||||||
|
private readonly dialog: MatDialog = inject(MatDialog);
|
||||||
|
private readonly qualificationService: QualificationService = inject(QualificationService);
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.loadQualifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadQualifications() {
|
||||||
|
this.qualifications$ = this.qualificationService.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const dialogRef = 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)
|
||||||
|
}
|
||||||
|
}
|
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/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body class="mat-typography">
|
<body class="mat-typography bg-white">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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%; }
|
html, body { height: 100%; }
|
||||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
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