From 50ce7994080e28987e02129b02f7c01967673b97 Mon Sep 17 00:00:00 2001
From: Davo00 <57496007+Davo00@users.noreply.github.com>
Date: Thu, 30 Nov 2023 10:45:25 +0100
Subject: [PATCH] Refactoring/filter technician UI query api (#76)
* UI changes to display filtering by internal and crowd as well as by skill
* query service now adds skills in return -> filtering
* listening to selection and filtering resources based on multi-selection in skills dropdown
* ts lint
trigger list at removal
* swapped expansion panel, filter is now collapsed
* shared skill service to bind mandatory skills selection
* resources listen to mandatory skills selection; AND logic for filtering
* UI changes
* added change trigger to resource-query.component.ts
* ui adjustment
* filtering in SQL
* caps in SQL
* adjust request payload accordingly
* adjust request payload accordingly
no animation
* skills sync
* moved the search component
* cleaned up
---
workbench/frontend/src/app/app.module.ts | 5 +-
.../resource-query.component.html | 58 +++--
.../resource-query.component.scss | 17 ++
.../resource-query.component.ts | 229 +++++++++++++-----
.../src/app/common/services/query.service.ts | 70 +++---
.../common/services/shared-skill.service.ts | 14 ++
.../job-builder/job-builder.component.ts | 59 +++--
.../services/slot-booking.service.ts | 11 +-
.../slot-booking/slot-booking.component.html | 83 ++++---
.../slot-booking/slot-booking.component.scss | 18 +-
.../slot-booking/slot-booking.component.ts | 142 +++++++----
11 files changed, 472 insertions(+), 234 deletions(-)
mode change 100644 => 100755 workbench/frontend/src/app/common/components/resource-query/resource-query.component.html
create mode 100644 workbench/frontend/src/app/common/services/shared-skill.service.ts
diff --git a/workbench/frontend/src/app/app.module.ts b/workbench/frontend/src/app/app.module.ts
index a6bf683..d2b02b9 100644
--- a/workbench/frontend/src/app/app.module.ts
+++ b/workbench/frontend/src/app/app.module.ts
@@ -2,7 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MonacoEditorModule } from 'ngx-monaco-editor';
-import { ReactiveFormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
@@ -65,7 +65,8 @@ import { PluginEditorComponent } from './common/components/plugin-editor/plugin-
ReactiveFormsModule,
MonacoEditorModule.forRoot(ngxMonacoEditorConfig),
FlexLayoutModule,
- ...MatModules
+ ...MatModules,
+ FormsModule
],
providers: [
ConfigService,
diff --git a/workbench/frontend/src/app/common/components/resource-query/resource-query.component.html b/workbench/frontend/src/app/common/components/resource-query/resource-query.component.html
old mode 100644
new mode 100755
index 988fb82..43c8de7
--- a/workbench/frontend/src/app/common/components/resource-query/resource-query.component.html
+++ b/workbench/frontend/src/app/common/components/resource-query/resource-query.component.html
@@ -1,32 +1,60 @@
-
+
+
Resources: {{ (resources$ | async)?.length }}
+
+
0" style="white-space: nowrap; font-weight: bold;">Selected Skills:
+
+
+ {{ skill }}
+ close
+
+
+
-
-
-
- delete
+
[{{ resource.id | slice:0:6 }}] {{ resource.firstName }} {{ resource.lastName }}
+ close
-
-
- query-templates:
-
-
-
-
-
+
Query Templates:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/workbench/frontend/src/app/common/components/resource-query/resource-query.component.scss b/workbench/frontend/src/app/common/components/resource-query/resource-query.component.scss
index e69de29..dd8227e 100644
--- a/workbench/frontend/src/app/common/components/resource-query/resource-query.component.scss
+++ b/workbench/frontend/src/app/common/components/resource-query/resource-query.component.scss
@@ -0,0 +1,17 @@
+.selected {
+ background-color: #e0e0e0;
+ color: #333;
+ border-bottom: 3px solid #ccc;
+}
+
+.mat-button {
+ transition: background-color 0.3s;
+}
+
+.mat-button:not(.selected):hover {
+ background-color: #e0e0e0;
+}
+
+.selected:hover {
+ background-color: #e0e0e0;
+}
diff --git a/workbench/frontend/src/app/common/components/resource-query/resource-query.component.ts b/workbench/frontend/src/app/common/components/resource-query/resource-query.component.ts
index 5a6ddde..64bb9ba 100644
--- a/workbench/frontend/src/app/common/components/resource-query/resource-query.component.ts
+++ b/workbench/frontend/src/app/common/components/resource-query/resource-query.component.ts
@@ -1,54 +1,46 @@
import { HttpErrorResponse } from '@angular/common/http';
-import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
+import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { BehaviorSubject, merge, of, Subject } from 'rxjs';
-import { catchError, mergeMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
+import { catchError, map, mergeMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { QueryService } from '../../services/query.service';
+import { SharedSkillsService } from '../../services/shared-skill.service';
const TEMPLATES = {
default: `
-SELECT
- resource.id as id,
- resource.firstName as firstName,
- resource.lastName as lastName
-FROM
- UnifiedPerson resource
-WHERE
- resource.plannableResource = true
- AND resource.inactive != true
-LIMIT 25
-`,
+ SELECT resource.id as id,
+ resource.firstName as firstName,
+ resource.lastName as lastName
+ FROM UnifiedPerson resource
+ WHERE resource.plannableResource = true
+ AND resource.inactive != true
+ LIMIT 25
+ `,
skill: `
-SELECT
- resource.id as id,
- resource.firstName as firstName,
- resource.lastName as lastName
-FROM
- Tag tag
- LEFT JOIN Skill skill ON skill.tag = tag.id
- LEFT JOIN UnifiedPerson resource ON resource.id = skill.person
-WHERE
- resource.plannableResource = true
- AND resource.inactive != true
- AND tag.name = '
'
-LIMIT 25
-`,
+ SELECT resource.id as id,
+ resource.firstName as firstName,
+ resource.lastName as lastName
+ FROM Tag tag
+ LEFT JOIN Skill skill ON skill.tag = tag.id
+ LEFT JOIN UnifiedPerson resource ON resource.id = skill.person
+ WHERE resource.plannableResource = true
+ AND resource.inactive != true
+ AND tag.name = ''
+ LIMIT 25
+ `,
region: `
-SELECT
- resource.id as id,
- resource.firstName as firstName,
- resource.lastName as lastName
-FROM
- UnifiedPerson resource
- LEFT JOIN Region region ON region.name = ''
-WHERE
- region.id IN resource.regions
- AND resource.plannableResource = true
- AND resource.inactive != true
-LIMIT 25
-`
-}
+ SELECT resource.id as id,
+ resource.firstName as firstName,
+ resource.lastName as lastName
+ FROM UnifiedPerson resource
+ LEFT JOIN Region region ON region.name = ''
+ WHERE region.id IN resource.regions
+ AND resource.plannableResource = true
+ AND resource.inactive != true
+ LIMIT 25
+ `
+};
@Component({
selector: 'resource-query',
@@ -61,10 +53,21 @@ export class ResourceQueryComponent implements OnInit, OnDestroy {
public resources$ = new BehaviorSubject[]>([]);
public isLoading$ = new BehaviorSubject(false);
+ public allSkills = [];
+ public skillResourcesMap: Map> = new Map();
+ public allResources: any[] = [];
+ public selectedSkills$ = new BehaviorSubject([]);
+
+ public selectedTemplate = 'default';
+ public crowdChecked = false;
+ public internalChecked = false;
+
+
+ @Input() selectedMandatorySkills: string[];
@Output() change = new EventEmitter();
public form: FormGroup;
- private onDistroy$ = new Subject();
+ private onDestroy$ = new Subject();
public editorOptions = {
theme: 'vs-dark',
language: 'pgsql',
@@ -75,10 +78,12 @@ export class ResourceQueryComponent implements OnInit, OnDestroy {
private fb: FormBuilder,
private svc: QueryService,
private snackBar: MatSnackBar,
- ) { }
+ private sharedSkillsService: SharedSkillsService
+ ) {
+ }
- public ngOnDestroy() {
- this.onDistroy$.next();
+ public ngOnDestroy(): void {
+ this.onDestroy$.next();
}
public ngOnInit(): void {
@@ -86,20 +91,35 @@ export class ResourceQueryComponent implements OnInit, OnDestroy {
query: [TEMPLATES.default],
});
- this.resources$.pipe(
- tap(list => {
- if (list.length) {
- this.change.next(list.map(it => it.id));
- }
- }),
- takeUntil(this.onDistroy$)
- ).subscribe();
+ this.svc.queryResourceSkills(TEMPLATES.default).subscribe(resources => {
+ this.allResources = resources;
+ this.skillResourcesMap.clear();
+
+ resources.forEach(resource => {
+ resource.skills.forEach(skill => {
+ const key = skill.name.toLowerCase();
+ if (!this.skillResourcesMap.has(key)) {
+ this.skillResourcesMap.set(key, new Set());
+ }
+ this.skillResourcesMap.get(key).add(resource);
+ });
+ });
+
+ this.resources$.next(this.allResources);
+ this.allSkills = Array.from(this.skillResourcesMap.keys());
+ });
+
+ this.selectedSkills$ = this.sharedSkillsService.selectedSkills$;
+
+ this.sharedSkillsService.selectedSkills$.subscribe((selectedSkills) => {
+ this.updateResources(selectedSkills);
+ });
this.onQuery.pipe(
withLatestFrom(merge(of(this.form.value), this.form.valueChanges)),
mergeMap(([_, form]) => {
this.isLoading$.next(true);
- return this.svc.queryResource(form.query).pipe(
+ return this.svc.queryResourceSkills(form.query).pipe(
catchError(error => {
console.error(error);
@@ -107,40 +127,123 @@ export class ResourceQueryComponent implements OnInit, OnDestroy {
if (error instanceof HttpErrorResponse) {
errorMessage = `Error [❌ ${error.status} ❌ ]\n\n${error.message}`;
}
-
- const snackBarRef = this.snackBar.open(errorMessage, 'ok', { duration: 3000 });
-
+ this.snackBar.open(errorMessage, 'ok', { duration: 3000 });
return of([]);
})
- )
+ );
}),
tap(list => {
this.isLoading$.next(false);
this.resources$.next(list);
}),
- takeUntil(this.onDistroy$)
+ takeUntil(this.onDestroy$)
).subscribe();
setTimeout(() => this.onQuery.next(), 100);
+
+ this.resources$.pipe(
+ tap(list => {
+ if (list.length) {
+ this.change.next(list.map(it => it.id));
+ }
+ }),
+ takeUntil(this.onDestroy$)
+ ).subscribe();
}
- public onEditor(editor) {
+ public onEditor(editor): void {
// let line = editor.getPosition();
}
- public remove(item: { id: string }) {
+ public remove(item: Partial<{ id: string; firstName: string; lastName: string }>): void {
this.resources$.pipe(
take(1),
tap(current => this.resources$.next(current.filter(it => it.id !== item.id)))
).subscribe();
}
- public doQuery() {
+
+ public removeSkill(skill: string): void {
+ this.sharedSkillsService.selectedSkills$.pipe(
+ take(1),
+ map((selectedSkills) => selectedSkills.filter((s) => s !== skill))
+ ).subscribe((updatedSkills) => {
+ this.selectedSkills$.next(updatedSkills);
+ });
+ this.selectedSkills$ = this.sharedSkillsService.selectedSkills$;
+ }
+
+ public doQuery(): void {
this.onQuery.next();
}
- public applyTmpl(t: keyof typeof TEMPLATES) {
+ public applyTmpl(t: keyof typeof TEMPLATES): void {
+ this.selectedTemplate = t;
this.form.patchValue({ query: TEMPLATES[t] });
+ this.updateSqlCode();
+ }
+
+ private updateResources(skills): void {
+ if (skills.length > 0) {
+ const firstSkill = skills[0];
+ const firstResourceSet = this.skillResourcesMap.get(firstSkill.toLowerCase()) || new Set();
+
+ const intersection = skills.slice(1).reduce((commonResources, skill) => {
+ const resourceSet = this.skillResourcesMap.get(skill.toLowerCase()) || new Set();
+ return new Set([...commonResources].filter(resource => resourceSet.has(resource)));
+ }, firstResourceSet);
+
+ this.resources$.next(Array.from(intersection));
+ } else {
+ this.resources$.next(this.allResources);
+ }
}
+
+ public switchCrowdInternal(changed: string): void {
+ if (changed === 'CROWD' && this.crowdChecked) {
+ this.internalChecked = false;
+ }
+ else if (changed === 'INTERNAL' && this.internalChecked) {
+ this.crowdChecked = false;
+ }
+
+ this.updateSqlCode();
+ }
+
+ public updateSqlCode(): void {
+ const updateCondition = (conditionToAdd: string, include: boolean): void => {
+ const query = this.form.value.query;
+
+ if (query.trim() !== '') {
+ if (include) {
+ const updatedQuery = this.insertTextAfterWhere(query, conditionToAdd);
+ this.form.patchValue({ query: updatedQuery });
+ } else {
+ const modifiedQuery = this.removeCondition(query, conditionToAdd);
+ this.form.patchValue({ query: modifiedQuery });
+ }
+ }
+ };
+
+ updateCondition('resource.crowdType LIKE \'CROWD\'\n\tAND ', this.crowdChecked);
+ updateCondition('resource.crowdType LIKE \'NON_CROWD\'\n\tAND ', this.internalChecked);
+ }
+
+ private insertTextAfterWhere(input: string, newText: string): string {
+ const whereRegex = /\bWHERE\b/i;
+ const whereMatch = whereRegex.exec(input);
+
+ if (whereMatch) {
+ const insertionIndex = whereMatch.index + whereMatch[0].length;
+
+ return `${input.slice(0, insertionIndex)} ${newText}${input.slice(insertionIndex)}`;
+ } else {
+ return `${input}\n${newText}`;
+ }
+ }
+
+ private removeCondition(query: string, conditionToRemove: string): string {
+ return query.replace(conditionToRemove, '');
+ }
}
diff --git a/workbench/frontend/src/app/common/services/query.service.ts b/workbench/frontend/src/app/common/services/query.service.ts
index 506d38d..fbb1d29 100644
--- a/workbench/frontend/src/app/common/services/query.service.ts
+++ b/workbench/frontend/src/app/common/services/query.service.ts
@@ -111,48 +111,48 @@ export class QueryService {
}
- public queryResource, item extends {
+ public queryResourceSkills, item extends {
id: string,
firstName: string,
- lastName: string
+ lastName: string,
+ skills: { name: string, start: string, end: string }[]
}>(query: string): Observable {
return combineLatest([
this._query(query),
this._query, TagObj>(`SELECT tag.id as id, tag.name as name
- FROM Tag tag
- WHERE tag.inactive = false`),
+ FROM Tag tag
+ WHERE tag.inactive = false`),
this._query, SkillObj>(`SELECT skill.id as id,
- skill.tag as tag,
- skill.person as person,
- skill.startDate as startDate,
- skill.endDate as endDate
- FROM Skill skill
- WHERE skill.inactive = false`)
- ])
- .pipe(
- map(([resources, tags, skills]) => {
- const tagMap = tags.data.reduce((m, { id, name }) => m.set(id, name), new Map());
- const skillMap = skills.data.reduce((m, skill) => {
- const currentItem = {
- name: (tagMap.get(skill.tag) || skill.tag),
- start: skill.startDate === 'null' ? null : skill.startDate,
- end: skill.endDate === 'null' ? null : skill.endDate
- };
- if (m.has(skill.person)) {
- m.get(skill.person)?.push(currentItem);
- } else {
- m.set(skill.person, [currentItem]);
- }
- return m;
- }, new Map());
-
- return resources.data.map(it => ({
- ...it,
- skills: skillMap.has(it.id) ? skillMap.get(it.id).sort((a, b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)) : []
- }));
- }),
- tap((currentList) => this.addToCache('resource', currentList))
- );
+ skill.tag as tag,
+ skill.person as person,
+ skill.startDate as startDate,
+ skill.endDate as endDate
+ FROM Skill skill
+ WHERE skill.inactive = false`)
+ ]).pipe(
+ map(([resources, tags, skills]) => {
+ const tagMap = tags.data.reduce((m, { id, name }) => m.set(id, name), new Map());
+ const skillMap = skills.data.reduce((m, skill) => {
+ const currentItem = {
+ name: (tagMap.get(skill.tag) || skill.tag),
+ start: skill.startDate === 'null' ? null : skill.startDate,
+ end: skill.endDate === 'null' ? null : skill.endDate
+ };
+ if (m.has(skill.person)) {
+ m.get(skill.person)?.push(currentItem);
+ } else {
+ m.set(skill.person, [currentItem]);
+ }
+ return m;
+ }, new Map());
+
+ return resources.data.map(it => ({
+ ...it,
+ skills: skillMap.has(it.id) ? skillMap.get(it.id).sort((a, b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0)) : []
+ }));
+ }),
+ tap((currentList) => this.addToCache('resource', currentList))
+ );
}
public getResourceFromCache(id: string): PersonObj {
diff --git a/workbench/frontend/src/app/common/services/shared-skill.service.ts b/workbench/frontend/src/app/common/services/shared-skill.service.ts
new file mode 100644
index 0000000..ddcf4ff
--- /dev/null
+++ b/workbench/frontend/src/app/common/services/shared-skill.service.ts
@@ -0,0 +1,14 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class SharedSkillsService {
+ private selectedSkillsSubject = new BehaviorSubject([]);
+ selectedSkills$ = this.selectedSkillsSubject;
+
+ updateSelectedSkills(selectedSkills: string[]): void {
+ this.selectedSkillsSubject.next(selectedSkills);
+ }
+}
diff --git a/workbench/frontend/src/app/slot-booking/components/job-builder/job-builder.component.ts b/workbench/frontend/src/app/slot-booking/components/job-builder/job-builder.component.ts
index 3f0c80f..21f7ae6 100644
--- a/workbench/frontend/src/app/slot-booking/components/job-builder/job-builder.component.ts
+++ b/workbench/frontend/src/app/slot-booking/components/job-builder/job-builder.component.ts
@@ -7,6 +7,7 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { filter, map, take, takeUntil, tap } from 'rxjs/operators';
+import { SharedSkillsService } from '../../../common/services/shared-skill.service';
@Component({
selector: 'job-builder',
@@ -32,17 +33,19 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
@ViewChild('mandatorySkillsInput') mandatorySkillsInput: ElementRef;
@ViewChild('optionalSkillsInput') optionalSkillsInput: ElementRef;
@Output() change = new EventEmitter();
+
form: FormGroup;
selectedAddress: FormControl;
- onDistroy$ = new Subject();
+ onDestroy$ = new Subject();
constructor(
private fb: FormBuilder,
private service: JobService,
+ private sharedSkillsService: SharedSkillsService
) { }
- public ngAfterContentInit() {
+ public ngAfterContentInit(): void {
}
@@ -55,10 +58,10 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
this.form.patchValue({
location_latitude: item.location.latitude,
location_longitude: item.location.longitude
- })
+ });
this.selectedAddress.patchValue(null);
}),
- takeUntil(this.onDistroy$)
+ takeUntil(this.onDestroy$)
).subscribe();
this.allAddress$ = this.service.fetchAllAddress().pipe(
@@ -71,7 +74,7 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
)
);
- const alltags = this.service.fetchAllTags()
+ const alltags = this.service.fetchAllTags();
alltags.pipe(
@@ -83,15 +86,15 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
// select mandatory skill
// this.selectedMandatorySkills$.next([first.name]);
}
- }, 100)
+ }, 100);
})
- ).subscribe()
+ ).subscribe();
this.matchingResources$ = combineLatest([this.selectedMandatorySkills$, alltags]).pipe(
map(([selected, all]) => selected.map(tagName => all.find(x => x.name === tagName))
.filter(it => !!it)
.reduce((theSet, it) => {
- it.persons.forEach(p => theSet.add(p))
+ it.persons.forEach(p => theSet.add(p));
return theSet;
}, new Set())),
map((theSet) => Array.from(theSet)),
@@ -115,12 +118,16 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
this.selectedMandatorySkills$.pipe(
tap((list) => this.form.patchValue({ mandatorySkills: list })),
- takeUntil(this.onDistroy$)
+ takeUntil(this.onDestroy$)
).subscribe();
+ this.sharedSkillsService.selectedSkills$.subscribe(() =>
+ this.selectedMandatorySkills$ = this.sharedSkillsService.selectedSkills$
+ );
+
this.selectedOptionalSkills$.pipe(
tap((list) => this.form.patchValue({ optionalSkills: list })),
- takeUntil(this.onDistroy$)
+ takeUntil(this.onDestroy$)
).subscribe();
const form$ = merge(of(this.form.value), this.form.valueChanges);
@@ -141,24 +148,25 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
};
this.change.next(job);
}),
- takeUntil(this.onDistroy$)
+ takeUntil(this.onDestroy$)
).subscribe();
}
- public ngOnDestroy() {
- this.onDistroy$.next();
+ public ngOnDestroy(): void {
+ this.onDestroy$.next();
}
- public mandatorySkillsSelectionChanged(event: MatAutocompleteSelectedEvent) {
+ public mandatorySkillsSelectionChanged(event: MatAutocompleteSelectedEvent): void {
this.selectedMandatorySkills$.next([...this.selectedMandatorySkills$.value, event.option.value]);
this.mandatorySkillsInput.nativeElement.value = '';
this.mandatorySkillsCtrl.setValue(null);
+ this.sharedSkillsService.updateSelectedSkills(this.selectedMandatorySkills$.value);
}
- public addMandatorySkill(event: MatChipInputEvent) {
+ public addMandatorySkill(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
- if (!value) return;
+ if (!value) { return; }
this.selectedMandatorySkills$.next([...this.selectedMandatorySkills$.value, value.trim()]);
if (input) {
@@ -166,20 +174,21 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
}
}
- public removeMandatorySkill(tagName: string) {
+ public removeMandatorySkill(tagName: string): void {
this.selectedMandatorySkills$.next(this.selectedMandatorySkills$.value.filter(it => (it !== tagName)));
+ this.sharedSkillsService.updateSelectedSkills(this.selectedMandatorySkills$.value);
}
- public optionalSkillsSelectionChanged(event: MatAutocompleteSelectedEvent) {
+ public optionalSkillsSelectionChanged(event: MatAutocompleteSelectedEvent): void {
this.selectedOptionalSkills$.next([...this.selectedOptionalSkills$.value, event.option.value]);
this.optionalSkillsInput.nativeElement.value = '';
this.optionalSkillsCtrl.setValue(null);
}
- public addOptionalSkill(event: MatChipInputEvent) {
+ public addOptionalSkill(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
- if (!value) return;
+ if (!value) { return; }
this.selectedOptionalSkills$.next([...this.selectedOptionalSkills$.value, value.trim()]);
if (input) {
@@ -187,23 +196,23 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit
}
}
- public removeOptionalSkill(tagName: string) {
+ public removeOptionalSkill(tagName: string): void {
this.selectedOptionalSkills$.next(this.selectedOptionalSkills$.value.filter(it => (it !== tagName)));
}
- public locationClear() {
- this.selectedAddress.patchValue(null)
+ public locationClear(): void {
+ this.selectedAddress.patchValue(null);
this.form.patchValue({
location_latitude: null,
location_longitude: null
});
}
- public pickFromMap() {
+ public pickFromMap(): void {
this.isPicking$.next(true);
}
- public mapSelect({ latitude, longitude }: { latitude: number; longitude: number; }) {
+ public mapSelect({ latitude, longitude }: { latitude: number; longitude: number; }): void {
if (this.isPicking$.value) {
this.form.patchValue({
location_latitude: latitude,
diff --git a/workbench/frontend/src/app/slot-booking/services/slot-booking.service.ts b/workbench/frontend/src/app/slot-booking/services/slot-booking.service.ts
index a27b4f6..67fda11 100644
--- a/workbench/frontend/src/app/slot-booking/services/slot-booking.service.ts
+++ b/workbench/frontend/src/app/slot-booking/services/slot-booking.service.ts
@@ -59,9 +59,7 @@ export type SearchRequest = Readonly<{
}
}>;
slots: ISearchRequestSlot[];
- resources: {
- personIds: string[]
- },
+ resources: {personIds: string[]} | ResourceFilters,
options: Readonly<{
maxResultsPerSlot: number;
}>;
@@ -72,6 +70,13 @@ type ILocation = {
longitude: number;
};
+export type ResourceFilters = {
+ filters: {
+ includeInternalPersons: boolean,
+ includeCrowdPersons: boolean,
+ includeMandatorySkills: boolean
+ }
+};
@Injectable({
providedIn: 'root'
diff --git a/workbench/frontend/src/app/slot-booking/slot-booking.component.html b/workbench/frontend/src/app/slot-booking/slot-booking.component.html
index f3543e8..bc334ab 100644
--- a/workbench/frontend/src/app/slot-booking/slot-booking.component.html
+++ b/workbench/frontend/src/app/slot-booking/slot-booking.component.html
@@ -1,4 +1,4 @@
-
+
Slot Search Request / Response
@@ -12,18 +12,18 @@
{{ (response$ | async).errorMessage }}
+ class="error-box">{{ (response$ | async).errorMessage }}
+ [expanded]="i === 0 && false">
+ [job]="jobBuilder$ | async">
@@ -35,7 +35,7 @@
+ [value]="(100 / (group.maxScore * 1000) ) * (item.score * 1000)">
@@ -52,7 +52,7 @@
directions_car
- {{ item.trip.durationInMinutes }} min ({{ (item.trip.distanceInMeters / 1000 ) }} km)
+ {{ item.trip.durationInMinutes }} min ({{ (item.trip.distanceInMeters / 1000) }} km)
@@ -65,7 +65,7 @@
+ [title]="skill.start + ' '+ skill.end">
{{ skill.name }}
@@ -80,8 +80,43 @@
Request
-
- Full JSON Request
+
+
+ Filter Based
+
+
+
+ Internal
+ Crowd
+ Use mandatory skills
+
+
+
+
+ using
+ /api/v3/job-slots/actions/search
+ Search Job Slot API see
+
+
+ documentation
+
+
+
+ auto refresh
+
+
+
+ Timing: {{ (response$ | async).time | number }} ms for {{ (response$ | async).grouped.length }} slots with
+ {{ (response$ | async).results.length }} matches
+
+
+
+ Full JSON Request
{{ requestPayload$ | async | json }}
@@ -91,7 +126,7 @@ Request
Response
- Full JSON Response
+ Full JSON Response
{{ (response$ | async)?.results | json }}
@@ -104,30 +139,6 @@ Response
-
-
-
-
- using /api/v3/job-slots/actions/search Search Job Slot API see
-
-
- documentation
-
-
-
- auto refresh
-
-
-
- Timing: {{ (response$ | async).time | number }} ms for {{ (response$ | async).grouped.length }} solts with
- {{ (response$ | async).results.length }} matches
-
-
-
Slot Search Options:
@@ -135,7 +146,7 @@
Slot Search Options:
🕑 When / Timing
- {{ (requestPayload$ | async)?.slots.length }} slots
+ {{ (requestPayload$ | async)?.slots.length }} slots
@@ -160,7 +171,7 @@
Slot Search Options:
👷 Who / People
- {{ (requestPayload$ | async)?.resources.personIds.length }} technicians
+ {{ (requestPayload$ | async)?.resources?.personIds?.length }} technicians
diff --git a/workbench/frontend/src/app/slot-booking/slot-booking.component.scss b/workbench/frontend/src/app/slot-booking/slot-booking.component.scss
index a50a154..82c08b4 100644
--- a/workbench/frontend/src/app/slot-booking/slot-booking.component.scss
+++ b/workbench/frontend/src/app/slot-booking/slot-booking.component.scss
@@ -3,24 +3,24 @@
}
.error-box {
- width: 100%;
- border-radius: 10px;
- background-color: #ab4545;
- color:#fff;
+ width: 100%;
+ border-radius: 10px;
+ background-color: #ab4545;
+ color:#fff;
padding: 10px;
}
.panel-title {
- padding-top:5px;
+ padding-top:5px;
margin-right: 10px;
}
.matching-tech {
display: inline-grid;
- text-align: center;
- width: 300px;
+ text-align: center;
+ width: 300px;
border: 2px solid #c1c1c1;
- border-radius: 10px;
+ border-radius: 10px;
padding: 10px;
margin-left: 10px;
margin-bottom: 10px;
@@ -30,7 +30,7 @@
.tech-info {
margin-bottom: 12px;
display: flex;
-
+
mat-icon {
margin-right: 10px;
}
diff --git a/workbench/frontend/src/app/slot-booking/slot-booking.component.ts b/workbench/frontend/src/app/slot-booking/slot-booking.component.ts
index 61defe2..deafdcd 100644
--- a/workbench/frontend/src/app/slot-booking/slot-booking.component.ts
+++ b/workbench/frontend/src/app/slot-booking/slot-booking.component.ts
@@ -1,18 +1,19 @@
import { HttpErrorResponse } from '@angular/common/http';
-import { Component, OnDestroy, OnInit, } from '@angular/core';
-import { FormBuilder, FormGroup, } from '@angular/forms';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
import { catchError, filter, map, mergeMap, pairwise, take, takeUntil, tap } from 'rxjs/operators';
import { AuthService } from '../common/services/auth.service';
import { Slot } from './components/slot-builder/slot-builder.component';
-import { SlotSearchService, SearchRequest, SearchResponseWrapper } from './services/slot-booking.service';
+import { ResourceFilters, SearchRequest, SearchResponseWrapper, SlotSearchService } from './services/slot-booking.service';
import { Job } from './services/job.service';
+import { Event } from '@angular/router';
@Component({
selector: 'slot-booking',
templateUrl: './slot-booking.component.html',
- styleUrls: ['./slot-booking.component.scss']
+ styleUrls: ['./slot-booking.component.scss'],
})
export class SlotBookingComponent implements OnInit, OnDestroy {
@@ -27,6 +28,16 @@ export class SlotBookingComponent implements OnInit, OnDestroy {
public requestOptions: FormGroup;
private onDistroy$ = new Subject();
+ public filterBased = false;
+ public internalChecked = true;
+ public crowdChecked = true;
+ public skillsChecked = false;
+ public filters$ = new BehaviorSubject(null);
+ public requestPayloadResources = null;
+
+ animationState = 'end';
+
+
constructor(
private fb: FormBuilder,
private service: SlotSearchService,
@@ -38,7 +49,7 @@ export class SlotBookingComponent implements OnInit, OnDestroy {
const autoRefresh$ = new Observable((op) => {
const timer = setInterval(() => {
- const { refresh } = this.requestOptions.value
+ const { refresh } = this.requestOptions.value;
if (refresh && !this.isLoading$.value && !!this.response$.value && !this.response$.value.isError) {
this.doRequest();
op.next();
@@ -47,7 +58,7 @@ export class SlotBookingComponent implements OnInit, OnDestroy {
return () => {
clearInterval(timer);
- }
+ };
});
autoRefresh$.pipe(
@@ -56,72 +67,111 @@ export class SlotBookingComponent implements OnInit, OnDestroy {
this.isLoggedIn$ = this.auth.isLoggedIn$;
- this.requestPayload$ = combineLatest([this.pluginEditor$, this.slotBuilder$, this.jobBuilder$, this.personIds$])
- .pipe(
- filter(([pluginEditor, slots, job, personIds]) => !!(pluginEditor && slots && job && personIds.length)),
- map(([pluginEditor, slots, job, personIds]): SearchRequest => {
- return {
-
- job: {
- durationMinutes: job.durationMinutes,
- location: job.location,
- mandatorySkills: job.mandatorySkills,
- optionalSkills: job.optionalSkills,
- udfValues: {}
- },
-
- slots,
-
- resources: {
- personIds
- },
-
- options: {
- maxResultsPerSlot: 8
- },
-
- policy: pluginEditor,
-
- }
- })
- );
+ this.updateRequestPayload();
this.requestOptions = this.fb.group({
- refresh: [false]
+ refresh: [false],
+ filterBasedToggle: [false]
});
+
this.response$.pipe(
filter(it => !!it),
map(it => it.time),
pairwise(),
+ // tslint:disable-next-line:no-console
tap(r => console.debug(r)),
takeUntil(this.onDistroy$)
).subscribe();
}
- public ngOnDestroy() {
+ public ngOnDestroy(): void {
this.onDistroy$.next();
}
- public onChangeSlots(windows: Slot[]) {
+ public onChangeSlots(windows: Slot[]): void {
this.slotBuilder$.next(windows);
}
- public onChangePluginEditor(name: string) {
+ public onChangePluginEditor(name: string): void {
this.pluginEditor$.next(name);
}
- public onChangeJobBuilder(job: Job) {
+ public onChangeJobBuilder(job: Job): void {
this.jobBuilder$.next(job);
}
- public onChangePersonIds(ids: string[]) {
+ public onChangePersonIds(ids: string[]): void {
this.personIds$.next(ids);
}
- public doRequest() {
+ public onCheckboxChange(): void {
+ this.filters$.next(this.buildFilters());
+ this.updateRequestPayload();
+ }
+
+ public onFilterBasedToggleChange(event: Event): void {
+ this.filterBased = !this.filterBased;
+ this.filters$.next(this.buildFilters());
+ const prevRequest = this.requestPayload$?.pipe();
+
+ this.requestPayload$.pipe(
+ tap(_ => this.animationState = 'start'),
+ ).subscribe(requestPayload => {
+ this.updateRequestPayload();
+ setTimeout(() => {
+ this.animationState = 'end';
+ }, 1000);
+ });
+ }
+
+
+ private buildFilters(): ResourceFilters {
+ return {filters: {
+ includeInternalPersons: this.internalChecked,
+ includeCrowdPersons: this.crowdChecked,
+ includeMandatorySkills: this.skillsChecked
+ }};
+ }
+
+
+ private updateRequestPayload(): void {
+ this.requestPayload$ = combineLatest([
+ this.pluginEditor$,
+ this.slotBuilder$,
+ this.jobBuilder$,
+ this.personIds$,
+ this.filters$
+ ]).pipe(
+ filter(([pluginEditor, slots, job, personIds, filters]) => !!(
+ pluginEditor && slots && job && (personIds.length || filters)
+ )),
+ map(([pluginEditor, slots, job, personIds, filters]): SearchRequest => {
+
+ const newResources = this.filterBased ? filters : { personIds };
+
+ return {
+ job: {
+ durationMinutes: job.durationMinutes,
+ location: job.location,
+ mandatorySkills: job.mandatorySkills,
+ optionalSkills: job.optionalSkills,
+ udfValues: {}
+ },
+ slots,
+ resources: newResources,
+ options: {
+ maxResultsPerSlot: 8
+ },
+ policy: pluginEditor,
+ };
+ })
+ );
+ }
+
+ public doRequest(): void {
this.isLoading$.next(true);
@@ -166,13 +216,13 @@ export class SlotBookingComponent implements OnInit, OnDestroy {
time: -1,
grouped: [],
results: []
- })
+ });
})
- )
+ );
}),
tap(value => {
- this.isLoading$.next(false)
+ this.isLoading$.next(false);
if (value) {
this.response$.next(value);
}
@@ -180,7 +230,7 @@ export class SlotBookingComponent implements OnInit, OnDestroy {
).subscribe();
}
- public bookingLoading(isLoading: boolean) {
+ public bookingLoading(isLoading: boolean): void {
this.isLoading$.next(isLoading);
}