From 996ed8ca056aa39274cf8a1fd4b5e6f0e6d35385 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ole=20K=C3=BCck?= <ole.kueck@hmmh.de>
Date: Mon, 20 Jan 2025 12:13:42 +0100
Subject: [PATCH] Feature: Qualification management

---
 src/app/app.routes.ts                         |   4 +-
 .../qualifications.component.html             |  54 ++++++
 .../qualifications.component.scss             |  48 ++++++
 .../qualifications.component.ts               | 158 ++++++++++++++++++
 4 files changed, 263 insertions(+), 1 deletion(-)
 create mode 100644 src/app/views/qualifications/qualifications.component.html
 create mode 100644 src/app/views/qualifications/qualifications.component.scss
 create mode 100644 src/app/views/qualifications/qualifications.component.ts

diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 87d779e..00a38ec 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,5 +1,6 @@
 import { Routes } from '@angular/router';
 import { DashboardComponent } from '@app/views/dashboard/dashboard.component';
+import {QualificationsComponent} from '@app/views/qualifications/qualifications.component';
 import { AuthService } from '@core/auth/auth.service';
 
 import { EmployeeDetailComponent } from './views/employee-detail/employee-detail.component';
@@ -7,5 +8,6 @@ import { EmployeeDetailComponent } from './views/employee-detail/employee-detail
 export const routes: Routes = [
     { path: '', component: DashboardComponent, title: 'Home' },
     { 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]}
 ];
diff --git a/src/app/views/qualifications/qualifications.component.html b/src/app/views/qualifications/qualifications.component.html
new file mode 100644
index 0000000..ef816aa
--- /dev/null
+++ b/src/app/views/qualifications/qualifications.component.html
@@ -0,0 +1,54 @@
+<div class="qualifications">
+    <div class="qualifications__action-row">
+        <mat-form-field>
+            <mat-label>Qualification</mat-label>
+            <input matInput>
+        </mat-form-field>
+        <button mat-fab class="shadowless">
+            <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>
+
diff --git a/src/app/views/qualifications/qualifications.component.scss b/src/app/views/qualifications/qualifications.component.scss
new file mode 100644
index 0000000..915d8e8
--- /dev/null
+++ b/src/app/views/qualifications/qualifications.component.scss
@@ -0,0 +1,48 @@
+.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{
+        flex-grow: 1;
+
+        form{
+          display: none;
+          padding-top: 0.25rem;
+        }
+
+        &.edit{
+         p{
+           display: none;
+         }
+
+          form{
+            display: block;
+          }
+        }
+      }
+
+      .mat-column-actions{
+        display: flex;
+        gap: 1rem;
+        padding: 0;
+        padding-top: 0.25rem;
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/app/views/qualifications/qualifications.component.ts b/src/app/views/qualifications/qualifications.component.ts
new file mode 100644
index 0000000..f4ab16b
--- /dev/null
+++ b/src/app/views/qualifications/qualifications.component.ts
@@ -0,0 +1,158 @@
+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 {DeleteModelComponent} from '@app/delete-model/delete-model.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);
+    }
+
+    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(DeleteModelComponent, {
+            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) => {
+                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.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);
+                    });
+                });
+            });
+        });
+    }
+}
-- 
2.45.3