Compare commits
19 commits
feature/em
...
trunk
Author | SHA1 | Date | |
---|---|---|---|
a9c8c79d0b | |||
e532d171c7 | |||
fd62dd02c4 | |||
523e5f4eee | |||
28bb4e220e | |||
a87d420f6d | |||
16a9aef3c5 | |||
76541fa2a8 | |||
264cd3f209 | |||
488990f634 | |||
2fe272c1e4 | |||
996ed8ca05 | |||
856d3e44b9 | |||
|
748df61dc6 | ||
c08df8c2d8 | |||
|
9d3be76744 | ||
c43d73aadb | |||
|
3bffc414ec | ||
3a528b0a01 |
24 changed files with 735 additions and 53 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=lf
|
|
@ -14,4 +14,11 @@ 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,17 +1,17 @@
|
||||||
import {provideHttpClient} from '@angular/common/http';
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
import {provideRouter} from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import {routes} from '@app/app.routes';
|
import { routes } from '@app/app.routes';
|
||||||
import {OpenAPI} from '@core/ems/core/OpenAPI';
|
import { OpenAPI } from '@core/ems/core/OpenAPI';
|
||||||
import {LogLevel, provideAuth} from 'angular-auth-oidc-client';
|
import { LogLevel, provideAuth } from 'angular-auth-oidc-client';
|
||||||
|
|
||||||
OpenAPI.BASE = 'http://localhost:8080';
|
OpenAPI.BASE = 'http://localhost:8080';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({eventCoalescing: true}),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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';
|
||||||
|
@ -7,5 +8,6 @@ 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,28 +1,26 @@
|
||||||
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 { Observable } from 'rxjs';
|
import { BehaviorSubject, 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: Observable<UserData | undefined>;
|
public $user: BehaviorSubject<UserData | undefined>;
|
||||||
|
|
||||||
constructor(private readonly oidcSecurityService: OidcSecurityService, private router: Router) {
|
constructor(private readonly oidcSecurityService: OidcSecurityService, private router: Router) {
|
||||||
this.$user = new Observable((publish) => {
|
this.$user = new BehaviorSubject<UserData | undefined>(undefined);
|
||||||
this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData }) => {
|
this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData, accessToken }) => {
|
||||||
publish.next(isAuthenticated ? {
|
OpenAPI.TOKEN = accessToken;
|
||||||
username: userData.preferred_username,
|
const isLoggedIn = isAuthenticated && userData != null && accessToken != '';
|
||||||
verified: userData.email_verified
|
this.$user.next(isLoggedIn ? {
|
||||||
} : undefined);
|
username: userData.preferred_username,
|
||||||
});
|
verified: userData.email_verified
|
||||||
|
} : undefined);
|
||||||
});
|
});
|
||||||
this.oidcSecurityService.getAccessToken().subscribe(token => OpenAPI.TOKEN = token);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivate(): MaybeAsync<GuardResult> {
|
canActivate(): MaybeAsync<GuardResult> {
|
||||||
|
|
8
src/app/delete-modal/DeleteModalData.d.ts
vendored
Normal file
8
src/app/delete-modal/DeleteModalData.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
interface DeleteModalData {
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
cancel: string,
|
||||||
|
ok: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeleteModalData;
|
8
src/app/delete-modal/delete-modal.component.html
Normal file
8
src/app/delete-modal/delete-modal.component.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<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>
|
4
src/app/delete-modal/delete-modal.component.scss
Normal file
4
src/app/delete-modal/delete-modal.component.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.delete-modal {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
32
src/app/delete-modal/delete-modal.component.ts
Normal file
32
src/app/delete-modal/delete-modal.component.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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,8 +1,9 @@
|
||||||
<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</a>
|
EMS
|
||||||
|
</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,6 +13,13 @@
|
||||||
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,
|
||||||
|
@ -28,5 +35,11 @@
|
||||||
&__login {
|
&__login {
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
>a:last-of-type {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import {AsyncPipe, Location, TitleCasePipe} from '@angular/common';
|
import { AsyncPipe, 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 {Router, RouterLink} from '@angular/router';
|
import { EventType, Router, RouterLink } from '@angular/router';
|
||||||
import {AuthService} from '@core/auth/auth.service';
|
import { AuthService } from '@core/auth/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
|
@ -26,22 +26,24 @@ 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 {
|
||||||
this.routes = this.router.config
|
const 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}` == (this.location.path() || '/') ? 'active' : ''
|
class: `/${route.path}` == event.url ? 'active' : ''
|
||||||
}));
|
}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
15
src/app/info-modal/info-modal.component.html
Normal file
15
src/app/info-modal/info-modal.component.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
11
src/app/info-modal/info-modal.component.scss
Normal file
11
src/app/info-modal/info-modal.component.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.info-modal {
|
||||||
|
&__skills-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__skills {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
src/app/info-modal/info-modal.component.ts
Normal file
32
src/app/info-modal/info-modal.component.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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,6 +12,11 @@
|
||||||
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
Normal file
8
src/app/views/dashboard/Filter.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {Qualification} from '@core/ems';
|
||||||
|
|
||||||
|
interface Filter {
|
||||||
|
fuzzy: string,
|
||||||
|
qualification: Qualification|undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Filter;
|
|
@ -1,4 +1,66 @@
|
||||||
<a mat-flat-button routerLink="/employee/new">New Employee</a>
|
<div class="dashboard">
|
||||||
<a mat-flat-button routerLink="/employee/1">Edit Employee 3</a>
|
@if (auth.$user|async; as user) {
|
||||||
<button mat-flat-button (click)="testInfo()">Test Info</button>
|
<div class="dashboard__action-row">
|
||||||
<button mat-flat-button (click)="testError()">Test Error</button>
|
<mat-form-field>
|
||||||
|
<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>
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
.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,20 +1,127 @@
|
||||||
import { Component } from '@angular/core';
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
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, RouterLink],
|
imports: [MatButtonModule, MatSelectModule, MatFormFieldModule, MatTableModule, ReactiveFormsModule, MatInputModule, MatIcon, RouterLink, AsyncPipe],
|
||||||
templateUrl: './dashboard.component.html',
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrl: './dashboard.component.scss'
|
styleUrl: './dashboard.component.scss'
|
||||||
})
|
})
|
||||||
export class DashboardComponent {
|
export class DashboardComponent implements OnInit {
|
||||||
constructor(private notifications: NotificationService) { }
|
selectedQualificationFilter: Qualification | undefined;
|
||||||
testInfo() {
|
fuzzyFilter: string = '';
|
||||||
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 (
|
||||||
|
filter.qualification != undefined
|
||||||
|
&& !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;
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
testError() {
|
onInfo(employee: Employee){
|
||||||
this.notifications.publish('Cake', NotificationType.Error);
|
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,6 +5,10 @@
|
||||||
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;
|
||||||
|
@ -16,6 +20,11 @@
|
||||||
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;
|
||||||
|
|
54
src/app/views/qualifications/qualifications.component.html
Normal file
54
src/app/views/qualifications/qualifications.component.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<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>
|
||||||
|
|
51
src/app/views/qualifications/qualifications.component.scss
Normal file
51
src/app/views/qualifications/qualifications.component.scss
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
175
src/app/views/qualifications/qualifications.component.ts
Normal file
175
src/app/views/qualifications/qualifications.component.ts
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
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