Compare commits
No commits in common. "trunk" and "feature/employee-edit" have entirely different histories.
trunk
...
feature/em
24 changed files with 53 additions and 735 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +0,0 @@
|
||||||
* text=auto eol=lf
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]}
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
8
src/app/delete-modal/DeleteModalData.d.ts
vendored
8
src/app/delete-modal/DeleteModalData.d.ts
vendored
|
@ -1,8 +0,0 @@
|
||||||
interface DeleteModalData {
|
|
||||||
title: string,
|
|
||||||
description: string,
|
|
||||||
cancel: string,
|
|
||||||
ok: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DeleteModalData;
|
|
|
@ -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>
|
|
|
@ -1,4 +0,0 @@
|
||||||
.delete-modal {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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' : ''
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,11 +0,0 @@
|
||||||
@media (min-width: 800px) {
|
|
||||||
.info-modal {
|
|
||||||
&__skills-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__skills {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
8
src/app/views/dashboard/Filter.d.ts
vendored
8
src/app/views/dashboard/Filter.d.ts
vendored
|
@ -1,8 +0,0 @@
|
||||||
import {Qualification} from '@core/ems';
|
|
||||||
|
|
||||||
interface Filter {
|
|
||||||
fuzzy: string,
|
|
||||||
qualification: Qualification|undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Filter;
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue