From 98fcdb85bb5799abc75f66df9d72115afb9d9dc5 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 15 Nov 2023 10:16:41 +0100 Subject: [PATCH 01/17] UI changes to display filtering by internal and crowd as well as by skill --- workbench/frontend/src/app/app.module.ts | 7 ++- .../resource-query.component.html | 61 +++++++++++++------ 2 files changed, 49 insertions(+), 19 deletions(-) mode change 100644 => 100755 workbench/frontend/src/app/common/components/resource-query/resource-query.component.html diff --git a/workbench/frontend/src/app/app.module.ts b/workbench/frontend/src/app/app.module.ts index a6bf683..737918a 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'; @@ -38,6 +38,7 @@ import { ResourceQueryComponent } from './common/components/resource-query/resou import { SaveDialog } from './common/components/plugin-editor/save-dialog/save-dialog.component'; import { SelectSheet } from './common/components/select-sheet/select-sheet.component'; import { PluginEditorComponent } from './common/components/plugin-editor/plugin-editor.component'; +import { MatRadioModule } from '@angular/material/radio'; @NgModule({ @@ -65,7 +66,9 @@ import { PluginEditorComponent } from './common/components/plugin-editor/plugin- ReactiveFormsModule, MonacoEditorModule.forRoot(ngxMonacoEditorConfig), FlexLayoutModule, - ...MatModules + ...MatModules, + MatRadioModule, + 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..8d76b0b --- 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,7 +1,6 @@

resources: {{ (resources$ | async)?.length }}

-
@@ -12,21 +11,49 @@

resources: {{ (resources$ | async)?.length }}

- - - query-templates: - - - - - -
- + +
+
+ Crowd + Internal +
+
+ + Skills + + + + {{ form.get('selectedSkills').value ? form.get('selectedSkills').value.length + ' selected' : 'Select Skills' }} + + + + {{ skill }} + + + +
+
+ +
+ + + SQL Query + + query-templates: + + + + + +
+ +
+
+ + +
+
+
-
- - -
-
\ No newline at end of file From 987c4fd1907c4ffa836840287ac10c04f2b4088c Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 15 Nov 2023 10:19:50 +0100 Subject: [PATCH 02/17] query service now adds skills in return -> filtering --- .../src/app/common/services/query.service.ts | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/workbench/frontend/src/app/common/services/query.service.ts b/workbench/frontend/src/app/common/services/query.service.ts index 506d38d..6ac0fe5 100644 --- a/workbench/frontend/src/app/common/services/query.service.ts +++ b/workbench/frontend/src/app/common/services/query.service.ts @@ -114,45 +114,45 @@ export class QueryService { public queryResource, 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 { From b745788be2247468e80233f04d623d745b4209da Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 15 Nov 2023 10:24:27 +0100 Subject: [PATCH 03/17] listening to selection and filtering resources based on multi-selection in skills dropdown --- .../resource-query.component.ts | 110 ++++++++++++------ 1 file changed, 74 insertions(+), 36 deletions(-) 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..5821b7c 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 @@ -8,29 +8,29 @@ import { QueryService } from '../../services/query.service'; const TEMPLATES = { default: ` -SELECT +SELECT resource.id as id, resource.firstName as firstName, resource.lastName as lastName -FROM - UnifiedPerson resource -WHERE - resource.plannableResource = true +FROM + UnifiedPerson resource +WHERE + resource.plannableResource = true AND resource.inactive != true LIMIT 25 `, skill: ` -SELECT +SELECT resource.id as id, resource.firstName as firstName, resource.lastName as lastName -FROM - Tag tag +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 + resource.plannableResource = true + AND resource.inactive != true AND tag.name = '' LIMIT 25 `, @@ -39,16 +39,16 @@ SELECT resource.id as id, resource.firstName as firstName, resource.lastName as lastName -FROM - UnifiedPerson resource +FROM + UnifiedPerson resource LEFT JOIN Region region ON region.name = '' -WHERE +WHERE region.id IN resource.regions - AND resource.plannableResource = true - AND resource.inactive != true + AND resource.plannableResource = true + AND resource.inactive != true LIMIT 25 ` -} +}; @Component({ selector: 'resource-query', @@ -61,10 +61,16 @@ 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: string[] = []; + + @Output() change = new EventEmitter(); public form: FormGroup; - private onDistroy$ = new Subject(); + private onDestroy$ = new Subject(); public editorOptions = { theme: 'vs-dark', language: 'pgsql', @@ -77,23 +83,40 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { private snackBar: MatSnackBar, ) { } - public ngOnDestroy() { - this.onDistroy$.next(); + public ngOnDestroy(): void { + this.onDestroy$.next(); } public ngOnInit(): void { this.form = this.fb.group({ query: [TEMPLATES.default], + selectedSkills: [] }); - this.resources$.pipe( - tap(list => { - if (list.length) { - this.change.next(list.map(it => it.id)); - } - }), - takeUntil(this.onDistroy$) - ).subscribe(); + this.svc.queryResource(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, []); + } + this.skillResourcesMap.get(key).push(resource); + }); + }); + + this.resources$.next(this.allResources); + this.allSkills = Array.from(this.skillResourcesMap.keys()); + }); + + this.form.get('selectedSkills').valueChanges.pipe( + takeUntil(this.onDestroy$) + ).subscribe(selectedSkills => { + this.selectedSkills = selectedSkills; + this.updateResources(); + }); this.onQuery.pipe( withLatestFrom(merge(of(this.form.value), this.form.valueChanges)), @@ -107,40 +130,55 @@ 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(); + // Trigger the initial query after a delay setTimeout(() => this.onQuery.next(), 100); } - 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 doQuery(): void { this.onQuery.next(); } - public applyTmpl(t: keyof typeof TEMPLATES) { + public applyTmpl(t: keyof typeof TEMPLATES): void { this.form.patchValue({ query: TEMPLATES[t] }); } + private updateResources(): void { + if (this.selectedSkills.length > 0) { + const filteredResourcesSet = new Set(); + + this.selectedSkills.forEach(skill => { + const resourcesForSkill = this.skillResourcesMap.get(skill.toLowerCase()) || []; + resourcesForSkill.forEach(resource => filteredResourcesSet.add(resource)); + }); + + this.resources$.next(Array.from(filteredResourcesSet)); + console.log(this.resources$); + } else { + this.resources$.next(this.allResources); + } + } + } From 2dd104b46d9157a899ff7bc9b075d17d4bf8c638 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Fri, 17 Nov 2023 13:15:53 +0100 Subject: [PATCH 04/17] ts lint trigger list at removal --- .../job-builder/job-builder.component.ts | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) 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..e327081 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,12 @@ 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.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 +144,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 +170,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 +192,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, From 5bb32bd422c9dba86eacd5fde5634de333081dde Mon Sep 17 00:00:00 2001 From: Davo00 Date: Fri, 17 Nov 2023 13:16:21 +0100 Subject: [PATCH 05/17] swapped expansion panel, filter is now collapsed --- .../resource-query.component.html | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) 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 index 8d76b0b..801cf13 100755 --- 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 @@ -12,48 +12,52 @@

resources: {{ (resources$ | async)?.length }}

-
-
- Crowd - Internal -
-
- - Skills - - - - {{ form.get('selectedSkills').value ? form.get('selectedSkills').value.length + ' selected' : 'Select Skills' }} - - - - {{ skill }} - - - -
+ + query-templates: + + + + + +
+
+
+ + +
-
+
- - SQL Query - - query-templates: - - - + + Filter -
- +
+
+ Crowd + Internal
- - + + Skills + + + + {{ form.get('selectedSkills').value ? form.get('selectedSkills').value.length + ' selected' : 'Select Skills' }} + + + + {{ skill }} + + +
- +
+ + + -
+
From e8deb39cedd920d9d587641d3dce092f2f45e2ce Mon Sep 17 00:00:00 2001 From: Davo00 Date: Fri, 17 Nov 2023 13:16:44 +0100 Subject: [PATCH 06/17] shared skill service to bind mandatory skills selection --- .../app/common/services/shared-skill.service.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 workbench/frontend/src/app/common/services/shared-skill.service.ts 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..a0edd3b --- /dev/null +++ b/workbench/frontend/src/app/common/services/shared-skill.service.ts @@ -0,0 +1,15 @@ +// shared-skills.service.ts +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class SharedSkillsService { + private selectedSkillsSubject = new BehaviorSubject([]); + selectedSkills$ = this.selectedSkillsSubject.asObservable(); + + updateSelectedSkills(selectedSkills: string[]): void { + this.selectedSkillsSubject.next(selectedSkills); + } +} From 5243b19246c638a684bd1c5f1277b13796def340 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Fri, 17 Nov 2023 13:17:34 +0100 Subject: [PATCH 07/17] resources listen to mandatory skills selection; AND logic for filtering --- .../resource-query.component.ts | 102 +++++++++--------- 1 file changed, 50 insertions(+), 52 deletions(-) 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 5821b7c..66f4f22 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,53 +1,45 @@ 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 { 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 + 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 -` + LIMIT 25 + ` }; @Component({ @@ -62,11 +54,11 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { public isLoading$ = new BehaviorSubject(false); public allSkills = []; - public skillResourcesMap: Map = new Map(); + public skillResourcesMap: Map> = new Map(); public allResources: any[] = []; public selectedSkills: string[] = []; - + @Input() selectedMandatorySkills: string[]; @Output() change = new EventEmitter(); public form: FormGroup; @@ -81,7 +73,9 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { private fb: FormBuilder, private svc: QueryService, private snackBar: MatSnackBar, - ) { } + private sharedSkillsService: SharedSkillsService + ) { + } public ngOnDestroy(): void { this.onDestroy$.next(); @@ -101,9 +95,9 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { resource.skills.forEach(skill => { const key = skill.name.toLowerCase(); if (!this.skillResourcesMap.has(key)) { - this.skillResourcesMap.set(key, []); + this.skillResourcesMap.set(key, new Set()); } - this.skillResourcesMap.get(key).push(resource); + this.skillResourcesMap.get(key).add(resource); }); }); @@ -115,7 +109,11 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { takeUntil(this.onDestroy$) ).subscribe(selectedSkills => { this.selectedSkills = selectedSkills; - this.updateResources(); + this.updateResources(this.selectedSkills); + }); + + this.sharedSkillsService.selectedSkills$.subscribe((selectedSkills) => { + this.updateResources(selectedSkills); }); this.onQuery.pipe( @@ -165,17 +163,17 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { this.form.patchValue({ query: TEMPLATES[t] }); } - private updateResources(): void { - if (this.selectedSkills.length > 0) { - const filteredResourcesSet = new Set(); + private updateResources(skills): void { + if (skills.length > 0) { + const firstSkill = skills[0]; + const firstResourceSet = this.skillResourcesMap.get(firstSkill.toLowerCase()) || new Set(); - this.selectedSkills.forEach(skill => { - const resourcesForSkill = this.skillResourcesMap.get(skill.toLowerCase()) || []; - resourcesForSkill.forEach(resource => filteredResourcesSet.add(resource)); - }); + 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(filteredResourcesSet)); - console.log(this.resources$); + this.resources$.next(Array.from(intersection)); } else { this.resources$.next(this.allResources); } From f0e7097163785123a4388872b92f13b2d908c021 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Tue, 21 Nov 2023 10:23:30 +0100 Subject: [PATCH 08/17] UI changes --- .../resource-query.component.html | 32 +++++++------ .../resource-query.component.scss | 17 +++++++ .../resource-query.component.ts | 8 +++- .../src/app/common/services/query.service.ts | 46 ++++++++++++++++++- 4 files changed, 86 insertions(+), 17 deletions(-) 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 index 801cf13..9773e13 100755 --- 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,8 +1,6 @@ -

resources: {{ (resources$ | async)?.length }}

- -
+

Resources: {{ (resources$ | async)?.length }}

delete @@ -11,12 +9,19 @@

resources: {{ (resources$ | async)?.length }}

+

Query Templates:

- - query-templates: - - - +
+ + + +
@@ -30,16 +35,14 @@

resources: {{ (resources$ | async)?.length }}


- - - Filter +
Crowd Internal
-
+
- + +
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..ad9037f 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; /* Greyish color for selected tab */ + color: #333; /* Adjust text color for better contrast */ + border-bottom: 3px solid #ccc; /* Border color for selected tab */ +} + +.mat-button { + transition: background-color 0.3s; /* Add a smooth transition for background color */ +} + +.mat-button:not(.selected):hover { + background-color: #e0e0e0; /* Change background color on hover for non-selected buttons */ +} + +.selected:hover { + background-color: #e0e0e0; /* Change background color on hover for non-selected buttons */ +} 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 66f4f22..0c1e108 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 @@ -58,6 +58,9 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { public allResources: any[] = []; public selectedSkills: string[] = []; + public selectedTemplate = 'default'; + + @Input() selectedMandatorySkills: string[]; @Output() change = new EventEmitter(); @@ -87,7 +90,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { selectedSkills: [] }); - this.svc.queryResource(TEMPLATES.default).subscribe(resources => { + this.svc.queryResourceSkills(TEMPLATES.default).subscribe(resources => { this.allResources = resources; this.skillResourcesMap.clear(); @@ -120,7 +123,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { 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); @@ -160,6 +163,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { } public applyTmpl(t: keyof typeof TEMPLATES): void { + this.selectedTemplate = t; this.form.patchValue({ query: TEMPLATES[t] }); } diff --git a/workbench/frontend/src/app/common/services/query.service.ts b/workbench/frontend/src/app/common/services/query.service.ts index 6ac0fe5..401d7a0 100644 --- a/workbench/frontend/src/app/common/services/query.service.ts +++ b/workbench/frontend/src/app/common/services/query.service.ts @@ -110,8 +110,52 @@ export class QueryService { ); } + public queryResourceSkills, item extends { + id: string, + firstName: string, + lastName: 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`), + 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)) + ); + } + - public queryResource, item extends { + public queryResourceSkills_new, item extends { id: string, firstName: string, lastName: string, From b15ed3f14092d5a1e9d6d71a6d22efe87d088af4 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Tue, 21 Nov 2023 16:05:21 +0100 Subject: [PATCH 09/17] added change trigger to resource-query.component.ts --- .../resource-query/resource-query.component.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 0c1e108..abf8886 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 @@ -143,8 +143,16 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { takeUntil(this.onDestroy$) ).subscribe(); - // Trigger the initial query after a delay 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): void { From 2d0db3184aef592c611980128da4eacb054d5a33 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Tue, 21 Nov 2023 20:05:55 +0100 Subject: [PATCH 10/17] ui adjustment --- .../resource-query.component.html | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) 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 index 9773e13..6638315 100755 --- 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 @@ -23,6 +23,12 @@

Query Templates:

+
+
+ Crowd + Internal +
+
@@ -33,15 +39,7 @@

Query Templates:

-
- -
- -
-
- Crowd - Internal -
+
-
+
From d9a0edcc3ecb0a0a481562b245ef2b73bb3e11ac Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 22 Nov 2023 09:44:22 +0100 Subject: [PATCH 11/17] filtering in SQL --- .../resource-query.component.html | 4 +- .../resource-query.component.ts | 42 +++++++++++++++++ .../src/app/common/services/query.service.ts | 46 +------------------ 3 files changed, 45 insertions(+), 47 deletions(-) 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 index 6638315..0bc0d0f 100755 --- 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 @@ -25,8 +25,8 @@

Query Templates:

- Crowd - Internal + Crowd + Internal
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 abf8886..4d1476b 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 @@ -59,6 +59,8 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { public selectedSkills: string[] = []; public selectedTemplate = 'default'; + public crowdChecked = false; + public internalChecked = false; @Input() selectedMandatorySkills: string[]; @@ -173,6 +175,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { public applyTmpl(t: keyof typeof TEMPLATES): void { this.selectedTemplate = t; this.form.patchValue({ query: TEMPLATES[t] }); + this.updateSqlCode(); } private updateResources(skills): void { @@ -191,4 +194,43 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { } } + public updateSqlCode(): void { + const updateCondition = (conditionToAdd: string, include: boolean): void => { + const query = this.form.value.query; + + if (query.trim() !== '') { + if (include) { + // If including, add the new condition + const updatedQuery = this.insertTextAfterWhere(query, conditionToAdd); + this.form.patchValue({ query: updatedQuery }); + } else { + // If excluding, remove the existing condition + 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; // case-insensitive match for WHERE + 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 401d7a0..fbb1d29 100644 --- a/workbench/frontend/src/app/common/services/query.service.ts +++ b/workbench/frontend/src/app/common/services/query.service.ts @@ -110,52 +110,8 @@ export class QueryService { ); } - public queryResourceSkills, item extends { - id: string, - firstName: string, - lastName: 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`), - 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)) - ); - } - - public queryResourceSkills_new, item extends { + public queryResourceSkills, item extends { id: string, firstName: string, lastName: string, From 63ca5988d4df1b3f9dd0e2962e26f9e3d442235e Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 22 Nov 2023 15:06:46 +0100 Subject: [PATCH 12/17] caps in SQL --- .../components/resource-query/resource-query.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4d1476b..86525dd 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 @@ -211,8 +211,8 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { } }; - updateCondition('resource.crowdType LIKE \'Crowd\'\n\tAND ', this.crowdChecked); - updateCondition('resource.crowdType LIKE \'Non_Crowd\'\n\tAND ', this.internalChecked); + 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 { From e1a2350fdd68d21f178bdd2c3935843918c8d9b0 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Thu, 23 Nov 2023 12:44:06 +0100 Subject: [PATCH 13/17] adjust request payload accordingly --- .../services/slot-booking.service.ts | 11 +- .../slot-booking/slot-booking.component.html | 47 ++++-- .../slot-booking/slot-booking.component.ts | 153 ++++++++++++------ 3 files changed, 146 insertions(+), 65 deletions(-) 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..95f370b 100644 --- a/workbench/frontend/src/app/slot-booking/slot-booking.component.html +++ b/workbench/frontend/src/app/slot-booking/slot-booking.component.html @@ -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,9 +80,9 @@

Request

- - Full JSON Request -
{{ requestPayload$ | async | json }}
+ + Full JSON Request +
{{ requestPayload$ | async | json }}
@@ -91,7 +91,7 @@

Request

Response

- Full JSON Response + Full JSON Response
{{ (response$ | async)?.results | json }}
@@ -106,15 +106,28 @@

Response

+
+ Filter Based +
+ +
+ Internal + Crowd + Use mandatory skills +
+ - using /api/v3/job-slots/actions/search Search Job Slot API see + using + /api/v3/job-slots/actions/search + Search Job Slot API see + href="https://eu.coresystems.net/optimization/api/v1/swagger-ui/#/Search%20Job%20Slot/post_api_v3_job_slots_actions_search"> documentation @@ -123,7 +136,7 @@

Response

- Timing: {{ (response$ | async).time | number }} ms for {{ (response$ | async).grouped.length }} solts with + Timing: {{ (response$ | async).time | number }} ms for {{ (response$ | async).grouped.length }} slots with {{ (response$ | async).results.length }} matches
@@ -135,7 +148,7 @@

Slot Search Options:

🕑  When / Timing - {{ (requestPayload$ | async)?.slots.length }} slots + {{ (requestPayload$ | async)?.slots.length }} slots @@ -160,7 +173,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.ts b/workbench/frontend/src/app/slot-booking/slot-booking.component.ts index 61defe2..f81c091 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,31 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Component, OnDestroy, OnInit, } from '@angular/core'; -import { FormBuilder, FormGroup, } from '@angular/forms'; +import { Form, 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 { SlotSearchService, SearchRequest, SearchResponseWrapper, ResourceFilters } from './services/slot-booking.service'; import { Job } from './services/job.service'; +import { Event } from '@angular/router'; +import { animate, state, style, transition, trigger } from '@angular/animations'; @Component({ selector: 'slot-booking', templateUrl: './slot-booking.component.html', - styleUrls: ['./slot-booking.component.scss'] + styleUrls: ['./slot-booking.component.scss'], + animations: [ + trigger('highlight', [ + state('start', style({ + backgroundColor: 'yellow', + })), + state('end', style({ + backgroundColor: 'transparent', + })), + transition('* => *', animate('500ms')), + ]), + ], }) export class SlotBookingComponent implements OnInit, OnDestroy { @@ -27,6 +40,15 @@ export class SlotBookingComponent implements OnInit, OnDestroy { public requestOptions: FormGroup; private onDistroy$ = new Subject(); + public filterBased = false; + public internalChecked = false; + public crowdChecked = false; + public skillsChecked = false; + public filters$ = new BehaviorSubject(null); + + animationState = 'end'; + + constructor( private fb: FormBuilder, private service: SlotSearchService, @@ -38,7 +60,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 +69,7 @@ export class SlotBookingComponent implements OnInit, OnDestroy { return () => { clearInterval(timer); - } + }; }); autoRefresh$.pipe( @@ -56,72 +78,113 @@ 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'), + // Add any other operators or transformations you need here + ).subscribe(requestPayload => { + // Process the requestPayload as needed + this.updateRequestPayload(); + // After a short delay, reset the animation state + setTimeout(() => { + this.animationState = 'end'; + }, 1000); // Adjust the delay as needed + }); + + } + + + 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 => { + console.log(this.filterBased ? filters : { personIds }); + return { + job: { + durationMinutes: job.durationMinutes, + location: job.location, + mandatorySkills: job.mandatorySkills, + optionalSkills: job.optionalSkills, + udfValues: {} + }, + slots, + resources: this.filterBased ? filters : { personIds }, + options: { + maxResultsPerSlot: 8 + }, + policy: pluginEditor, + }; + }) + ); + } + + public doRequest(): void { this.isLoading$.next(true); @@ -166,13 +229,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 +243,7 @@ export class SlotBookingComponent implements OnInit, OnDestroy { ).subscribe(); } - public bookingLoading(isLoading: boolean) { + public bookingLoading(isLoading: boolean): void { this.isLoading$.next(isLoading); } From c612c4136f8a831b7885ca5a4fdbfde6679cb1cb Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 29 Nov 2023 09:43:46 +0100 Subject: [PATCH 14/17] adjust request payload accordingly no animation --- .../slot-booking/slot-booking.component.html | 5 ++- .../slot-booking/slot-booking.component.scss | 25 ++++++++----- .../slot-booking/slot-booking.component.ts | 35 ++++++------------- 3 files changed, 29 insertions(+), 36 deletions(-) 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 95f370b..9558933 100644 --- a/workbench/frontend/src/app/slot-booking/slot-booking.component.html +++ b/workbench/frontend/src/app/slot-booking/slot-booking.component.html @@ -82,7 +82,7 @@

Request

Full JSON Request -
{{ requestPayload$ | async | json }}
+
{{ requestPayload$ | async | json }}
@@ -103,11 +103,10 @@

Response

-
- Filter Based + Filter Based
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..a85805e 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; } @@ -50,3 +50,10 @@ margin-left: 3px; font-size: 9px; } + +.highlighted-resources { + background-color: khaki; + display: block; + padding-top: 8px; + padding-bottom: 8px; +} 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 f81c091..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,31 +1,19 @@ import { HttpErrorResponse } from '@angular/common/http'; -import { Component, OnDestroy, OnInit, } from '@angular/core'; -import { Form, 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, ResourceFilters } 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'; -import { animate, state, style, transition, trigger } from '@angular/animations'; @Component({ selector: 'slot-booking', templateUrl: './slot-booking.component.html', styleUrls: ['./slot-booking.component.scss'], - animations: [ - trigger('highlight', [ - state('start', style({ - backgroundColor: 'yellow', - })), - state('end', style({ - backgroundColor: 'transparent', - })), - transition('* => *', animate('500ms')), - ]), - ], }) export class SlotBookingComponent implements OnInit, OnDestroy { @@ -41,10 +29,11 @@ export class SlotBookingComponent implements OnInit, OnDestroy { private onDistroy$ = new Subject(); public filterBased = false; - public internalChecked = false; - public crowdChecked = false; + public internalChecked = true; + public crowdChecked = true; public skillsChecked = false; public filters$ = new BehaviorSubject(null); + public requestPayloadResources = null; animationState = 'end'; @@ -130,16 +119,12 @@ export class SlotBookingComponent implements OnInit, OnDestroy { this.requestPayload$.pipe( tap(_ => this.animationState = 'start'), - // Add any other operators or transformations you need here ).subscribe(requestPayload => { - // Process the requestPayload as needed this.updateRequestPayload(); - // After a short delay, reset the animation state setTimeout(() => { this.animationState = 'end'; - }, 1000); // Adjust the delay as needed + }, 1000); }); - } @@ -164,7 +149,9 @@ export class SlotBookingComponent implements OnInit, OnDestroy { pluginEditor && slots && job && (personIds.length || filters) )), map(([pluginEditor, slots, job, personIds, filters]): SearchRequest => { - console.log(this.filterBased ? filters : { personIds }); + + const newResources = this.filterBased ? filters : { personIds }; + return { job: { durationMinutes: job.durationMinutes, @@ -174,7 +161,7 @@ export class SlotBookingComponent implements OnInit, OnDestroy { udfValues: {} }, slots, - resources: this.filterBased ? filters : { personIds }, + resources: newResources, options: { maxResultsPerSlot: 8 }, From dfb54478c672624bd16ff0bcdd5e870b17c299e1 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 29 Nov 2023 14:06:46 +0100 Subject: [PATCH 15/17] skills sync --- .../resource-query.component.html | 18 ++++++++++--- .../resource-query.component.ts | 25 +++++++++++-------- .../common/services/shared-skill.service.ts | 2 +- .../job-builder/job-builder.component.ts | 4 +++ 4 files changed, 35 insertions(+), 14 deletions(-) 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 index 0bc0d0f..1957e54 100755 --- 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,10 +1,22 @@
-
+

Resources: {{ (resources$ | async)?.length }}

+
+ +

Selected Skills:

+ + + + {{ skill }} + close + + +
+ - - delete + [{{ resource.id | slice:0:6 }}] {{ resource.firstName }} {{ resource.lastName }} + close
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 86525dd..ac8412c 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 @@ -2,8 +2,8 @@ import { HttpErrorResponse } from '@angular/common/http'; 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 { BehaviorSubject, merge, Observable, of, Subject } from 'rxjs'; +import { catchError, map, mergeMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators'; import { QueryService } from '../../services/query.service'; import { SharedSkillsService } from '../../services/shared-skill.service'; @@ -56,7 +56,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { public allSkills = []; public skillResourcesMap: Map> = new Map(); public allResources: any[] = []; - public selectedSkills: string[] = []; + public selectedSkills$ = new BehaviorSubject([]); public selectedTemplate = 'default'; public crowdChecked = false; @@ -89,7 +89,6 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { public ngOnInit(): void { this.form = this.fb.group({ query: [TEMPLATES.default], - selectedSkills: [] }); this.svc.queryResourceSkills(TEMPLATES.default).subscribe(resources => { @@ -110,12 +109,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { this.allSkills = Array.from(this.skillResourcesMap.keys()); }); - this.form.get('selectedSkills').valueChanges.pipe( - takeUntil(this.onDestroy$) - ).subscribe(selectedSkills => { - this.selectedSkills = selectedSkills; - this.updateResources(this.selectedSkills); - }); + this.selectedSkills$ = this.sharedSkillsService.selectedSkills$; this.sharedSkillsService.selectedSkills$.subscribe((selectedSkills) => { this.updateResources(selectedSkills); @@ -168,6 +162,17 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { ).subscribe(); } + + public removeSkill(skill: string): void { + this.sharedSkillsService.selectedSkills$.pipe( + take(1), // Take only one emission to avoid unnecessary subscriptions + map((selectedSkills) => selectedSkills.filter((s) => s !== skill)) + ).subscribe((updatedSkills) => { + this.selectedSkills$.next(updatedSkills); + }); + this.selectedSkills$ = this.sharedSkillsService.selectedSkills$; + } + public doQuery(): void { this.onQuery.next(); } diff --git a/workbench/frontend/src/app/common/services/shared-skill.service.ts b/workbench/frontend/src/app/common/services/shared-skill.service.ts index a0edd3b..2f4a587 100644 --- a/workbench/frontend/src/app/common/services/shared-skill.service.ts +++ b/workbench/frontend/src/app/common/services/shared-skill.service.ts @@ -7,7 +7,7 @@ import { BehaviorSubject } from 'rxjs'; }) export class SharedSkillsService { private selectedSkillsSubject = new BehaviorSubject([]); - selectedSkills$ = this.selectedSkillsSubject.asObservable(); + 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 e327081..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 @@ -121,6 +121,10 @@ export class JobBuilderComponent implements OnInit, OnDestroy, AfterContentInit 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.onDestroy$) From 7e5145e4d603c1c0f405870aca21ade28ca4339c Mon Sep 17 00:00:00 2001 From: Davo00 Date: Wed, 29 Nov 2023 15:39:48 +0100 Subject: [PATCH 16/17] moved the search component --- .../slot-booking/slot-booking.component.html | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) 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 9558933..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 @@ -80,6 +80,41 @@

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 }}
@@ -103,43 +138,7 @@

Response

- - -
- 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 -
-

Slot Search Options:

From 88759a29cf152531ffc467d84a36d07ace75f6d4 Mon Sep 17 00:00:00 2001 From: Davo00 Date: Thu, 30 Nov 2023 10:20:03 +0100 Subject: [PATCH 17/17] cleaned up --- workbench/frontend/src/app/app.module.ts | 2 -- .../resource-query.component.html | 21 ++---------------- .../resource-query.component.scss | 12 +++++----- .../resource-query.component.ts | 22 +++++++++++++------ .../common/services/shared-skill.service.ts | 1 - .../slot-booking/slot-booking.component.scss | 7 ------ 6 files changed, 23 insertions(+), 42 deletions(-) diff --git a/workbench/frontend/src/app/app.module.ts b/workbench/frontend/src/app/app.module.ts index 737918a..d2b02b9 100644 --- a/workbench/frontend/src/app/app.module.ts +++ b/workbench/frontend/src/app/app.module.ts @@ -38,7 +38,6 @@ import { ResourceQueryComponent } from './common/components/resource-query/resou import { SaveDialog } from './common/components/plugin-editor/save-dialog/save-dialog.component'; import { SelectSheet } from './common/components/select-sheet/select-sheet.component'; import { PluginEditorComponent } from './common/components/plugin-editor/plugin-editor.component'; -import { MatRadioModule } from '@angular/material/radio'; @NgModule({ @@ -67,7 +66,6 @@ import { MatRadioModule } from '@angular/material/radio'; MonacoEditorModule.forRoot(ngxMonacoEditorConfig), FlexLayoutModule, ...MatModules, - MatRadioModule, FormsModule ], providers: [ 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 index 1957e54..43c8de7 100755 --- 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 @@ -37,8 +37,8 @@

Query Templates:

- Crowd - Internal + Crowd + Internal
@@ -52,23 +52,6 @@

Query Templates:

- - -
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 ad9037f..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 @@ -1,17 +1,17 @@ .selected { - background-color: #e0e0e0; /* Greyish color for selected tab */ - color: #333; /* Adjust text color for better contrast */ - border-bottom: 3px solid #ccc; /* Border color for selected tab */ + background-color: #e0e0e0; + color: #333; + border-bottom: 3px solid #ccc; } .mat-button { - transition: background-color 0.3s; /* Add a smooth transition for background color */ + transition: background-color 0.3s; } .mat-button:not(.selected):hover { - background-color: #e0e0e0; /* Change background color on hover for non-selected buttons */ + background-color: #e0e0e0; } .selected:hover { - background-color: #e0e0e0; /* Change background color on hover for non-selected buttons */ + 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 ac8412c..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 @@ -2,7 +2,7 @@ import { HttpErrorResponse } from '@angular/common/http'; 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, Observable, of, Subject } from 'rxjs'; +import { BehaviorSubject, merge, of, Subject } from 'rxjs'; import { catchError, map, mergeMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators'; import { QueryService } from '../../services/query.service'; import { SharedSkillsService } from '../../services/shared-skill.service'; @@ -165,7 +165,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { public removeSkill(skill: string): void { this.sharedSkillsService.selectedSkills$.pipe( - take(1), // Take only one emission to avoid unnecessary subscriptions + take(1), map((selectedSkills) => selectedSkills.filter((s) => s !== skill)) ).subscribe((updatedSkills) => { this.selectedSkills$.next(updatedSkills); @@ -199,17 +199,27 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { } } + + 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) { - // If including, add the new condition const updatedQuery = this.insertTextAfterWhere(query, conditionToAdd); this.form.patchValue({ query: updatedQuery }); } else { - // If excluding, remove the existing condition const modifiedQuery = this.removeCondition(query, conditionToAdd); this.form.patchValue({ query: modifiedQuery }); } @@ -221,7 +231,7 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { } private insertTextAfterWhere(input: string, newText: string): string { - const whereRegex = /\bWHERE\b/i; // case-insensitive match for WHERE + const whereRegex = /\bWHERE\b/i; const whereMatch = whereRegex.exec(input); if (whereMatch) { @@ -236,6 +246,4 @@ export class ResourceQueryComponent implements OnInit, OnDestroy { private removeCondition(query: string, conditionToRemove: string): string { return query.replace(conditionToRemove, ''); } - - } diff --git a/workbench/frontend/src/app/common/services/shared-skill.service.ts b/workbench/frontend/src/app/common/services/shared-skill.service.ts index 2f4a587..ddcf4ff 100644 --- a/workbench/frontend/src/app/common/services/shared-skill.service.ts +++ b/workbench/frontend/src/app/common/services/shared-skill.service.ts @@ -1,4 +1,3 @@ -// shared-skills.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; 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 a85805e..82c08b4 100644 --- a/workbench/frontend/src/app/slot-booking/slot-booking.component.scss +++ b/workbench/frontend/src/app/slot-booking/slot-booking.component.scss @@ -50,10 +50,3 @@ margin-left: 3px; font-size: 9px; } - -.highlighted-resources { - background-color: khaki; - display: block; - padding-top: 8px; - padding-bottom: 8px; -}