Compare commits

..

No commits in common. "trunk" and "feature/employee-edit" have entirely different histories.

24 changed files with 53 additions and 735 deletions

1
.gitattributes vendored
View file

@ -1 +0,0 @@
* text=auto eol=lf

View file

@ -14,11 +14,4 @@ main {
min-width: 60rem; min-width: 60rem;
max-width: 80rem; max-width: 80rem;
} }
@media (max-width: 800px) {
width: 100%;
min-width: 100%;
max-width: 100%;
padding: 0.5rem;
}
} }

View file

@ -1,6 +1,5 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { DashboardComponent } from '@app/views/dashboard/dashboard.component'; import { DashboardComponent } from '@app/views/dashboard/dashboard.component';
import {QualificationsComponent} from '@app/views/qualifications/qualifications.component';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { EmployeeDetailComponent } from './views/employee-detail/employee-detail.component'; import { EmployeeDetailComponent } from './views/employee-detail/employee-detail.component';
@ -8,6 +7,5 @@ import { EmployeeDetailComponent } from './views/employee-detail/employee-detail
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: DashboardComponent, title: 'Home' }, { path: '', component: DashboardComponent, title: 'Home' },
{ path: 'employee/new', component: EmployeeDetailComponent, title: 'New Employee', canActivate: [AuthService] }, { path: 'employee/new', component: EmployeeDetailComponent, title: 'New Employee', canActivate: [AuthService] },
{ path: 'employee/:id', component: EmployeeDetailComponent, title: 'Edit Employee', canActivate: [AuthService] }, { path: 'employee/:id', component: EmployeeDetailComponent, title: 'Edit Employee', canActivate: [AuthService] }
{path: 'qualifications', component: QualificationsComponent, title: 'Qualifications', canActivate: [AuthService]}
]; ];

View file

@ -1,26 +1,28 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CanActivate, GuardResult, MaybeAsync, RedirectCommand, Router } from '@angular/router'; import { CanActivate, GuardResult, MaybeAsync, RedirectCommand, Router } from '@angular/router';
import UserData from '@core/auth/UserData'; import UserData from '@core/auth/UserData';
import { OpenAPI } from '@core/ems';
import { OidcSecurityService } from 'angular-auth-oidc-client'; import { OidcSecurityService } from 'angular-auth-oidc-client';
import { BehaviorSubject, Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { OpenAPI } from '../ems';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AuthService implements CanActivate { export class AuthService implements CanActivate {
public $user: BehaviorSubject<UserData | undefined>; public $user: Observable<UserData | undefined>;
constructor(private readonly oidcSecurityService: OidcSecurityService, private router: Router) { constructor(private readonly oidcSecurityService: OidcSecurityService, private router: Router) {
this.$user = new BehaviorSubject<UserData | undefined>(undefined); this.$user = new Observable((publish) => {
this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData, accessToken }) => { this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData }) => {
OpenAPI.TOKEN = accessToken; publish.next(isAuthenticated ? {
const isLoggedIn = isAuthenticated && userData != null && accessToken != '';
this.$user.next(isLoggedIn ? {
username: userData.preferred_username, username: userData.preferred_username,
verified: userData.email_verified verified: userData.email_verified
} : undefined); } : undefined);
}); });
});
this.oidcSecurityService.getAccessToken().subscribe(token => OpenAPI.TOKEN = token);
} }
canActivate(): MaybeAsync<GuardResult> { canActivate(): MaybeAsync<GuardResult> {

View file

@ -1,8 +0,0 @@
interface DeleteModalData {
title: string,
description: string,
cancel: string,
ok: string,
}
export default DeleteModalData;

View file

@ -1,8 +0,0 @@
<h2 mat-dialog-title>{{ data.title}}</h2>
<mat-dialog-content>
<p>{{data.description}}</p>
</mat-dialog-content>
<mat-dialog-actions class="delete-modal">
<button mat-flat-button [mat-dialog-close]="false">{{data.cancel}}</button>
<button mat-flat-button [mat-dialog-close]="true" class="warn">{{data.ok}}</button>
</mat-dialog-actions>

View file

@ -1,4 +0,0 @@
.delete-modal {
display: flex;
justify-content: space-between;
}

View file

@ -1,32 +0,0 @@
import {Component, Inject} from '@angular/core';
import {MatButton} from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogActions, MatDialogClose,
MatDialogContent,
MatDialogTitle
} from '@angular/material/dialog';
import DeleteModalData from '@app/delete-modal/DeleteModalData';
@Component({
selector: 'app-delete-model',
imports: [
MatDialogContent,
MatDialogActions,
MatDialogTitle,
MatButton,
MatDialogClose
],
templateUrl: './delete-modal.component.html',
styleUrl: './delete-modal.component.scss'
})
export class DeleteModalComponent {
constructor(
@Inject(MAT_DIALOG_DATA) protected data: DeleteModalData,
) {
}
}

View file

@ -1,9 +1,8 @@
<mat-toolbar class="header"> <mat-toolbar class="header">
<nav> <nav>
<a routerLink="" mat-button> <a routerLink="'/'" mat-button>
<mat-icon>badge</mat-icon> <mat-icon>badge</mat-icon>
EMS EMS</a>
</a>
@for (route of routes; track route) { @for (route of routes; track route) {
<a mat-button routerLink="{{ route.path }}" class="{{ route.class }}">{{ route.title }}</a> <a mat-button routerLink="{{ route.path }}" class="{{ route.class }}">{{ route.title }}</a>
} }

View file

@ -13,13 +13,6 @@
box-sizing: border-box; box-sizing: border-box;
list-style: none; list-style: none;
gap: 0.25rem; gap: 0.25rem;
@media (max-width: 800px) {
a:first-of-type {
display: none;
}
}
} }
a, a,
@ -35,11 +28,5 @@
&__login { &__login {
min-width: fit-content; min-width: fit-content;
} }
}
@media (max-width: 500px) {
>a:last-of-type {
display: none;
}
}
}

View file

@ -1,9 +1,9 @@
import { AsyncPipe, TitleCasePipe } from '@angular/common'; import {AsyncPipe, Location, TitleCasePipe} from '@angular/common';
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {MatAnchor, MatButton, MatIconButton} from '@angular/material/button'; import {MatAnchor, MatButton, MatIconButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon'; import {MatIcon} from '@angular/material/icon';
import {MatToolbar} from '@angular/material/toolbar'; import {MatToolbar} from '@angular/material/toolbar';
import { EventType, Router, RouterLink } from '@angular/router'; import {Router, RouterLink} from '@angular/router';
import {AuthService} from '@core/auth/auth.service'; import {AuthService} from '@core/auth/auth.service';
@Component({ @Component({
@ -26,24 +26,22 @@ export class HeaderComponent implements OnInit {
constructor( constructor(
private router: Router, private router: Router,
private location: Location,
protected auth: AuthService protected auth: AuthService
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
const routes = this.router.config this.routes = this.router.config
.filter(route => !route.path?.includes(':')) .filter(route => !route.path?.includes(':'))
.filter(route => !route.path?.includes('/')); .filter(route => !route.path?.includes('/'))
.map(route => ({
this.router.events.subscribe((event) => {
if (event.type != EventType.NavigationEnd) {
return;
}
this.routes = routes.map(route => ({
path: `/${route.path}`, path: `/${route.path}`,
title: route.title as string, title: route.title as string,
class: `/${route.path}` == event.url ? 'active' : '' class: `/${route.path}` == (this.location.path() || '/') ? 'active' : ''
})); }));
});
} }
} }

View file

@ -1,15 +0,0 @@
<h2 mat-dialog-title>{{ data.firstName }} {{ data.lastName }}</h2>
<mat-dialog-content class="info-modal">
<p>Street: {{data.street}}</p>
<p>Location: {{data.postcode}} {{data.city}}</p>
<p>Phone: <a href="tel:{{data.phone}}">{{data.phone}}</a></p>
<p class="info-modal__skills-label">Skills:</p>
<ul class="info-modal__skills">
@for (skill of data.skillSet; track skill){
<li>{{skill.skill}}</li>
}
</ul>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-flat-button [mat-dialog-close]="false">Close</button>
</mat-dialog-actions>

View file

@ -1,11 +0,0 @@
@media (min-width: 800px) {
.info-modal {
&__skills-label {
display: none;
}
&__skills {
display: none;
}
}
}

View file

@ -1,32 +0,0 @@
import {Component, Inject} from '@angular/core';
import {MatButton} from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogActions, MatDialogClose,
MatDialogContent,
MatDialogTitle
} from '@angular/material/dialog';
import {Employee} from '@core/ems';
@Component({
selector: 'app-delete-model',
imports: [
MatDialogContent,
MatDialogActions,
MatDialogTitle,
MatButton,
MatDialogClose
],
templateUrl: './info-modal.component.html',
styleUrl: './info-modal.component.scss'
})
export class InfoModalComponent {
constructor(
@Inject(MAT_DIALOG_DATA) protected data: Employee,
) {
}
}

View file

@ -12,11 +12,6 @@
padding: 0.5rem; padding: 0.5rem;
gap: 0.5rem; gap: 0.5rem;
@media (max-width: 20rem) {
min-width: calc(100% - 0.5rem);
padding: 0.25rem;
}
overflow: hidden; overflow: hidden;
&__card { &__card {

View file

@ -1,8 +0,0 @@
import {Qualification} from '@core/ems';
interface Filter {
fuzzy: string,
qualification: Qualification|undefined
}
export default Filter;

View file

@ -1,66 +1,4 @@
<div class="dashboard"> <a mat-flat-button routerLink="/employee/new">New Employee</a>
@if (auth.$user|async; as user) { <a mat-flat-button routerLink="/employee/1">Edit Employee 3</a>
<div class="dashboard__action-row"> <button mat-flat-button (click)="testInfo()">Test Info</button>
<mat-form-field> <button mat-flat-button (click)="testError()">Test Error</button>
<mat-label>Search</mat-label>
<input matInput (keyup)="onFuzzyFilter($event)">
</mat-form-field>
<mat-form-field>
<mat-label>Qualification</mat-label>
<mat-select [(value)]="selectedQualificationFilter" (valueChange)="onFilterUpdate()">
<mat-option>None</mat-option>
@for (skill of ($qualifications|async); track skill) {
<mat-option [value]="skill">{{ skill.skill }}</mat-option>
}
</mat-select>
</mat-form-field>
<button mat-fab class="shadowless" routerLink="employee/new">
<mat-icon>add</mat-icon>
</button>
</div>
<mat-table [dataSource]="employeeDataSource" class="dashboard__employees">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>Id</th>
<td mat-cell *matCellDef="let employee">{{ employee.id }}</td>
</ng-container>
<ng-container matColumnDef="first-name">
<th mat-header-cell *matHeaderCellDef>First Name</th>
<td mat-cell *matCellDef="let employee">{{ employee.firstName }}</td>
</ng-container>
<ng-container matColumnDef="last-name">
<th mat-header-cell *matHeaderCellDef>Last Name</th>
<td mat-cell *matCellDef="let employee">{{ employee.lastName }}</td>
</ng-container>
<ng-container matColumnDef="skills">
<th mat-header-cell *matHeaderCellDef>Skills</th>
<td mat-cell *matCellDef="let employee">
<ul>
@for (skill of employee.skillSet; track skill) {
<li>{{ skill.skill }}</li>
}
</ul>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let employee">
<button mat-mini-fab class="shadowless" (click)="onInfo(employee)">
<mat-icon>info</mat-icon>
</button>
<button mat-mini-fab class="shadowless" routerLink="employee/{{employee.id}}">
<mat-icon>edit</mat-icon>
</button>
<button mat-mini-fab class="shadowless warn" (click)="onDelete(employee)">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="employeesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: employeesDisplayedColumns;"></tr>
</mat-table>
} @else {
<p>Log in to see more!</p>
}
</div>

View file

@ -1,77 +0,0 @@
.dashboard {
display: flex;
flex-direction: column;
gap: 1rem;
&__action-row {
display: flex;
gap: 1rem;
:first-child {
flex-grow: 1;
}
@media (max-width: 800px) {
gap: 0.25rem;
}
}
&__employees {
width: 100%;
tr {
display: flex;
width: 100%;
height: auto;
.mat-column-id {
width: 4rem;
}
.mat-column-first-name,
.mat-column-last-name,
.mat-column-skills {
width: 0;
flex-grow: 1;
}
ul {
margin: 0;
padding-left: 1rem;
}
$action-count: 3;
$fap-size: 40px;
$gap-size: 1rem;
th.mat-column-actions {
width: calc($fap-size * $action-count + $gap-size * ($action-count - 1));
}
td.mat-column-actions {
gap: $gap-size;
display: flex;
padding: 0.25rem 0;
align-items: start;
}
@media (max-width: 800px) {
$gap-size: 0.25rem;
.mat-column-id,
.mat-column-skills {
display: none;
}
th.mat-column-actions {
width: calc($fap-size * $action-count + $gap-size * ($action-count - 1));
}
td.mat-column-actions {
gap: $gap-size;
}
}
}
}
}

View file

@ -1,127 +1,20 @@
import { AsyncPipe } from '@angular/common'; import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIcon } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { DeleteModalComponent } from '@app/delete-modal/delete-modal.component';
import {InfoModalComponent} from '@app/info-modal/info-modal.component';
import Filter from '@app/views/dashboard/Filter';
import { AuthService } from '@core/auth/auth.service';
import { Employee, EmployeeService, Qualification, QualificationService } from '@core/ems';
import { NotificationService, NotificationType } from '@core/notification/notification.service'; import { NotificationService, NotificationType } from '@core/notification/notification.service';
import { Observable, of } from 'rxjs';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
imports: [MatButtonModule, MatSelectModule, MatFormFieldModule, MatTableModule, ReactiveFormsModule, MatInputModule, MatIcon, RouterLink, AsyncPipe], imports: [MatButtonModule, RouterLink],
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss' styleUrl: './dashboard.component.scss'
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent {
selectedQualificationFilter: Qualification | undefined; constructor(private notifications: NotificationService) { }
fuzzyFilter: string = ''; testInfo() {
this.notifications.publish('Cake', NotificationType.Information);
employeeDataSource: MatTableDataSource<Employee> = new MatTableDataSource<Employee>([]);
employeesDisplayedColumns = ['id', 'first-name', 'last-name', 'skills', 'actions'];
$qualifications: Observable<Array<Qualification> | undefined> = of();
constructor(
protected auth: AuthService,
private employeeService: EmployeeService,
private qualificationService: QualificationService,
private dialog: MatDialog,
private notifications: NotificationService
) { }
ngOnInit(): void {
this.auth.$user.subscribe((user) => {
if (user === undefined) {
return;
}
this.$qualifications = this.qualificationService.getAllQualifications();
this.employeeService.getAllEmployees().subscribe((employees) => {
this.employeeDataSource = new MatTableDataSource(employees);
this.employeeDataSource.filterPredicate = (employee: Employee, rawFilter: string): boolean => {
const filter = JSON.parse(rawFilter) as Filter;
if (filter.fuzzy == '' && filter.qualification == undefined) {
return true;
} }
if ( testError() {
filter.qualification != undefined this.notifications.publish('Cake', NotificationType.Error);
&& !employee.skillSet.map((skill) => skill.id).includes(filter.qualification.id) }}
) {
return false;
}
if (
filter.fuzzy != ''
&& !employee.id.toString().includes(filter.fuzzy)
&& !employee.firstName.toLowerCase().includes(filter.fuzzy)
&& !employee.lastName.toLowerCase().includes(filter.fuzzy)
) {
return false;
}
return true;
};
});
});
}
onInfo(employee: Employee){
this.dialog.open(InfoModalComponent, {
data: employee
});
}
onDelete(employee: Employee) {
const dialogRef = this.dialog.open(DeleteModalComponent, {
data: {
title: `Delete ${employee.firstName} ${employee.lastName}`,
description: `Do you really want to delete ${employee.firstName} ${employee.lastName}?`,
cancel: 'No',
ok: 'Yes',
}
});
dialogRef.afterClosed().subscribe((accepted: boolean) => {
if (!accepted) {
return;
}
this.employeeService.deleteEmployee({ id: employee.id }).subscribe(() => {
const data = this.employeeDataSource.data;
const i = data.indexOf(employee);
if (i != -1) {
data.splice(i, 1);
this.employeeDataSource.data = data;
}
this.notifications.publish(`Deleted ${employee.firstName} ${employee.lastName}`, NotificationType.Information);
});
});
}
onFuzzyFilter(event: KeyboardEvent) {
this.fuzzyFilter = (event.target as HTMLInputElement).value;
this.onFilterUpdate();
}
onFilterUpdate() {
const skill = this.selectedQualificationFilter;
const fuzzy = this.fuzzyFilter;
if (skill == undefined && fuzzy == '') {
this.employeeDataSource.filter = '';
return;
}
this.employeeDataSource.filter = JSON.stringify({
qualification: skill,
fuzzy: fuzzy.toLowerCase()
} as Filter);
}
}

View file

@ -5,10 +5,6 @@
flex-direction: column; flex-direction: column;
gap: 4rem; gap: 4rem;
@media (max-width: 800px) {
gap: 1rem;
}
// Calculate Height and Spacing based on the Form Fields // Calculate Height and Spacing based on the Form Fields
$inner-form-height: 56px; $inner-form-height: 56px;
$form-height: 75.5px; $form-height: 75.5px;
@ -20,11 +16,6 @@
display: flex; display: flex;
gap: 4rem; gap: 4rem;
@media (max-width: 800px) {
flex-direction: column;
gap: 1rem;
}
>* { >* {
min-width: 0; min-width: 0;
flex-basis: 0; flex-basis: 0;

View file

@ -1,54 +0,0 @@
<div class="qualifications">
<div class="qualifications__action-row">
<mat-form-field>
<mat-label>Qualification</mat-label>
<input matInput #qualificationInput minlength="1" maxlength="25">
</mat-form-field>
<button mat-fab class="shadowless" (click)="onAdd(qualificationInput)">
<mat-icon>add</mat-icon>
</button>
</div>
<mat-table class="qualifications__view" [dataSource]="qualificationDataSource">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>Id</th>
<td mat-cell *matCellDef="let qualification">{{qualification.id}}</td>
</ng-container>
<ng-container matColumnDef="skill">
<th mat-header-cell *matHeaderCellDef>Skill</th>
<td mat-cell *matCellDef="let qualification" [class.edit]="isEditing(qualification.id)">
<p>{{qualification.skill}}</p>
<form [formGroup]="getSkillFormGroup(qualification)">
<mat-form-field>
<mat-label>Skill</mat-label>
<input matInput formControlName="skill" required>
<mat-error>Skill can't be empty</mat-error>
</mat-form-field>
</form>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let qualification">
@if (!isEditing(qualification.id)) {
<button mat-mini-fab class="shadowless" (click)="startEdit(qualification.id)">
<mat-icon>edit</mat-icon>
</button>
<button mat-mini-fab class="warn shadowless" (click)="onDelete(qualification)">
<mat-icon>delete</mat-icon>
</button>
} @else {
<button mat-mini-fab class="shadowless" (click)="endEdit(qualification, true)">
<mat-icon>save</mat-icon>
</button>
<button mat-mini-fab class="warn shadowless" (click)="endEdit(qualification, false)">
<mat-icon>cancel</mat-icon>
</button>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="qualificationsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: qualificationsDisplayedColumns;"></tr>
</mat-table>
</div>

View file

@ -1,51 +0,0 @@
.qualifications{
&__action-row{
display: flex;
gap: 1rem;
mat-form-field:first-of-type{
flex-grow: 1;
}
}
&__view{
tr{
display: flex;
width: 100%;
height: fit-content;
.mat-column-id{
width: 4rem;
}
.mat-column-skill{
display: flex;
flex-grow: 1;
form{
display: none;
width: 0;
flex-grow: 1;
padding-top: 0.25rem;
}
&.edit{
p{
display: none;
}
form{
display: inline-grid;
}
}
}
.mat-column-actions{
display: flex;
gap: 1rem;
padding: 0;
padding-top: 0.25rem;
}
}
}
}

View file

@ -1,175 +0,0 @@
import {Component} from '@angular/core';
import {FormBuilder, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatDialog} from '@angular/material/dialog';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIcon} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import {MatTableDataSource, MatTableModule} from '@angular/material/table';
import {DeleteModalComponent} from '@app/delete-modal/delete-modal.component';
import {
EmployeeQualifications,
EmployeeService,
Qualification,
QualificationEmployees,
QualificationService,
RemoveQualificationFromEmployeeResponse
} from '@core/ems';
import {NotificationService, NotificationType} from '@core/notification/notification.service';
import {forkJoin, Observable} from 'rxjs';
@Component({
selector: 'app-qualifications',
imports: [
MatIcon,
MatButtonModule,
MatInputModule,
MatSelectModule,
MatTableModule,
MatFormFieldModule,
ReactiveFormsModule
],
templateUrl: './qualifications.component.html',
styleUrl: './qualifications.component.scss'
})
export class QualificationsComponent {
qualificationDataSource: MatTableDataSource<Qualification> = new MatTableDataSource<Qualification>([]);
qualificationsDisplayedColumns = ['id', 'skill', 'actions'];
qualificationSkillFormGroups: Map<number, FormGroup> = new Map();
qualificationEdits: Map<number, boolean> = new Map();
constructor(
private qualificationService: QualificationService,
private employeeService: EmployeeService,
protected formBuilder: FormBuilder,
private notifications: NotificationService,
private dialog: MatDialog
) {
this.qualificationService.getAllQualifications().subscribe((qualifications) => {
this.qualificationDataSource = new MatTableDataSource<Qualification>(qualifications);
});
}
getSkillFormGroup(skill: Qualification): FormGroup {
let formGroup = this.qualificationSkillFormGroups.get(skill.id);
if (formGroup == undefined) {
formGroup = this.formBuilder.group(skill);
this.qualificationSkillFormGroups.set(skill.id, formGroup);
}
return formGroup;
}
save() {
}
isEditing(id: number): boolean {
const editing = this.qualificationEdits.get(id);
if (editing == undefined) {
return false;
}
return editing;
}
startEdit(id: number) {
this.qualificationEdits.set(id, true);
}
onAdd(qualificationField: HTMLInputElement){
this.qualificationService.createQualification({requestBody: {skill: qualificationField.value}}).subscribe((qualification)=>{
const data = this.qualificationDataSource.data;
data.push(qualification);
this.qualificationDataSource = new MatTableDataSource<Qualification>(data);
this.notifications.publish(`Added ${qualification.skill}`);
qualificationField.value='';
});
}
endEdit(oldQualification: Qualification, save: boolean) {
const qualificationFormGroup = this.qualificationSkillFormGroups.get(oldQualification.id);
if (qualificationFormGroup == undefined) {
return;
}
const qualification: Qualification = qualificationFormGroup.value;
if (!save) {
qualificationFormGroup.setValue(oldQualification);
this.qualificationEdits.set(oldQualification.id, false);
return;
}
if (qualificationFormGroup.invalid) {
return;
}
this.qualificationService.updateQualification({
id: oldQualification.id,
requestBody: {
skill: qualification.skill
}
}).subscribe(() => {
const data = this.qualificationDataSource.data;
const i = data.indexOf(oldQualification);
data[i] = qualification;
this.qualificationDataSource.data = data;
this.qualificationEdits.set(oldQualification.id, false);
this.notifications.publish(`Saved ${qualification.skill}`, NotificationType.Information);
});
}
onDelete(qualification: Qualification) {
const dialogRef = this.dialog.open(DeleteModalComponent, {
data: {
title: `Delete ${qualification.skill}`,
description: `Do you really want to delete ${qualification.skill}?`,
cancel: 'No',
ok: 'Yes',
}
});
dialogRef.afterClosed().subscribe((accepted: boolean) => {
if (!accepted) {
return;
}
this.qualificationService.getAllQualificationEmployees({id: qualification.id}).subscribe((employees: QualificationEmployees) => {
if (employees.employees.length==0){
this.execDelete(qualification);
return;
}
const requests: Array<Observable<RemoveQualificationFromEmployeeResponse>> = [];
for (const employee of employees.employees) {
requests.push(this.employeeService.removeQualificationFromEmployee({
employeeId: employee.id,
qualificationId: qualification.id
}));
}
forkJoin(requests).subscribe((employeesQualifications: Array<EmployeeQualifications>)=>{
for (const employee of employeesQualifications){
if (employee.skillSet?.map((q)=>q.id).includes(qualification.id)){
this.notifications.publish(`Could not remove ${qualification.skill} from ${employee.firstName} ${employee.lastName}`, NotificationType.Error);
return;
}
}
this.execDelete(qualification);
});
});
});
}
execDelete(qualification: Qualification){
this.qualificationService.deleteQualification({id: qualification.id}).subscribe(() => {
const data = this.qualificationDataSource.data;
const i = data.indexOf(qualification);
if (i != -1) {
data.splice(i, 1);
this.qualificationDataSource.data = data;
}
this.notifications.publish(`Deleted ${qualification.skill}`, NotificationType.Information);
});
}
}