style: format code and improve readability across files
This commit is contained in:
parent
26ba6b1054
commit
9ba377b0c8
76
README.md
76
README.md
@ -1,11 +1,9 @@
|
||||
# Starter für das LF10 Projekt
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* Docker https://docs.docker.com/get-docker/
|
||||
* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
|
||||
|
||||
- Docker https://docs.docker.com/get-docker/
|
||||
- Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
|
||||
|
||||
### Abhängigkeiten starten (Postgres, EmployeeBackend)
|
||||
|
||||
@ -37,7 +35,7 @@ http://localhost:8089/swagger
|
||||
|
||||
# Postgres
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
### Intellij-Ansicht für Postgres Datenbank einrichten (geht nicht in Webstorm!)
|
||||
|
||||
@ -50,8 +48,8 @@ http://localhost:8089/swagger
|
||||
6. URL der DB einfügen (jdbc:postgresql://postgres-employee:5432/employee_db) und PostgreSQL-Treiber auswählen, mit OK bestätigen
|
||||
7. Username lf10_starter und Passwort secret eintragen (siehe application.properties), mit Apply bestätigen
|
||||
8. im Reiter Schemas alle Häkchen entfernen und lediglich vor lf10_starter_db und public Häkchen setzen
|
||||
9. mit Apply und ok bestätigen
|
||||
```
|
||||
9. mit Apply und ok bestätigen
|
||||
````
|
||||
|
||||
# Keycloak
|
||||
|
||||
@ -80,57 +78,54 @@ die ClientId deines Angular Frontends lautet: employee-management-service-fronte
|
||||
Hier ein Beispiel einer app.config.ts mit der Konfiguration für Keycloak. Mit dem KeycloakService, der hier definiert wird, kannst du in einem AuthGuard z.B. feststellen, ob der Benutzer eingeloggt ist oder nicht oder ihn mit keycloakService.login() zum Login weiterleiten.
|
||||
|
||||
```typescript
|
||||
import {APP_INITIALIZER, ApplicationConfig} from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { APP_INITIALIZER, ApplicationConfig } from "@angular/core";
|
||||
import { provideRouter } from "@angular/router";
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import {KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular";
|
||||
import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http";
|
||||
import { routes } from "./app.routes";
|
||||
import { KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService } from "keycloak-angular";
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http";
|
||||
|
||||
export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
|
||||
keycloak.init({
|
||||
config: {
|
||||
url: 'KEYCLOAK_URL',
|
||||
realm: 'REALM',
|
||||
clientId: 'CLIENT_ID',
|
||||
url: "KEYCLOAK_URL",
|
||||
realm: "REALM",
|
||||
clientId: "CLIENT_ID",
|
||||
},
|
||||
loadUserProfileAtStartUp: true,
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri:
|
||||
window.location.origin + '/silent-check-sso.html',
|
||||
onLoad: "check-sso",
|
||||
silentCheckSsoRedirectUri: window.location.origin + "/silent-check-sso.html",
|
||||
checkLoginIframe: false,
|
||||
redirectUri: 'http://localhost:4200',
|
||||
redirectUri: "http://localhost:4200",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
|
||||
return () => initializeKeycloak(keycloak)();
|
||||
}
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes),
|
||||
KeycloakAngularModule,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeApp,
|
||||
multi: true,
|
||||
deps: [KeycloakService]
|
||||
},
|
||||
KeycloakService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
KeycloakAngularModule,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeApp,
|
||||
multi: true,
|
||||
deps: [KeycloakService],
|
||||
},
|
||||
KeycloakService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
Der Benutzer, mit dem ihr eure Integration testen könnt, hat den Benutzernamen user und das Passwort test. Die einzige Rolle heißt user.
|
||||
|
||||
Des Weiteren ist der Client mit der Bezeichnung employee-management-service-frontend wie folgt konfiguriert:
|
||||
@ -140,5 +135,4 @@ Des Weiteren ist der Client mit der Bezeichnung employee-management-service-fron
|
||||
|
||||
# Bugs
|
||||
|
||||
Trage hier die Features ein, die nicht funktionieren. Beschreibe den jeweiligen Fehler.
|
||||
|
||||
Trage hier die Features ein, die nicht funktionieren. Beschreibe den jeweiligen Fehler.
|
||||
|
18
angular.json
18
angular.json
@ -16,9 +16,7 @@
|
||||
"outputPath": "dist/lf10-starter2024",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
@ -74,10 +72,7 @@
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
{
|
||||
@ -95,10 +90,7 @@
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,8 +98,6 @@
|
||||
},
|
||||
"cli": {
|
||||
"analytics": "33c8483f-3876-4eb5-9c9b-1001cab9b273",
|
||||
"schematicCollections": [
|
||||
"angular-eslint"
|
||||
]
|
||||
"schematicCollections": ["angular-eslint"]
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
volumes:
|
||||
employee_postgres_data:
|
||||
driver: local
|
||||
|
@ -39,5 +39,5 @@ module.exports = tseslint.config(
|
||||
...angular.configs.templateAccessibility,
|
||||
],
|
||||
rules: {},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -47,4 +47,4 @@
|
||||
"typescript": "~5.5.4",
|
||||
"typescript-eslint": "8.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,5 +1,4 @@
|
||||
<main class="container mx-auto px-4 py-8 max-w-6xl">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">{{ title }}</h1>
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [CommonModule, RouterOutlet],
|
||||
templateUrl: './app.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './app.component.css'
|
||||
styleUrl: './app.component.css',
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'Employee Management System';
|
||||
|
@ -1,9 +1,17 @@
|
||||
import {APP_INITIALIZER, ApplicationConfig} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {KeycloakAngularModule, KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular";
|
||||
import {routes} from './app.routes';
|
||||
import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http";
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import {
|
||||
KeycloakAngularModule,
|
||||
KeycloakBearerInterceptor,
|
||||
KeycloakService,
|
||||
} from 'keycloak-angular';
|
||||
import { routes } from './app.routes';
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
|
||||
export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
|
||||
keycloak.init({
|
||||
@ -22,7 +30,6 @@ export const initializeKeycloak = (keycloak: KeycloakService) => async () =>
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function initializeApp(keycloak: KeycloakService): () => Promise<boolean> {
|
||||
return () => initializeKeycloak(keycloak)();
|
||||
}
|
||||
@ -37,14 +44,14 @@ export const appConfig: ApplicationConfig = {
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeApp,
|
||||
multi: true,
|
||||
deps: [KeycloakService]
|
||||
deps: [KeycloakService],
|
||||
},
|
||||
KeycloakService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {LoginComponent} from "./login/login.component";
|
||||
import {AuthGuardService} from "./services/auth-guard.service";
|
||||
import {HomeComponent} from "./home/home.component";
|
||||
import { Routes } from '@angular/router';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { AuthGuardService } from './services/auth-guard.service';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: 'login', component: LoginComponent},
|
||||
{path: '', component: HomeComponent, canActivate: [AuthGuardService]},
|
||||
{path: '**', redirectTo: ''}
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: '', component: HomeComponent, canActivate: [AuthGuardService] },
|
||||
{ path: '**', redirectTo: '' },
|
||||
];
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Qualification} from "../qualification/Qualification";
|
||||
import { Qualification } from '../qualification/Qualification';
|
||||
|
||||
export class Employee {
|
||||
constructor(
|
||||
@ -9,7 +9,6 @@ export class Employee {
|
||||
public postcode?: string,
|
||||
public city?: string,
|
||||
public phone?: string,
|
||||
public skillSet?: Qualification[]
|
||||
) {
|
||||
}
|
||||
public skillSet?: Qualification[],
|
||||
) {}
|
||||
}
|
||||
|
@ -5,74 +5,98 @@
|
||||
<div class="flex gap-x-4">
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<input matInput formControlName="firstName" required />
|
||||
<mat-hint>Enter the first name</mat-hint>
|
||||
<mat-error *ngIf="errors['firstName']">{{errors['firstName']}}</mat-error>
|
||||
<mat-error *ngIf="errors['firstName']">{{
|
||||
errors["firstName"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" required>
|
||||
<input matInput formControlName="lastName" required />
|
||||
<mat-hint>Enter the last name</mat-hint>
|
||||
<mat-error *ngIf="errors['lastName']">{{errors['lastName']}}</mat-error>
|
||||
<mat-error *ngIf="errors['lastName']">{{
|
||||
errors["lastName"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Street</mat-label>
|
||||
<input matInput formControlName="street" required>
|
||||
<input matInput formControlName="street" required />
|
||||
<mat-hint>Enter the street address</mat-hint>
|
||||
<mat-error *ngIf="errors['street']">{{errors['street']}}</mat-error>
|
||||
<mat-error *ngIf="errors['street']">{{ errors["street"] }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="flex gap-x-4">
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>City</mat-label>
|
||||
<input matInput formControlName="city" required>
|
||||
<input matInput formControlName="city" required />
|
||||
<mat-hint>Enter the city</mat-hint>
|
||||
<mat-error *ngIf="errors['city']">{{errors['city']}}</mat-error>
|
||||
<mat-error *ngIf="errors['city']">{{ errors["city"] }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="!w-1/2">
|
||||
<mat-label>Postcode</mat-label>
|
||||
<input matInput formControlName="postcode" minlength="5" maxlength="5" type="number" required>
|
||||
<input
|
||||
matInput
|
||||
formControlName="postcode"
|
||||
minlength="5"
|
||||
maxlength="5"
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
<mat-hint>Enter postcode</mat-hint>
|
||||
<mat-error *ngIf="errors['postcode']">{{errors['postcode']}}</mat-error>
|
||||
<mat-error *ngIf="errors['postcode']">{{
|
||||
errors["postcode"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" required>
|
||||
<input matInput formControlName="phone" required />
|
||||
<mat-hint>Enter the phone number</mat-hint>
|
||||
<mat-error *ngIf="errors['phone']">{{errors['phone']}}</mat-error>
|
||||
<mat-error *ngIf="errors['phone']">{{ errors["phone"] }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Qualifications</mat-label>
|
||||
<mat-hint>Select the qualifications</mat-hint>
|
||||
<mat-select formControlName="qualifications" multiple>
|
||||
<mat-option *ngFor="let qualification of qualifications" [value]="qualification.id">
|
||||
{{qualification.skill}}
|
||||
<mat-option
|
||||
*ngFor="let qualification of qualifications"
|
||||
[value]="qualification.id"
|
||||
>
|
||||
{{ qualification.skill }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="errors['qualifications']">{{errors['qualifications']}}</mat-error>
|
||||
<mat-error *ngIf="errors['qualifications']">{{
|
||||
errors["qualifications"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
|
||||
<button mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1">
|
||||
<mat-dialog-actions
|
||||
align="end"
|
||||
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
@ -1,23 +1,29 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {MatFormField, MatHint, MatLabel} from "@angular/material/form-field";
|
||||
import {MatError, MatInput} from "@angular/material/input";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatFormField, MatHint, MatLabel } from '@angular/material/form-field';
|
||||
import { MatError, MatInput } from '@angular/material/input';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import {
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {Employee} from "../Employee";
|
||||
import EmployeeApiService from "../../services/employee-api.service";
|
||||
import {NgForOf, NgIf} from "@angular/common";
|
||||
import {MatOption} from "@angular/material/core";
|
||||
import {MatSelect} from "@angular/material/select";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import {Qualification} from "../../qualification/Qualification";
|
||||
import {debounceTime} from "rxjs";
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { Employee } from '../Employee';
|
||||
import EmployeeApiService from '../../services/employee-api.service';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { MatOption } from '@angular/material/core';
|
||||
import { MatSelect } from '@angular/material/select';
|
||||
import QualificationService from '../../services/qualification.service';
|
||||
import { Qualification } from '../../qualification/Qualification';
|
||||
import { debounceTime } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-employee',
|
||||
@ -36,11 +42,11 @@ import {debounceTime} from "rxjs";
|
||||
MatSelect,
|
||||
NgForOf,
|
||||
MatError,
|
||||
MatHint
|
||||
MatHint,
|
||||
],
|
||||
templateUrl: './create.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './create.component.css'
|
||||
styleUrl: './create.component.css',
|
||||
})
|
||||
export class CreateComponent implements OnInit {
|
||||
employeeForm!: FormGroup;
|
||||
@ -49,17 +55,17 @@ export class CreateComponent implements OnInit {
|
||||
dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef);
|
||||
qualificationService: QualificationService = inject(QualificationService);
|
||||
qualifications: Qualification[] = [];
|
||||
errorMsgs: { [key: string]: string } = {
|
||||
errorMsgs: Record<string, string> = {
|
||||
firstName: 'First name is required',
|
||||
lastName: 'Last name is required',
|
||||
street: 'Street is required',
|
||||
postcode: 'Postcode must be 5 characters long',
|
||||
city: 'City is required',
|
||||
phone: 'Phone is required',
|
||||
qualifications: 'Qualifications are required'
|
||||
}
|
||||
qualifications: 'Qualifications are required',
|
||||
};
|
||||
|
||||
errors: { [key: string]: string } = {}
|
||||
errors: Record<string, string> = {};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadQualifications();
|
||||
@ -70,7 +76,7 @@ export class CreateComponent implements OnInit {
|
||||
postcode: ['', [Validators.required, this.validatePostcode]],
|
||||
city: ['', Validators.required],
|
||||
phone: ['', Validators.required],
|
||||
qualifications: [[]]
|
||||
qualifications: [[]],
|
||||
});
|
||||
|
||||
Object.keys(this.employeeForm.controls).forEach((controlName: string) => {
|
||||
@ -82,9 +88,9 @@ export class CreateComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadQualifications() {
|
||||
this.qualificationService.getAll().subscribe(
|
||||
qualifications => this.qualifications = qualifications
|
||||
);
|
||||
this.qualificationService
|
||||
.getAll()
|
||||
.subscribe((qualifications) => (this.qualifications = qualifications));
|
||||
}
|
||||
|
||||
submit() {
|
||||
@ -95,7 +101,7 @@ export class CreateComponent implements OnInit {
|
||||
const formValue = this.employeeForm.value;
|
||||
const employeeData = {
|
||||
...formValue,
|
||||
skillSet: formValue.qualifications
|
||||
skillSet: formValue.qualifications,
|
||||
};
|
||||
|
||||
this.employeeService.create(employeeData as Employee).subscribe();
|
||||
@ -111,7 +117,7 @@ export class CreateComponent implements OnInit {
|
||||
validatePostcode(control: AbstractControl) {
|
||||
const postcode = control.value as number;
|
||||
if (postcode.toString().length !== 5) {
|
||||
return {invalidPostcode: true};
|
||||
return { invalidPostcode: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -1,31 +1,48 @@
|
||||
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Delete Employee</h2>
|
||||
<h2
|
||||
mat-dialog-title
|
||||
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
|
||||
>
|
||||
Delete Employee
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="!px-3 md:!px-6">
|
||||
<div class="w-full min-w-[280px] md:min-w-[400px] space-y-4 md:space-y-6">
|
||||
<div class="bg-amber-50 p-3 md:p-4 rounded-lg border border-amber-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8">warning</mat-icon>
|
||||
<mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8"
|
||||
>warning</mat-icon
|
||||
>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
Are you sure you want to delete {{employee.firstName}} {{employee.lastName}}?
|
||||
Are you sure you want to delete {{ employee.firstName }}
|
||||
{{ employee.lastName }}?
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">This action cannot be undone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
|
||||
<button mat-button
|
||||
[mat-dialog-close]="false"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1">
|
||||
<mat-dialog-actions
|
||||
align="end"
|
||||
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
[mat-dialog-close]="false"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-flat-button
|
||||
color="warn"
|
||||
(click)="deleteEmployee(employee.id ?? 0)"
|
||||
mat-dialog-close
|
||||
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
|
||||
cdkFocusInitial>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="warn"
|
||||
(click)="deleteEmployee(employee.id ?? 0)"
|
||||
mat-dialog-close
|
||||
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
|
||||
cdkFocusInitial
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
@ -1,15 +1,16 @@
|
||||
import {Component, Inject, inject} from '@angular/core';
|
||||
import {Employee} from "../Employee";
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Employee } from '../Employee';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent, MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import EmployeeApiService from "../../services/employee-api.service";
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import EmployeeApiService from '../../services/employee-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-employee',
|
||||
@ -19,11 +20,11 @@ import EmployeeApiService from "../../services/employee-api.service";
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatDialogClose,
|
||||
MatIcon
|
||||
MatIcon,
|
||||
],
|
||||
templateUrl: './delete.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './delete.component.css'
|
||||
styleUrl: './delete.component.css',
|
||||
})
|
||||
export class DeleteComponent {
|
||||
private apiService: EmployeeApiService = inject(EmployeeApiService);
|
||||
|
@ -1,21 +1,32 @@
|
||||
<h2 mat-dialog-title class="text-2xl font-semibold text-gray-800 mb-4">Employee Details</h2>
|
||||
<h2 mat-dialog-title class="text-2xl font-semibold text-gray-800 mb-4">
|
||||
Employee Details
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="!px-4 sm:!px-6">
|
||||
<div class="w-full min-w-[280px] sm:min-w-[500px] space-y-6 sm:space-y-8">
|
||||
<div class="flex flex-col sm:flex-row items-center sm:items-start text-center sm:text-left space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
<div class="h-20 w-20 sm:h-16 sm:w-16 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row items-center sm:items-start text-center sm:text-left space-y-4 sm:space-y-0 sm:space-x-4"
|
||||
>
|
||||
<div
|
||||
class="h-20 w-20 sm:h-16 sm:w-16 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<span class="text-blue-600 text-2xl sm:text-xl font-medium">
|
||||
{{ employee.firstName?.charAt(0) ?? '' }}{{ employee.lastName?.charAt(0) ?? '' }}
|
||||
{{ employee.firstName?.charAt(0) ?? ""
|
||||
}}{{ employee.lastName?.charAt(0) ?? "" }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-medium text-gray-900">{{ employee.firstName }} {{ employee.lastName }}</h3>
|
||||
<h3 class="text-xl font-medium text-gray-900">
|
||||
{{ employee.firstName }} {{ employee.lastName }}
|
||||
</h3>
|
||||
<p class="text-gray-500">ID: {{ employee.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">Contact Information</h4>
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">
|
||||
Contact Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm text-gray-500">Phone</p>
|
||||
@ -45,11 +56,15 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">Qualifications</h4>
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-2 sm:mb-3">
|
||||
Qualifications
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (skill of employee.skillSet; track skill.id) {
|
||||
<div class="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm font-medium">
|
||||
{{skill.skill}}
|
||||
<div
|
||||
class="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm font-medium"
|
||||
>
|
||||
{{ skill.skill }}
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="text-gray-500 italic">No qualifications added</p>
|
||||
@ -60,9 +75,11 @@
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-4 sm:!px-6 !py-4 !mt-4 border-t">
|
||||
<button mat-button
|
||||
(click)="closeModal()"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full">
|
||||
<button
|
||||
mat-button
|
||||
(click)="closeModal()"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
@ -1,20 +1,20 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, MatDialogTitle} from "@angular/material/dialog";
|
||||
import {Employee} from "../Employee";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {DialogRef} from "@angular/cdk/dialog";
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { Employee } from '../Employee';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { DialogRef } from '@angular/cdk/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details',
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatButton,
|
||||
MatDialogActions
|
||||
],
|
||||
imports: [MatDialogTitle, MatDialogContent, MatButton, MatDialogActions],
|
||||
templateUrl: './details.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './details.component.css'
|
||||
styleUrl: './details.component.css',
|
||||
})
|
||||
export class DetailsComponent {
|
||||
employee: Employee = inject(MAT_DIALOG_DATA);
|
||||
|
@ -5,74 +5,98 @@
|
||||
<div class="flex gap-x-4">
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" required>
|
||||
<input matInput formControlName="firstName" required />
|
||||
<mat-hint>Enter the first name</mat-hint>
|
||||
<mat-error *ngIf="errors['firstName']">{{errors['firstName']}}</mat-error>
|
||||
<mat-error *ngIf="errors['firstName']">{{
|
||||
errors["firstName"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" required>
|
||||
<input matInput formControlName="lastName" required />
|
||||
<mat-hint>Enter the last name</mat-hint>
|
||||
<mat-error *ngIf="errors['lastName']">{{errors['lastName']}}</mat-error>
|
||||
<mat-error *ngIf="errors['lastName']">{{
|
||||
errors["lastName"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Street</mat-label>
|
||||
<input matInput formControlName="street" required>
|
||||
<input matInput formControlName="street" required />
|
||||
<mat-hint>Enter the street address</mat-hint>
|
||||
<mat-error *ngIf="errors['street']">{{errors['street']}}</mat-error>
|
||||
<mat-error *ngIf="errors['street']">{{ errors["street"] }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="flex gap-x-4">
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>City</mat-label>
|
||||
<input matInput formControlName="city" required>
|
||||
<input matInput formControlName="city" required />
|
||||
<mat-hint>Enter the city</mat-hint>
|
||||
<mat-error *ngIf="errors['city']">{{errors['city']}}</mat-error>
|
||||
<mat-error *ngIf="errors['city']">{{ errors["city"] }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="!w-1/2">
|
||||
<mat-label>Postcode</mat-label>
|
||||
<input matInput formControlName="postcode" minlength="5" maxlength="5" type="number" required>
|
||||
<input
|
||||
matInput
|
||||
formControlName="postcode"
|
||||
minlength="5"
|
||||
maxlength="5"
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
<mat-hint>Enter postcode</mat-hint>
|
||||
<mat-error *ngIf="errors['postcode']">{{errors['postcode']}}</mat-error>
|
||||
<mat-error *ngIf="errors['postcode']">{{
|
||||
errors["postcode"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input matInput formControlName="phone" required>
|
||||
<input matInput formControlName="phone" required />
|
||||
<mat-hint>Enter phone number</mat-hint>
|
||||
<mat-error *ngIf="errors['phone']">{{errors['phone']}}</mat-error>
|
||||
<mat-error *ngIf="errors['phone']">{{ errors["phone"] }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="!w-full">
|
||||
<mat-label>Qualifications</mat-label>
|
||||
<mat-select formControlName="qualifications" multiple>
|
||||
<mat-option *ngFor="let qualification of qualifications" [value]="qualification.id">
|
||||
{{qualification.skill}}
|
||||
<mat-option
|
||||
*ngFor="let qualification of qualifications"
|
||||
[value]="qualification.id"
|
||||
>
|
||||
{{ qualification.skill }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-hint>Select qualifications</mat-hint>
|
||||
<mat-error *ngIf="errors['qualifications']">{{errors['qualifications']}}</mat-error>
|
||||
<mat-error *ngIf="errors['qualifications']">{{
|
||||
errors["qualifications"]
|
||||
}}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
|
||||
<button mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1">
|
||||
<mat-dialog-actions
|
||||
align="end"
|
||||
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
@ -1,23 +1,29 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {NgForOf, NgIf} from "@angular/common";
|
||||
import {MatFormField, MatHint} from "@angular/material/form-field";
|
||||
import {MatError, MatInput, MatLabel} from "@angular/material/input";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {Employee} from "../Employee";
|
||||
import EmployeeApiService from "../../services/employee-api.service";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import {Qualification} from "../../qualification/Qualification";
|
||||
import {MatOption, MatSelect} from "@angular/material/select";
|
||||
import {debounceTime} from "rxjs";
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { MatFormField, MatHint } from '@angular/material/form-field';
|
||||
import { MatError, MatInput, MatLabel } from '@angular/material/input';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { Employee } from '../Employee';
|
||||
import EmployeeApiService from '../../services/employee-api.service';
|
||||
import QualificationService from '../../services/qualification.service';
|
||||
import { Qualification } from '../../qualification/Qualification';
|
||||
import { MatOption, MatSelect } from '@angular/material/select';
|
||||
import { debounceTime } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit',
|
||||
@ -40,7 +46,7 @@ import {debounceTime} from "rxjs";
|
||||
],
|
||||
templateUrl: './edit.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './edit.component.css'
|
||||
styleUrl: './edit.component.css',
|
||||
})
|
||||
export class EditComponent implements OnInit {
|
||||
employeeForm!: FormGroup;
|
||||
@ -50,17 +56,17 @@ export class EditComponent implements OnInit {
|
||||
dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef);
|
||||
employee: Employee = inject(MAT_DIALOG_DATA);
|
||||
qualifications: Qualification[] = [];
|
||||
errorMsgs: { [key: string]: string } = {
|
||||
errorMsgs: Record<string, string> = {
|
||||
firstName: 'First name is required',
|
||||
lastName: 'Last name is required',
|
||||
street: 'Street is required',
|
||||
postcode: 'Postcode must be 5 characters long',
|
||||
city: 'City is required',
|
||||
phone: 'Phone is required',
|
||||
qualifications: 'Qualifications are required'
|
||||
}
|
||||
qualifications: 'Qualifications are required',
|
||||
};
|
||||
|
||||
errors: { [key: string]: string } = {}
|
||||
errors: Record<string, string> = {};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadQualifications();
|
||||
@ -68,10 +74,13 @@ export class EditComponent implements OnInit {
|
||||
firstName: [this.employee.firstName, Validators.required],
|
||||
lastName: [this.employee.lastName, Validators.required],
|
||||
street: [this.employee.street, Validators.required],
|
||||
postcode: [this.employee.postcode, [Validators.required, this.validatePostcode]],
|
||||
postcode: [
|
||||
this.employee.postcode,
|
||||
[Validators.required, this.validatePostcode],
|
||||
],
|
||||
city: [this.employee.city, Validators.required],
|
||||
phone: [this.employee.phone, Validators.required],
|
||||
qualifications: [this.employee.skillSet?.map(skill => skill.id) ?? []]
|
||||
qualifications: [this.employee.skillSet?.map((skill) => skill.id) ?? []],
|
||||
});
|
||||
|
||||
Object.keys(this.employeeForm.controls).forEach((controlName: string) => {
|
||||
@ -83,9 +92,9 @@ export class EditComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadQualifications() {
|
||||
this.qualificationService.getAll().subscribe(
|
||||
qualifications => this.qualifications = qualifications
|
||||
);
|
||||
this.qualificationService
|
||||
.getAll()
|
||||
.subscribe((qualifications) => (this.qualifications = qualifications));
|
||||
}
|
||||
|
||||
submit() {
|
||||
@ -102,10 +111,12 @@ export class EditComponent implements OnInit {
|
||||
const formValue = this.employeeForm.value;
|
||||
const employeeData = {
|
||||
...formValue,
|
||||
skillSet: formValue.qualifications
|
||||
skillSet: formValue.qualifications,
|
||||
};
|
||||
|
||||
this.employeeService.update(employeeData as Employee, this.employee.id).subscribe();
|
||||
this.employeeService
|
||||
.update(employeeData as Employee, this.employee.id)
|
||||
.subscribe();
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
@ -118,7 +129,7 @@ export class EditComponent implements OnInit {
|
||||
validatePostcode(control: AbstractControl) {
|
||||
const postcode = control.value as number;
|
||||
if (postcode.toString().length !== 5) {
|
||||
return {invalidPostcode: true};
|
||||
return { invalidPostcode: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -1,113 +1,163 @@
|
||||
<section class="!space-y-6 mb-6">
|
||||
@defer {
|
||||
@if (employees$ | async; as employees) {
|
||||
<div class="!space-y-6">
|
||||
<div class="!flex !justify-between !items-center">
|
||||
<div class="!flex !items-center !gap-4">
|
||||
<h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">Employee Directory</h2>
|
||||
<mat-form-field class="!m-0" subscriptSizing="dynamic">
|
||||
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Search employees..."
|
||||
(keyup)="filterEmployees($event)">
|
||||
<div matSuffix class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center">
|
||||
<mat-progress-spinner [diameter]="20" mode="indeterminate"
|
||||
[class.!opacity-0]="!isSearching"
|
||||
[class.!opacity-100]="isSearching"
|
||||
class="!transition-opacity"></mat-progress-spinner>
|
||||
@if (employees$ | async; as employees) {
|
||||
<div class="!space-y-6">
|
||||
<div class="!flex !justify-between !items-center">
|
||||
<div class="!flex !items-center !gap-4">
|
||||
<h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">
|
||||
Employee Directory
|
||||
</h2>
|
||||
<mat-form-field class="!m-0" subscriptSizing="dynamic">
|
||||
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Search employees..."
|
||||
(keyup)="filterEmployees($event)"
|
||||
/>
|
||||
<div
|
||||
matSuffix
|
||||
class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center"
|
||||
>
|
||||
<mat-progress-spinner
|
||||
[diameter]="20"
|
||||
mode="indeterminate"
|
||||
[class.!opacity-0]="!isSearching"
|
||||
[class.!opacity-100]="isSearching"
|
||||
class="!transition-opacity"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="!bg-blue-600 !text-white !shrink-0"
|
||||
(click)="showCreateEmployeeModal()"
|
||||
>
|
||||
<mat-icon class="!mr-2">add</mat-icon>
|
||||
Add Employee
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (employees) {
|
||||
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
|
||||
<table mat-table [dataSource]="employees" matSort class="!w-full">
|
||||
<ng-container matColumnDef="name">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
class="!text-left !w-full"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let employee" class="!py-4">
|
||||
<div class="!flex !items-center">
|
||||
<div
|
||||
class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3"
|
||||
>
|
||||
<span class="!text-blue-600 !font-medium">
|
||||
{{ employee.firstName[0] }}{{ employee.lastName[0] }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="text-blue-600 hover:underline cursor-pointer"
|
||||
[matTooltip]="'View Employee details'"
|
||||
(click)="openDetailModal(employee)"
|
||||
(keydown.enter)="openDetailModal(employee)"
|
||||
>
|
||||
{{ employee.lastName }}, {{ employee.firstName }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
class="!text-right !w-[120px]"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
<td
|
||||
mat-cell
|
||||
*matCellDef="let employee"
|
||||
class="!text-right !py-4 !whitespace-nowrap"
|
||||
>
|
||||
<div class="!flex !justify-end !items-center !gap-1">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
[matTooltip]="'Edit employee'"
|
||||
(click)="showEditEmployeeModal(employee)"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
[matTooltip]="'Delete employee'"
|
||||
(click)="openDeleteDialogue(employee)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-card class="!text-center !py-8">
|
||||
<mat-card-content>
|
||||
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">people</mat-icon>
|
||||
<p class="!text-gray-600">No employees found</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white !shrink-0"
|
||||
(click)="showCreateEmployeeModal()">
|
||||
<mat-icon class="!mr-2">add</mat-icon>
|
||||
Add Employee
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (employees) {
|
||||
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
|
||||
<table mat-table [dataSource]="employees" matSort class="!w-full">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef class="!text-left !w-full"> Name</th>
|
||||
<td mat-cell *matCellDef="let employee" class="!py-4">
|
||||
<div class="!flex !items-center">
|
||||
<div class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3">
|
||||
<span class="!text-blue-600 !font-medium">
|
||||
{{ employee.firstName[0] }}{{ employee.lastName[0] }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<a class="!text-blue-600 hover:!underline cursor-pointer"
|
||||
[matTooltip]="'Click to view Employee details'" (click)="openDetailModal(employee)">
|
||||
{{ employee.lastName }}, {{ employee.firstName }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="!text-right !w-[120px]"> Actions</th>
|
||||
<td mat-cell *matCellDef="let employee" class="!text-right !py-4 !whitespace-nowrap">
|
||||
<div class="!flex !justify-end !items-center !gap-1">
|
||||
<button mat-icon-button color="primary" [matTooltip]="'Edit employee'"
|
||||
(click)="showEditEmployeeModal(employee)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" [matTooltip]="'Delete employee'"
|
||||
(click)="openDeleteDialogue(employee)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-card class="!text-center !py-8">
|
||||
<mat-card-content>
|
||||
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">people</mat-icon>
|
||||
<p class="!text-gray-600">No employees found</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @placeholder {
|
||||
<div class="!space-y-6">
|
||||
<div class="!animate-pulse">
|
||||
<div class="!flex !justify-between !items-center !mb-8">
|
||||
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
|
||||
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
|
||||
</div>
|
||||
<div class="!bg-gray-50 !p-4 !rounded-lg">
|
||||
<div class="!space-y-4">
|
||||
<div class="!h-10 !bg-gray-200 !rounded"></div>
|
||||
@for (i of [1, 2, 3]; track i) {
|
||||
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
|
||||
}
|
||||
<div class="!space-y-6">
|
||||
<div class="!animate-pulse">
|
||||
<div class="!flex !justify-between !items-center !mb-8">
|
||||
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
|
||||
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
|
||||
</div>
|
||||
<div class="!bg-gray-50 !p-4 !rounded-lg">
|
||||
<div class="!space-y-4">
|
||||
<div class="!h-10 !bg-gray-200 !rounded"></div>
|
||||
@for (i of [1, 2, 3]; track i) {
|
||||
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @error {
|
||||
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error loading the employees.</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">Please try refreshing the page.</p>
|
||||
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
|
||||
>error</mat-icon
|
||||
>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
There was an error loading the employees.
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">
|
||||
Please try refreshing the page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @loading {
|
||||
<div class="!flex !justify-center !items-center !py-12">
|
||||
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||
</div>
|
||||
<div class="!flex !justify-center !items-center !py-12">
|
||||
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</section>
|
||||
|
@ -1,28 +1,36 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {catchError, debounceTime, distinctUntilChanged, Observable, of, retry, Subject} from 'rxjs';
|
||||
import {HttpErrorResponse} from '@angular/common/http';
|
||||
import {Employee} from '../Employee';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
Observable,
|
||||
of,
|
||||
retry,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Employee } from '../Employee';
|
||||
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
import {MatSnackBarModule} from '@angular/material/snack-bar';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {MatTableModule} from '@angular/material/table';
|
||||
import {MatSortModule} from '@angular/material/sort';
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {DeleteComponent} from "../delete/delete.component";
|
||||
import EmployeeApiService from "../../services/employee-api.service";
|
||||
import {CreateComponent} from "../create/create.component";
|
||||
import {EditComponent} from "../edit/edit.component";
|
||||
import {DetailsComponent} from "../details/details.component";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {ErrorHandlerService} from "../../services/error.handler.service";
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DeleteComponent } from '../delete/delete.component';
|
||||
import EmployeeApiService from '../../services/employee-api.service';
|
||||
import { CreateComponent } from '../create/create.component';
|
||||
import { EditComponent } from '../edit/edit.component';
|
||||
import { DetailsComponent } from '../details/details.component';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ErrorHandlerService } from '../../services/error.handler.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-employee-list',
|
||||
@ -43,12 +51,13 @@ import {ErrorHandlerService} from "../../services/error.handler.service";
|
||||
MatInputModule,
|
||||
],
|
||||
templateUrl: './table.component.html',
|
||||
styleUrl: './table.component.css'
|
||||
styleUrl: './table.component.css',
|
||||
})
|
||||
export class TableComponent implements OnInit {
|
||||
private readonly apiService: EmployeeApiService = inject(EmployeeApiService);
|
||||
private readonly matDialog: MatDialog = inject(MatDialog);
|
||||
private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService);
|
||||
private readonly errorHandlerService: ErrorHandlerService =
|
||||
inject(ErrorHandlerService);
|
||||
|
||||
private static readonly MAX_RETRIES = 3;
|
||||
|
||||
@ -64,27 +73,27 @@ export class TableComponent implements OnInit {
|
||||
}
|
||||
|
||||
private loadEmployees(): void {
|
||||
this.fetchEmployees().subscribe(employees => {
|
||||
this.fetchEmployees().subscribe((employees) => {
|
||||
this.allEmployees = employees;
|
||||
this.employees$ = of(employees);
|
||||
});
|
||||
}
|
||||
|
||||
private setupSearch(): void {
|
||||
this.searchSubject.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged()
|
||||
).subscribe(searchTerm => {
|
||||
this.isSearching = true;
|
||||
setTimeout(() => {
|
||||
const filteredEmployees = this.allEmployees.filter(employee =>
|
||||
employee.firstName?.toLowerCase().includes(searchTerm) ||
|
||||
employee.lastName?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
this.employees$ = of(filteredEmployees);
|
||||
this.isSearching = false;
|
||||
}, 150);
|
||||
});
|
||||
this.searchSubject
|
||||
.pipe(debounceTime(300), distinctUntilChanged())
|
||||
.subscribe((searchTerm) => {
|
||||
this.isSearching = true;
|
||||
setTimeout(() => {
|
||||
const filteredEmployees = this.allEmployees.filter(
|
||||
(employee) =>
|
||||
employee.firstName?.toLowerCase().includes(searchTerm) ||
|
||||
employee.lastName?.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
this.employees$ = of(filteredEmployees);
|
||||
this.isSearching = false;
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
private fetchEmployees(): Observable<Employee[]> {
|
||||
@ -92,14 +101,17 @@ export class TableComponent implements OnInit {
|
||||
retry(TableComponent.MAX_RETRIES),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
console.error('Error fetching employees:', error);
|
||||
this.errorHandlerService.showErrorMessage('Failed to load employees. Please try again.');
|
||||
this.errorHandlerService.showErrorMessage(
|
||||
'Failed to load employees. Please try again.',
|
||||
);
|
||||
return of([]);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected openDeleteDialogue(employee: Employee): void {
|
||||
this.matDialog.open(DeleteComponent, {data: employee})
|
||||
this.matDialog
|
||||
.open(DeleteComponent, { data: employee })
|
||||
.afterClosed()
|
||||
.subscribe((deleted: boolean) => {
|
||||
if (deleted) {
|
||||
@ -109,7 +121,8 @@ export class TableComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected showCreateEmployeeModal() {
|
||||
this.matDialog.open(CreateComponent)
|
||||
this.matDialog
|
||||
.open(CreateComponent)
|
||||
.afterClosed()
|
||||
.subscribe((created: boolean) => {
|
||||
if (created) {
|
||||
@ -119,7 +132,8 @@ export class TableComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected showEditEmployeeModal(employee: Employee) {
|
||||
this.matDialog.open(EditComponent, {data: employee})
|
||||
this.matDialog
|
||||
.open(EditComponent, { data: employee })
|
||||
.afterClosed()
|
||||
.subscribe((edited: boolean) => {
|
||||
if (edited) {
|
||||
@ -129,7 +143,7 @@ export class TableComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected openDetailModal(employee: Employee) {
|
||||
this.matDialog.open(DetailsComponent, {data: employee});
|
||||
this.matDialog.open(DetailsComponent, { data: employee });
|
||||
}
|
||||
|
||||
protected filterEmployees(event: Event): void {
|
||||
|
@ -1,2 +1,2 @@
|
||||
<app-employee-list></app-employee-list>
|
||||
<app-qualifications></app-qualifications>
|
||||
<app-employee-list></app-employee-list>
|
||||
<app-qualifications></app-qualifications>
|
||||
|
@ -1,16 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {TableComponent} from "../employee/table/table.component";
|
||||
import {QualificationsComponent} from "../qualification/table/table.component";
|
||||
import { TableComponent } from '../employee/table/table.component';
|
||||
import { QualificationsComponent } from '../qualification/table/table.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [
|
||||
TableComponent,
|
||||
QualificationsComponent
|
||||
],
|
||||
imports: [TableComponent, QualificationsComponent],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.css'
|
||||
styleUrl: './home.component.css',
|
||||
})
|
||||
export class HomeComponent {
|
||||
|
||||
}
|
||||
export class HomeComponent {}
|
||||
|
@ -1 +1,3 @@
|
||||
<div class="dot-loader">Logging in<span>{{ dots }}</span></div>
|
||||
<div class="dot-loader">
|
||||
Logging in<span>{{ dots }}</span>
|
||||
</div>
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {interval, Subscription} from "rxjs";
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { interval, Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
imports: [],
|
||||
templateUrl: './login.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './login.component.css'
|
||||
styleUrl: './login.component.css',
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy{
|
||||
dots: string = '';
|
||||
private maxDots: number = 4; // Maximum number of dots
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
dots = '';
|
||||
private maxDots = 4; // Maximum number of dots
|
||||
private intervalSub!: Subscription;
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -1,4 +1,6 @@
|
||||
export class Qualification {
|
||||
constructor(public id: number, public skill?: string) {
|
||||
}
|
||||
constructor(
|
||||
public id: number,
|
||||
public skill?: string,
|
||||
) {}
|
||||
}
|
||||
|
@ -1,15 +1,30 @@
|
||||
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Create Qualification</h2>
|
||||
<h2
|
||||
mat-dialog-title
|
||||
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
|
||||
>
|
||||
Create Qualification
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="!px-3 md:!px-6">
|
||||
<form [formGroup]="qualificationForm" (ngSubmit)="create()" class="w-full min-w-[280px] md:min-w-[400px]">
|
||||
<form
|
||||
[formGroup]="qualificationForm"
|
||||
(ngSubmit)="create()"
|
||||
class="w-full min-w-[280px] md:min-w-[400px]"
|
||||
>
|
||||
<div class="space-y-4 md:space-y-6">
|
||||
@if (apiErrorMessage) {
|
||||
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon>
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
|
||||
>error</mat-icon
|
||||
>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error creating the qualification.</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiErrorMessage }}</p>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
There was an error creating the qualification.
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">
|
||||
{{ apiErrorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -18,27 +33,36 @@
|
||||
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3">
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Skill</mat-label>
|
||||
<input matInput
|
||||
formControlName="skill"
|
||||
placeholder="Enter skill name"
|
||||
required>
|
||||
<input
|
||||
matInput
|
||||
formControlName="skill"
|
||||
placeholder="Enter skill name"
|
||||
required
|
||||
/>
|
||||
<mat-hint class="text-sm">Enter the skill name</mat-hint>
|
||||
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
|
||||
{{ getErrorMessage('skill') }}
|
||||
{{ getErrorMessage("skill") }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
|
||||
<button mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1">
|
||||
<mat-dialog-actions
|
||||
align="end"
|
||||
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
@ -1,86 +1,94 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {FormBuilder, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import QualificationService from '../../services/qualification.service';
|
||||
import {
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {NgIf} from "@angular/common";
|
||||
import {MatError, MatFormField, MatHint, MatLabel} from "@angular/material/form-field";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {filter} from "rxjs";
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {
|
||||
MatError,
|
||||
MatFormField,
|
||||
MatHint,
|
||||
MatLabel,
|
||||
} from '@angular/material/form-field';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-qualification',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatError,
|
||||
NgIf,
|
||||
MatLabel,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatFormField,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatInput,
|
||||
MatDialogClose,
|
||||
MatHint,
|
||||
MatIcon
|
||||
],
|
||||
templateUrl: './create.component.html',
|
||||
styleUrl: './create.component.css'
|
||||
selector: 'app-create-qualification',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatError,
|
||||
NgIf,
|
||||
MatLabel,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatFormField,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatInput,
|
||||
MatDialogClose,
|
||||
MatHint,
|
||||
MatIcon,
|
||||
],
|
||||
templateUrl: './create.component.html',
|
||||
styleUrl: './create.component.css',
|
||||
})
|
||||
export class CreateComponent {
|
||||
private formBuilder: FormBuilder = inject(FormBuilder);
|
||||
private qualificationService: QualificationService = inject(QualificationService);
|
||||
private dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef);
|
||||
private formBuilder: FormBuilder = inject(FormBuilder);
|
||||
private qualificationService: QualificationService =
|
||||
inject(QualificationService);
|
||||
private dialogRef: MatDialogRef<CreateComponent> = inject(MatDialogRef);
|
||||
|
||||
public apiErrorMessage: string = '';
|
||||
public apiErrorMessage = '';
|
||||
|
||||
qualificationForm = this.formBuilder.group({
|
||||
'skill': ['', Validators.required],
|
||||
qualificationForm = this.formBuilder.group({
|
||||
skill: ['', Validators.required],
|
||||
});
|
||||
|
||||
isFieldInvalid(fieldName: string): boolean {
|
||||
const field = this.qualificationForm.get(fieldName);
|
||||
|
||||
if (!field) {
|
||||
throw new Error('Form field does not exist: ' + fieldName);
|
||||
}
|
||||
|
||||
return field.invalid && (field.dirty || field.touched);
|
||||
}
|
||||
|
||||
getErrorMessage(fieldName: string): string {
|
||||
const field = this.qualificationForm.get(fieldName);
|
||||
|
||||
if (field?.errors?.['required']) {
|
||||
return 'This field is required';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
create() {
|
||||
if (!this.qualificationForm.valid) {
|
||||
console.error('Validation failed');
|
||||
return;
|
||||
}
|
||||
|
||||
const qualification = {
|
||||
skill: this.qualificationForm.value.skill || '',
|
||||
};
|
||||
|
||||
this.qualificationService.create(qualification).subscribe({
|
||||
next: (createdQualification) => {
|
||||
this.dialogRef.close(createdQualification);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating qualification:', error);
|
||||
this.apiErrorMessage = 'API Error';
|
||||
},
|
||||
});
|
||||
|
||||
isFieldInvalid(fieldName: string): boolean {
|
||||
const field = this.qualificationForm.get(fieldName);
|
||||
|
||||
if (!field) {
|
||||
throw new Error('Form field does not exist: ' + fieldName)
|
||||
}
|
||||
|
||||
return field.invalid && (field.dirty || field.touched);
|
||||
}
|
||||
|
||||
getErrorMessage(fieldName: string): string {
|
||||
const field = this.qualificationForm.get(fieldName);
|
||||
|
||||
if (field?.errors?.['required']) {
|
||||
return 'This field is required';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
create() {
|
||||
if (!this.qualificationForm.valid) {
|
||||
console.error('Validation failed');
|
||||
return;
|
||||
}
|
||||
|
||||
this.qualificationService.create(this.qualificationForm.value).subscribe({
|
||||
next: (createdQualification) => {
|
||||
this.dialogRef.close(createdQualification);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating qualification:', error);
|
||||
|
||||
this.apiErrorMessage = 'API Error';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,62 @@
|
||||
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Delete Qualification</h2>
|
||||
<h2
|
||||
mat-dialog-title
|
||||
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
|
||||
>
|
||||
Delete Qualification
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="!px-3 md:!px-6">
|
||||
<div class="w-full min-w-[280px] md:min-w-[400px] space-y-4 md:space-y-6">
|
||||
@if (apiError) {
|
||||
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon>
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
|
||||
>error</mat-icon
|
||||
>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error deleting the qualification.</p>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
There was an error deleting the qualification.
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<div class="bg-amber-50 p-3 md:p-4 rounded-lg border border-amber-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8">warning</mat-icon>
|
||||
<mat-icon class="text-amber-600 text-xl md:text-2xl !w-8 !h-8"
|
||||
>warning</mat-icon
|
||||
>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">Are you sure you want to delete this qualification?</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">This action cannot be undone.</p>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
Are you sure you want to delete this qualification?
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
|
||||
<button mat-button
|
||||
(click)="closeModal()"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1">
|
||||
<mat-dialog-actions
|
||||
align="end"
|
||||
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
(click)="closeModal()"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-flat-button
|
||||
color="warn"
|
||||
(click)="delete()"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
|
||||
cdkFocusInitial>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="warn"
|
||||
(click)="delete()"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
|
||||
cdkFocusInitial
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
@ -1,17 +1,16 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {HttpErrorResponse} from "@angular/common/http";
|
||||
import { MatError } from '@angular/material/form-field'
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import QualificationService from '../../services/qualification.service';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-qualification',
|
||||
@ -22,17 +21,18 @@ import {MatIcon} from "@angular/material/icon";
|
||||
ReactiveFormsModule,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatIcon
|
||||
MatIcon,
|
||||
],
|
||||
templateUrl: './delete.component.html',
|
||||
standalone: true,
|
||||
styleUrl: './delete.component.css'
|
||||
styleUrl: './delete.component.css',
|
||||
})
|
||||
export class DeleteComponent {
|
||||
public id: number = inject(MAT_DIALOG_DATA);
|
||||
public apiError: string | null = null;
|
||||
|
||||
private qualificationService: QualificationService = inject(QualificationService);
|
||||
private qualificationService: QualificationService =
|
||||
inject(QualificationService);
|
||||
private dialogRef: MatDialogRef<DeleteComponent> = inject(MatDialogRef);
|
||||
|
||||
delete() {
|
||||
@ -45,11 +45,12 @@ export class DeleteComponent {
|
||||
|
||||
if (error.error.message.includes('SQL')) {
|
||||
// The API message is undescriptive but this is the most common
|
||||
this.apiError = 'This qualification cannot be deleted because it is currently assigned to one or more employees';
|
||||
this.apiError =
|
||||
'This qualification cannot be deleted because it is currently assigned to one or more employees';
|
||||
} else {
|
||||
this.apiError = 'API Error';
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">
|
||||
<h2
|
||||
mat-dialog-title
|
||||
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
|
||||
>
|
||||
{{ qualification.skill }} Developers
|
||||
</h2>
|
||||
|
||||
@ -7,25 +10,37 @@
|
||||
@if (employees$ | async; as employees) {
|
||||
@if (employees.length === 0) {
|
||||
<div class="bg-gray-50 p-3 md:p-4 rounded-lg text-center">
|
||||
<mat-icon class="text-gray-400 text-xl md:text-2xl mb-2 !w-8 !h-8">person_off</mat-icon>
|
||||
<p class="text-gray-600 text-sm md:text-base">No employees found with this qualification.</p>
|
||||
<mat-icon class="text-gray-400 text-xl md:text-2xl mb-2 !w-8 !h-8"
|
||||
>person_off</mat-icon
|
||||
>
|
||||
<p class="text-gray-600 text-sm md:text-base">
|
||||
No employees found with this qualification.
|
||||
</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-2">
|
||||
@for (employee of employees; track employee.id) {
|
||||
<a class="block w-full p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors cursor-pointer border border-gray-100 hover:border-blue-100"
|
||||
(click)="openEmployeeDetailsModal(employee.id)">
|
||||
<button
|
||||
class="block w-full p-3 bg-white rounded-lg hover:bg-blue-50 transition-colors cursor-pointer border border-gray-100 hover:border-blue-100 text-left"
|
||||
(click)="openEmployeeDetailsModal(employee.id)"
|
||||
(keydown.enter)="openEmployeeDetailsModal(employee.id)"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="h-8 w-8 md:h-10 md:w-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<div
|
||||
class="h-8 w-8 md:h-10 md:w-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<span class="text-blue-600 font-medium text-sm md:text-base">
|
||||
{{ employee.firstName?.charAt(0) }}{{ employee.lastName?.charAt(0) }}
|
||||
{{ employee.firstName?.charAt(0)
|
||||
}}{{ employee.lastName?.charAt(0) }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base">{{ employee.firstName }} {{ employee.lastName }}</span>
|
||||
<span class="font-medium text-gray-900 text-sm md:text-base"
|
||||
>{{ employee.firstName }} {{ employee.lastName }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@ -34,9 +49,11 @@
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-3 md:!px-6 !py-4 !mt-4 border-t">
|
||||
<button mat-button
|
||||
(click)="closeModal()"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full">
|
||||
<button
|
||||
mat-button
|
||||
(click)="closeModal()"
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
@ -1,19 +1,19 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import {Qualification} from "../Qualification";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {DetailsComponent as EmployeeDetailsComponent} from "../../employee/details/details.component";
|
||||
import EmployeeApiService from "../../services/employee-api.service";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import QualificationService from '../../services/qualification.service';
|
||||
import { Qualification } from '../Qualification';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { DetailsComponent as EmployeeDetailsComponent } from '../../employee/details/details.component';
|
||||
import EmployeeApiService from '../../services/employee-api.service';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details',
|
||||
@ -23,10 +23,10 @@ import {MatIcon} from "@angular/material/icon";
|
||||
MatDialogTitle,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatIcon
|
||||
MatIcon,
|
||||
],
|
||||
templateUrl: './details.component.html',
|
||||
styleUrl: './details.component.css'
|
||||
styleUrl: './details.component.css',
|
||||
})
|
||||
export class DetailsComponent {
|
||||
private qualificationService = inject(QualificationService);
|
||||
@ -35,7 +35,9 @@ export class DetailsComponent {
|
||||
private dialog: MatDialog = inject(MatDialog);
|
||||
|
||||
public qualification: Qualification = inject(MAT_DIALOG_DATA);
|
||||
public employees$ = this.qualificationService.findEmployees(this.qualification.id);
|
||||
public employees$ = this.qualificationService.findEmployees(
|
||||
this.qualification.id,
|
||||
);
|
||||
|
||||
closeModal() {
|
||||
this.dialogRef.close();
|
||||
@ -43,12 +45,12 @@ export class DetailsComponent {
|
||||
|
||||
openEmployeeDetailsModal(id: number | undefined) {
|
||||
if (!id) {
|
||||
throw new Error("ID must not be undefined");
|
||||
throw new Error('ID must not be undefined');
|
||||
}
|
||||
|
||||
this.employeeService.getById(id).subscribe(employee => {
|
||||
this.employeeService.getById(id).subscribe((employee) => {
|
||||
this.dialog.open(EmployeeDetailsComponent, {
|
||||
data: employee
|
||||
data: employee,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,15 +1,30 @@
|
||||
<h2 mat-dialog-title class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4">Edit Qualification</h2>
|
||||
<h2
|
||||
mat-dialog-title
|
||||
class="text-xl md:text-2xl font-semibold text-gray-800 mb-3 md:mb-4"
|
||||
>
|
||||
Edit Qualification
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="!px-3 md:!px-6">
|
||||
<form [formGroup]="qualificationForm" (ngSubmit)="edit()" class="w-full min-w-[280px] md:min-w-[400px]">
|
||||
<form
|
||||
[formGroup]="qualificationForm"
|
||||
(ngSubmit)="edit()"
|
||||
class="w-full min-w-[280px] md:min-w-[400px]"
|
||||
>
|
||||
<div class="space-y-4 md:space-y-6">
|
||||
@if (apiErrorMessage) {
|
||||
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon>
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
|
||||
>error</mat-icon
|
||||
>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error editing the qualification.</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">{{ apiErrorMessage }}</p>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
There was an error editing the qualification.
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">
|
||||
{{ apiErrorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -18,27 +33,36 @@
|
||||
<div class="bg-gray-50 p-3 md:p-4 rounded-lg space-y-3">
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Skill</mat-label>
|
||||
<input matInput
|
||||
formControlName="skill"
|
||||
<input
|
||||
matInput
|
||||
formControlName="skill"
|
||||
placeholder="Enter skill name"
|
||||
required>
|
||||
required
|
||||
/>
|
||||
<mat-hint class="text-sm">Enter the skill name</mat-hint>
|
||||
<mat-error *ngIf="isFieldInvalid('skill')" class="text-sm">
|
||||
{{ getErrorMessage('skill') }}
|
||||
{{ getErrorMessage("skill") }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions align="end" class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3">
|
||||
<button mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1">
|
||||
<mat-dialog-actions
|
||||
align="end"
|
||||
class="!px-0 !mb-0 flex flex-col sm:flex-row w-full gap-3"
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
mat-dialog-close
|
||||
class="text-sm md:text-base hover:bg-gray-100 py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
class="!ml-0 text-sm md:text-base py-2 px-4 md:px-6 rounded-md w-full sm:flex-1"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
@ -1,58 +1,70 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {FormBuilder, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions, MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {MatError, MatFormField, MatHint, MatLabel} from "@angular/material/form-field";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
import {NgIf} from "@angular/common";
|
||||
import {Qualification} from "../Qualification";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
FormBuilder,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import QualificationService from '../../services/qualification.service';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import {
|
||||
MatError,
|
||||
MatFormField,
|
||||
MatHint,
|
||||
MatLabel,
|
||||
} from '@angular/material/form-field';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Qualification } from '../Qualification';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-qualification',
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButton,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogTitle,
|
||||
MatError,
|
||||
MatFormField,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
MatDialogClose,
|
||||
MatHint,
|
||||
MatIcon
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButton,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogTitle,
|
||||
MatError,
|
||||
MatFormField,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
MatDialogClose,
|
||||
MatHint,
|
||||
MatIcon,
|
||||
],
|
||||
templateUrl: './edit.component.html',
|
||||
styleUrl: './edit.component.css'
|
||||
styleUrl: './edit.component.css',
|
||||
})
|
||||
export class EditComponent {
|
||||
public apiErrorMessage: string = '';
|
||||
public apiErrorMessage = '';
|
||||
public qualification: Qualification = inject(MAT_DIALOG_DATA);
|
||||
|
||||
private formBuilder: FormBuilder = inject(FormBuilder);
|
||||
private qualificationService: QualificationService = inject(QualificationService);
|
||||
private qualificationService: QualificationService =
|
||||
inject(QualificationService);
|
||||
private dialogRef: MatDialogRef<EditComponent> = inject(MatDialogRef);
|
||||
|
||||
qualificationForm = this.formBuilder.group({
|
||||
'skill': [this.qualification.skill, Validators.required],
|
||||
skill: [this.qualification.skill, Validators.required],
|
||||
});
|
||||
|
||||
isFieldInvalid(fieldName: string): boolean {
|
||||
const field = this.qualificationForm.get(fieldName);
|
||||
|
||||
if (!field) {
|
||||
throw new Error('Form field does not exist: ' + fieldName)
|
||||
throw new Error('Form field does not exist: ' + fieldName);
|
||||
}
|
||||
|
||||
return field.invalid && (field.dirty || field.touched);
|
||||
@ -74,15 +86,20 @@ export class EditComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.qualificationService.edit(this.qualification.id, this.qualificationForm.value).subscribe({
|
||||
next: (editedQualification) => {
|
||||
this.dialogRef.close(editedQualification);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creating qualification:', error);
|
||||
const qualification = {
|
||||
skill: this.qualificationForm.value.skill || '',
|
||||
};
|
||||
|
||||
this.apiErrorMessage = 'API Error';
|
||||
}
|
||||
});
|
||||
this.qualificationService
|
||||
.update(this.qualification.id, qualification)
|
||||
.subscribe({
|
||||
next: (editedQualification) => {
|
||||
this.dialogRef.close(editedQualification);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error updating qualification:', error);
|
||||
this.apiErrorMessage = 'API Error';
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,118 +1,170 @@
|
||||
<section class="!space-y-6 mb-6">
|
||||
@defer {
|
||||
@if (qualifications$ | async; as qualifications) {
|
||||
<div class="!space-y-6">
|
||||
<div class="!flex !justify-between !items-center">
|
||||
<div class="!flex !items-center !gap-4">
|
||||
<h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">Qualifications</h2>
|
||||
<mat-form-field class="!m-0" subscriptSizing="dynamic">
|
||||
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
|
||||
<input matInput
|
||||
type="text"
|
||||
placeholder="Search qualifications..."
|
||||
(keyup)="filterQualifications($event)">
|
||||
<div matSuffix class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center">
|
||||
<mat-progress-spinner [diameter]="20" mode="indeterminate"
|
||||
[class.!opacity-0]="!isSearching"
|
||||
[class.!opacity-100]="isSearching"
|
||||
class="!transition-opacity"></mat-progress-spinner>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" class="!bg-blue-600 !text-white !shrink-0"
|
||||
(click)="openCreateModal()">
|
||||
<mat-icon class="!mr-2">add</mat-icon>
|
||||
Add Qualification
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (qualifications) {
|
||||
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
|
||||
<table mat-table [dataSource]="qualifications" matSort class="!w-full">
|
||||
<ng-container matColumnDef="skill">
|
||||
<th mat-header-cell *matHeaderCellDef class="!text-left !w-full">Skill</th>
|
||||
<td mat-cell *matCellDef="let qualification" class="!py-4">
|
||||
<div class="!flex !items-center">
|
||||
<div class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3">
|
||||
<span class="!text-blue-600 !font-medium">
|
||||
{{ qualification.skill[0]?.toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<a class="!text-blue-600 hover:!underline cursor-pointer"
|
||||
[matTooltip]="'Click to view qualification details'"
|
||||
(click)="openDetailsModal(qualification)">
|
||||
{{ qualification.skill }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="!text-right !w-[120px]">Actions</th>
|
||||
<td mat-cell *matCellDef="let qualification" class="!text-right !py-4 !whitespace-nowrap">
|
||||
<div class="!flex !justify-end !items-center !gap-1">
|
||||
<button mat-icon-button
|
||||
color="primary"
|
||||
[matTooltip]="'Edit qualification'"
|
||||
(click)="openEditModal(qualification)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button
|
||||
color="warn"
|
||||
[matTooltip]="'Delete qualification'"
|
||||
(click)="openDeleteModal(qualification.id)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-card class="!text-center !py-8">
|
||||
<mat-card-content>
|
||||
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">school</mat-icon>
|
||||
<p class="!text-gray-600">No qualifications found</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @placeholder {
|
||||
<div class="!space-y-6">
|
||||
<div class="!animate-pulse">
|
||||
<div class="!flex !justify-between !items-center !mb-8">
|
||||
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
|
||||
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
|
||||
</div>
|
||||
<div class="!bg-gray-50 !p-4 !rounded-lg">
|
||||
<div class="!space-y-4">
|
||||
<div class="!h-10 !bg-gray-200 !rounded"></div>
|
||||
@for (i of [1, 2, 3]; track i) {
|
||||
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @error {
|
||||
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8">error</mat-icon>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">There was an error loading the qualifications.</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">Please try refreshing the page.</p>
|
||||
@defer {
|
||||
@if (qualifications$ | async; as qualifications) {
|
||||
<div class="!space-y-6">
|
||||
<div class="!flex !justify-between !items-center">
|
||||
<div class="!flex !items-center !gap-4">
|
||||
<h2 class="!text-2xl !font-semibold !text-gray-900 !shrink-0">
|
||||
Qualifications
|
||||
</h2>
|
||||
<mat-form-field class="!m-0" subscriptSizing="dynamic">
|
||||
<mat-icon matPrefix class="!text-gray-400 !mr-2">search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Search qualifications..."
|
||||
(keyup)="filterQualifications($event)"
|
||||
/>
|
||||
<div
|
||||
matSuffix
|
||||
class="!w-[24px] !h-[24px] !ml-2 !flex !items-center !justify-center"
|
||||
>
|
||||
<mat-progress-spinner
|
||||
[diameter]="20"
|
||||
mode="indeterminate"
|
||||
[class.!opacity-0]="!isSearching"
|
||||
[class.!opacity-100]="isSearching"
|
||||
class="!transition-opacity"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
} @loading {
|
||||
<div class="!flex !justify-center !items-center !py-12">
|
||||
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="!bg-blue-600 !text-white !shrink-0"
|
||||
(click)="openCreateModal()"
|
||||
>
|
||||
<mat-icon class="!mr-2">add</mat-icon>
|
||||
Add Qualification
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (qualifications) {
|
||||
<div class="!overflow-x-auto !rounded-lg !bg-gray-50 !p-4">
|
||||
<table
|
||||
mat-table
|
||||
[dataSource]="qualifications"
|
||||
matSort
|
||||
class="!w-full"
|
||||
>
|
||||
<ng-container matColumnDef="skill">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
class="!text-left !w-full"
|
||||
>
|
||||
Skill
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let qualification" class="!py-4">
|
||||
<div class="!flex !items-center">
|
||||
<div
|
||||
class="!h-10 !w-10 !rounded-full !bg-blue-100 !flex !items-center !justify-center !mr-3"
|
||||
>
|
||||
<span class="!text-blue-600 !font-medium">
|
||||
{{ qualification.skill[0]?.toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="text-blue-600 hover:underline cursor-pointer"
|
||||
[matTooltip]="'View qualification details'"
|
||||
(click)="openDetailsModal(qualification)"
|
||||
(keydown.enter)="openDetailsModal(qualification)"
|
||||
>
|
||||
{{ qualification.skill }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
class="!text-right !w-[120px]"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
<td
|
||||
mat-cell
|
||||
*matCellDef="let qualification"
|
||||
class="!text-right !py-4 !whitespace-nowrap"
|
||||
>
|
||||
<div class="!flex !justify-end !items-center !gap-1">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
[matTooltip]="'Edit qualification'"
|
||||
(click)="openEditModal(qualification)"
|
||||
(keydown.enter)="openEditModal(qualification)"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
[matTooltip]="'Delete qualification'"
|
||||
(click)="openDeleteModal(qualification.id)"
|
||||
(keydown.enter)="openDeleteModal(qualification.id)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-card class="!text-center !py-8">
|
||||
<mat-card-content>
|
||||
<mat-icon class="!w-8 !h-8 !text-gray-400 !mb-4">school</mat-icon>
|
||||
<p class="!text-gray-600">No qualifications found</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @placeholder {
|
||||
<div class="!space-y-6">
|
||||
<div class="!animate-pulse">
|
||||
<div class="!flex !justify-between !items-center !mb-8">
|
||||
<div class="!h-8 !bg-gray-200 !rounded !w-1/4"></div>
|
||||
<div class="!h-10 !bg-gray-200 !rounded !w-32"></div>
|
||||
</div>
|
||||
<div class="!bg-gray-50 !p-4 !rounded-lg">
|
||||
<div class="!space-y-4">
|
||||
<div class="!h-10 !bg-gray-200 !rounded"></div>
|
||||
@for (i of [1, 2, 3]; track i) {
|
||||
<div class="!h-16 !bg-white !rounded-lg !shadow-sm"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @error {
|
||||
<div class="bg-red-50 p-3 md:p-4 rounded-lg border border-red-200">
|
||||
<div class="flex items-start space-x-2 md:space-x-3">
|
||||
<mat-icon class="text-red-600 text-xl md:text-2xl !w-8 !h-8"
|
||||
>error</mat-icon
|
||||
>
|
||||
<div>
|
||||
<p class="text-gray-800 font-medium text-sm md:text-base">
|
||||
There was an error loading the qualifications.
|
||||
</p>
|
||||
<p class="text-gray-600 mt-1 text-xs md:text-sm">
|
||||
Please try refreshing the page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @loading {
|
||||
<div class="!flex !justify-center !items-center !py-12">
|
||||
<mat-spinner diameter="48" class="!text-blue-600"></mat-spinner>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
@ -1,143 +1,155 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {catchError, debounceTime, distinctUntilChanged, Observable, of, retry, Subject} from 'rxjs';
|
||||
import {HttpErrorResponse} from '@angular/common/http';
|
||||
import {Qualification} from "../Qualification";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import QualificationService from "../../services/qualification.service";
|
||||
import {CreateComponent} from "../create/create.component";
|
||||
import {EditComponent} from "../edit/edit.component";
|
||||
import {DeleteComponent} from "../delete/delete.component";
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
import {MatSnackBarModule} from '@angular/material/snack-bar';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {MatTableModule} from '@angular/material/table';
|
||||
import {MatSortModule} from '@angular/material/sort';
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {DetailsComponent} from "../details/details.component";
|
||||
import {ErrorHandlerService} from "../../services/error.handler.service";
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
Observable,
|
||||
of,
|
||||
retry,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Qualification } from '../Qualification';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import QualificationService from '../../services/qualification.service';
|
||||
import { CreateComponent } from '../create/create.component';
|
||||
import { EditComponent } from '../edit/edit.component';
|
||||
import { DeleteComponent } from '../delete/delete.component';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { DetailsComponent } from '../details/details.component';
|
||||
import { ErrorHandlerService } from '../../services/error.handler.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-qualifications',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSnackBarModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatMenuModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
],
|
||||
templateUrl: './table.component.html',
|
||||
styleUrl: './table.component.css'
|
||||
selector: 'app-qualifications',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSnackBarModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatMenuModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
],
|
||||
templateUrl: './table.component.html',
|
||||
styleUrl: './table.component.css',
|
||||
})
|
||||
export class QualificationsComponent implements OnInit {
|
||||
private readonly qualificationService: QualificationService = inject(QualificationService);
|
||||
private readonly errorHandlerService: ErrorHandlerService = inject(ErrorHandlerService);
|
||||
private readonly dialog: MatDialog = inject(MatDialog);
|
||||
private readonly qualificationService: QualificationService =
|
||||
inject(QualificationService);
|
||||
private readonly errorHandlerService: ErrorHandlerService =
|
||||
inject(ErrorHandlerService);
|
||||
private readonly dialog: MatDialog = inject(MatDialog);
|
||||
|
||||
private static readonly MAX_RETRIES = 3;
|
||||
private static readonly MAX_RETRIES = 3;
|
||||
|
||||
private allQualifications: Qualification[] = [];
|
||||
private searchSubject = new Subject<string>();
|
||||
public qualifications$: Observable<Qualification[]> = of([]);
|
||||
public isSearching = false;
|
||||
public readonly displayedColumns: string[] = ['skill', 'actions'];
|
||||
private allQualifications: Qualification[] = [];
|
||||
private searchSubject = new Subject<string>();
|
||||
public qualifications$: Observable<Qualification[]> = of([]);
|
||||
public isSearching = false;
|
||||
public readonly displayedColumns: string[] = ['skill', 'actions'];
|
||||
|
||||
ngOnInit() {
|
||||
this.loadQualifications();
|
||||
this.setupSearch();
|
||||
}
|
||||
ngOnInit() {
|
||||
this.loadQualifications();
|
||||
this.setupSearch();
|
||||
}
|
||||
|
||||
private loadQualifications(): void {
|
||||
this.fetchQualifications().subscribe(qualifications => {
|
||||
this.allQualifications = qualifications;
|
||||
this.qualifications$ = of(qualifications);
|
||||
});
|
||||
}
|
||||
private loadQualifications(): void {
|
||||
this.fetchQualifications().subscribe((qualifications) => {
|
||||
this.allQualifications = qualifications;
|
||||
this.qualifications$ = of(qualifications);
|
||||
});
|
||||
}
|
||||
|
||||
private setupSearch(): void {
|
||||
this.searchSubject.pipe(
|
||||
debounceTime(300),
|
||||
distinctUntilChanged()
|
||||
).subscribe(searchTerm => {
|
||||
this.isSearching = true;
|
||||
setTimeout(() => {
|
||||
const filteredQualifications = this.allQualifications.filter(qualification =>
|
||||
qualification.skill?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
this.qualifications$ = of(filteredQualifications);
|
||||
this.isSearching = false;
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
private setupSearch(): void {
|
||||
this.searchSubject
|
||||
.pipe(debounceTime(300), distinctUntilChanged())
|
||||
.subscribe((searchTerm) => {
|
||||
this.isSearching = true;
|
||||
setTimeout(() => {
|
||||
const filteredQualifications = this.allQualifications.filter(
|
||||
(qualification) =>
|
||||
qualification.skill?.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
this.qualifications$ = of(filteredQualifications);
|
||||
this.isSearching = false;
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
private fetchQualifications(): Observable<Qualification[]> {
|
||||
return this.qualificationService.getAll().pipe(
|
||||
retry(QualificationsComponent.MAX_RETRIES),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
console.error('Error fetching qualifications:', error);
|
||||
this.errorHandlerService.showErrorMessage('Failed to load qualifications. Please try again.');
|
||||
return of([]);
|
||||
})
|
||||
private fetchQualifications(): Observable<Qualification[]> {
|
||||
return this.qualificationService.getAll().pipe(
|
||||
retry(QualificationsComponent.MAX_RETRIES),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
console.error('Error fetching qualifications:', error);
|
||||
this.errorHandlerService.showErrorMessage(
|
||||
'Failed to load qualifications. Please try again.',
|
||||
);
|
||||
}
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected filterQualifications(event: Event): void {
|
||||
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase();
|
||||
this.searchSubject.next(searchTerm);
|
||||
}
|
||||
protected filterQualifications(event: Event): void {
|
||||
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase();
|
||||
this.searchSubject.next(searchTerm);
|
||||
}
|
||||
|
||||
openCreateModal() {
|
||||
const dialogRef = this.dialog.open(CreateComponent);
|
||||
openCreateModal() {
|
||||
const dialogRef = this.dialog.open(CreateComponent);
|
||||
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openEditModal(qualification: Qualification) {
|
||||
const dialogRef = this.dialog.open(EditComponent, {
|
||||
data: qualification
|
||||
});
|
||||
openEditModal(qualification: Qualification) {
|
||||
const dialogRef = this.dialog.open(EditComponent, {
|
||||
data: qualification,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openDeleteModal(id: number) {
|
||||
const dialogRef = this.dialog.open(DeleteComponent, {
|
||||
data: id
|
||||
});
|
||||
openDeleteModal(id: number) {
|
||||
const dialogRef = this.dialog.open(DeleteComponent, {
|
||||
data: id,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
dialogRef.afterClosed().subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.loadQualifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openDetailsModal(qualification: Qualification) {
|
||||
this.dialog.open(DetailsComponent, {
|
||||
data: qualification
|
||||
});
|
||||
}
|
||||
openDetailsModal(qualification: Qualification) {
|
||||
this.dialog.open(DetailsComponent, {
|
||||
data: qualification,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {AuthService} from "./auth.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {KeycloakService} from "keycloak-angular";
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuardService {
|
||||
constructor(public auth: AuthService, public router: Router) {
|
||||
}
|
||||
constructor(
|
||||
public auth: AuthService,
|
||||
public router: Router,
|
||||
) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {KeycloakService} from "keycloak-angular";
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private keycloakService = inject(KeycloakService);
|
||||
|
@ -1,11 +1,10 @@
|
||||
import {inject, Injectable} from "@angular/core";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {Observable} from "rxjs";
|
||||
import {Employee} from "../employee/Employee";
|
||||
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Employee } from '../employee/Employee';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export default class EmployeeApiService {
|
||||
private http: HttpClient = inject(HttpClient);
|
||||
@ -13,22 +12,30 @@ export default class EmployeeApiService {
|
||||
private static readonly BASE_URL = '/backend';
|
||||
|
||||
public getById(id: number): Observable<Employee> {
|
||||
return this.http.get(`${EmployeeApiService.BASE_URL}/employees/${id}`)
|
||||
return this.http.get(`${EmployeeApiService.BASE_URL}/employees/${id}`);
|
||||
}
|
||||
|
||||
public deleteById(id: number): Observable<Employee> {
|
||||
return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`)
|
||||
return this.http.delete(`${EmployeeApiService.BASE_URL}/employees/${id}`);
|
||||
}
|
||||
|
||||
public getAll(): Observable<Employee[]> {
|
||||
return this.http.get<Employee[]>(`${EmployeeApiService.BASE_URL}/employees`)
|
||||
return this.http.get<Employee[]>(
|
||||
`${EmployeeApiService.BASE_URL}/employees`,
|
||||
);
|
||||
}
|
||||
|
||||
public create(employee: Employee) {
|
||||
return this.http.post<Employee>(`${EmployeeApiService.BASE_URL}/employees`, employee)
|
||||
return this.http.post<Employee>(
|
||||
`${EmployeeApiService.BASE_URL}/employees`,
|
||||
employee,
|
||||
);
|
||||
}
|
||||
|
||||
public update(employee: Employee, id: number) {
|
||||
return this.http.put(`${EmployeeApiService.BASE_URL}/employees/${id}`, employee)
|
||||
return this.http.put(
|
||||
`${EmployeeApiService.BASE_URL}/employees/${id}`,
|
||||
employee,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ErrorHandlerService {
|
||||
private readonly snackBar: MatSnackBar = inject(MatSnackBar);
|
||||
@ -12,7 +12,7 @@ export class ErrorHandlerService {
|
||||
duration: 5000,
|
||||
horizontalPosition: 'end',
|
||||
verticalPosition: 'bottom',
|
||||
panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100']
|
||||
panelClass: ['!bg-red-50', '!text-red-900', '!border', '!border-red-100'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,53 @@
|
||||
import {inject, Injectable} from "@angular/core";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {map, Observable} from "rxjs";
|
||||
import {Qualification} from "../qualification/Qualification";
|
||||
import {Employee} from "../employee/Employee";
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Qualification } from '../qualification/Qualification';
|
||||
import { Employee } from '../employee/Employee';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export default class QualificationService {
|
||||
private http: HttpClient = inject(HttpClient);
|
||||
|
||||
private static readonly BASE_URL = '/backend';
|
||||
|
||||
public getAll(): Observable<Qualification[]> {
|
||||
return this.http.get<Qualification[]>(`${QualificationService.BASE_URL}/qualifications`).pipe(
|
||||
map(qualifications => qualifications.sort((a, b) => a.id - b.id))
|
||||
)
|
||||
}
|
||||
private readonly apiUrl = `${QualificationService.BASE_URL}/qualifications`;
|
||||
|
||||
public create(data: any) {
|
||||
return this.http.post(`${QualificationService.BASE_URL}/qualifications`, data)
|
||||
}
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
public edit(id: number, data: any) {
|
||||
return this.http.put(`${QualificationService.BASE_URL}/qualifications/${id}`, data)
|
||||
}
|
||||
|
||||
public delete(id: number) {
|
||||
return this.http.delete(`${QualificationService.BASE_URL}/qualifications/${id}`)
|
||||
}
|
||||
|
||||
public findEmployees(id: number): Observable<Employee[]> {
|
||||
return this.http.get<any>(`${QualificationService.BASE_URL}/qualifications/${id}/employees`)
|
||||
getAll(): Observable<Qualification[]> {
|
||||
return this.http
|
||||
.get<Qualification[]>(this.apiUrl)
|
||||
.pipe(
|
||||
map(response => response.employees)
|
||||
map((qualifications) => qualifications.sort((a, b) => a.id - b.id)),
|
||||
);
|
||||
}
|
||||
|
||||
getById(id: number): Observable<Qualification> {
|
||||
return this.http.get<Qualification>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
create(qualification: Omit<Qualification, 'id'>): Observable<Qualification> {
|
||||
return this.http.post<Qualification>(this.apiUrl, qualification);
|
||||
}
|
||||
|
||||
update(
|
||||
id: number,
|
||||
qualification: Partial<Qualification>,
|
||||
): Observable<Qualification> {
|
||||
return this.http.put<Qualification>(`${this.apiUrl}/${id}`, qualification);
|
||||
}
|
||||
|
||||
delete(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
findEmployees(id: number): Observable<Employee[]> {
|
||||
interface EmployeeResponse {
|
||||
employees: Employee[];
|
||||
}
|
||||
return this.http
|
||||
.get<EmployeeResponse>(`${this.apiUrl}/${id}/employees`)
|
||||
.pipe(map((response) => response.employees));
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Lf10StarterNew</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography bg-white">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Lf10StarterNew</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body class="mat-typography bg-white">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -2,5 +2,6 @@ import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
|
@ -2,5 +2,11 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{html,ts,css,scss,sass,less,styl}',
|
||||
],
|
||||
content: ["./src/**/*.{html,ts,css,scss,sass,less,styl}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -6,10 +6,6 @@
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
|
@ -19,10 +19,7 @@
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
]
|
||||
"lib": ["ES2022", "dom"]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
|
@ -4,12 +4,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
"types": ["jasmine"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user