feat(handledare): Added functionality to change handledare inside deltagare-card. Also implemented same component inside avrop. (TV-603)

Squashed commit of the following:

commit 7a7db1d1eb43ac059fe012cd53e59a74410b86be
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 12:14:36 2021 +0200

    Fixed imports

commit 7f312731fc3fb1dd7b0ae6e0e4e88598ab45db70
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 12:07:42 2021 +0200

    Updated service

commit 30164f5d5bc452727408c57ea16aeb87e5e5c91e
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 10:06:50 2021 +0200

    Fixed test

commit 5e5c7f54d9338ba8c5d8c97381e33cfd8ecaaa52
Merge: bb0e92e0 c2a02dba
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 09:57:28 2021 +0200

    Merge branch 'develop' into feature/TV-603-erik

commit bb0e92e0d515cc4cca059e09d7dd887ceb074c95
Merge: 500b37b9 93556d48
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Oct 4 08:58:08 2021 +0200

    Merged develop, fixed conflicts and fixed some minor issues

commit 500b37b9d640f5a181fe5080c5f2d213fa1e0182
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 29 16:03:31 2021 +0200

    Fixed error handling

commit 60e753d3eebf94d3a0823a752dd220e2ed171d14
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 29 15:47:38 2021 +0200

    Cleanup

commit 0ef8c0df78e6c8a6301df73d9275b4b153fcc747
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 29 15:38:40 2021 +0200

    Implemented handledare-service and handledare picker inside avrop and deltagare card

commit 89f03f6be1872cc1db83b81f881793ce9806ce4a
Merge: 5d2a6876 776889ae
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 29 11:02:41 2021 +0200

    Merge branch 'develop' into feature/TV-603-erik

commit 5d2a687694c52e6591f1dea8b553f03b3c2c821f
Merge: 548dd2be 82bcab40
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 29 10:55:21 2021 +0200

    Merged develop and fixed conflicts

commit 548dd2bea713af4b17f5e97f4fc315bd8b0d92c3
Author: WP\holno <nikola.holst-nikolic@arbetsformedlingen.se>
Date:   Mon Sep 27 13:46:27 2021 +0200

    Cleanup

commit 1bafcc6045506e87319ddf8cf51447ff87816494
Author: WP\holno <nikola.holst-nikolic@arbetsformedlingen.se>
Date:   Mon Sep 27 11:27:31 2021 +0200

    Implementation of Handledare select on avrop.

commit d20285e3ccff7761c88d3a78592262de6ad4dee8
Author: WP\holno <nikola.holst-nikolic@arbetsformedlingen.se>
Date:   Fri Sep 24 14:35:34 2021 +0200

    Added validation-messages

commit 4a5771e05104ba3c7e771e9c028faa349e4de14d
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Sep 24 12:52:28 2021 +0200

    Assign handledare from deltagare-card
This commit is contained in:
Erik Tiekstra
2021-10-04 12:19:35 +02:00
parent c2a02dbae9
commit d139f7504b
24 changed files with 362 additions and 69 deletions

View File

@@ -0,0 +1,35 @@
<form class="handledare-picker-form" [formGroup]="formGroup" (ngSubmit)="onFormSubmitted()">
<digi-ng-form-select
class="handledare-picker-form__select"
[formControl]="handledareFormControl"
[afLabel]="label"
[afPlaceholder]="label"
[afSelectItems]="selectableHandledare"
[afDisableValidStyle]="true"
[afRequired]="true"
[afAnnounceIfOptional]="true"
[afInvalid]="(handledareFormControl.invalid && handledareFormControl.touched) || invalid"
(afOnChange)="handledareChanged()"
></digi-ng-form-select>
<digi-button *ngIf="!skipSubmit" class="handledare-picker-form__submit" af-type="submit">
{{submitText}}
</digi-button>
<div class="handledare-picker-form__validation-wrapper" aria-atomic="true" role="alert">
<digi-form-validation-message
class="handledare-picker__validation-message"
*ngIf="(handledareFormControl.invalid && handledareFormControl.touched) || invalid"
af-variation="error"
>
Handledare måste väljas för att kunna spara
</digi-form-validation-message>
<digi-form-validation-message
*ngIf="(lastSavedHandledare$ | async) && submitted"
class="handledare-picker__validation-message"
af-variation="success"
>
Vald handledare har uppdaterats
</digi-form-validation-message>
</div>
<msfa-loader *ngIf="submitHandledareLoading$ | async" size="s" type="absolute"></msfa-loader>
</form>

View File

@@ -0,0 +1,35 @@
@import 'variables/gutters';
.handledare-picker-form {
position: relative;
display: grid;
grid-template-columns: 1fr auto;
column-gap: $digi--layout--gutter;
row-gap: $digi--layout--gutter--s;
grid-template-areas:
'select select'
'validation validation'
'submit .';
&__select {
grid-area: select;
}
&__submit {
grid-area: submit;
align-self: end;
}
&__validation-wrapper {
grid-area: validation;
}
&__validation-message {
display: block;
margin-bottom: $digi--layout--gutter--s;
}
::ng-deep .digi-ng-form-select__footer {
display: none !important;
}
}

View File

@@ -0,0 +1,29 @@
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HandledarePickerFormComponent } from './handledare-picker-form.component';
describe('HandledarePickerFormComponent', () => {
let component: HandledarePickerFormComponent;
let fixture: ComponentFixture<HandledarePickerFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, HttpClientTestingModule, FormsModule, DigiNgFormSelectModule],
declarations: [HandledarePickerFormComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HandledarePickerFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,77 @@
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { Handledare } from '@msfa-models/handledare.model';
import { HandledareService } from '@msfa-services/handledare.service';
import { RequiredValidator } from '@msfa-utils/validators/required.validator';
import { Observable } from 'rxjs';
@Component({
selector: 'msfa-handledare-picker-form',
templateUrl: './handledare-picker-form.component.html',
styleUrls: ['./handledare-picker-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HandledarePickerFormComponent implements OnChanges {
@Input() selectedHandledareId: string;
@Input() handledare: Handledare[];
@Input() avropIds: string[];
@Input() label = 'Välj handledare';
@Input() submitText = 'Spara handledare';
@Input() skipSubmit = false;
@Input() invalid = false;
@Output() selectedHandledareChanged = new EventEmitter<string>();
formGroup: FormGroup = new FormGroup({
handledare: new FormControl(null, [RequiredValidator('Handledare')]),
});
selectableHandledare: FormSelectItem[] = [];
submitted = false;
lastSavedHandledare$: Observable<Handledare> = this.handledareService.lastSavedHandledare$;
submitHandledareLoading$: Observable<boolean> = this.handledareService.submitHandledareLoading$;
constructor(private handledareService: HandledareService) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.selectedHandledareId) {
this.handledareFormControl.patchValue(this.selectedHandledareId);
}
if (changes.handledare && this.handledare?.length) {
this.selectableHandledare = this.handledare.map(({ ciamUserId, fullName }) => ({
name: fullName,
value: ciamUserId,
}));
}
}
get handledareFormControl(): AbstractControl {
return this.formGroup.get('handledare');
}
handledareChanged(): void {
this.submitted = false;
this.selectedHandledareChanged.emit(this.handledareFormControl.value);
}
onFormSubmitted(): void {
this.submitted = true;
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid || this.handledareFormControl.value === this.selectedHandledareId) {
return;
}
const newHandledare = this.handledare.find(
handledare => handledare.ciamUserId === this.handledareFormControl.value
);
this.handledareService.assignHandledare(this.avropIds, newHandledare);
}
}

View File

@@ -0,0 +1,14 @@
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { LoaderModule } from '../loader/loader.module';
import { HandledarePickerFormComponent } from './handledare-picker-form.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [HandledarePickerFormComponent],
imports: [CommonModule, ReactiveFormsModule, LoaderModule, DigiNgFormSelectModule],
exports: [HandledarePickerFormComponent],
})
export class HandledarePickerFormModule {}

View File

@@ -1,3 +1,3 @@
<div [ngClass]="classes">
<digi-icon-spinner class="loader__spinner"></digi-icon-spinner>
<div [ngClass]="loaderClass">
<digi-icon-spinner [ngClass]="spinnerClass"></digi-icon-spinner>
</div>

View File

@@ -28,5 +28,9 @@
&__spinner {
display: inline-flex;
animation: spinning 1s linear infinite;
&--s {
width: 2.5rem;
}
}
}

View File

@@ -10,11 +10,16 @@ import { LoaderType } from '@msfa-enums/loader-type.enum';
export class LoaderComponent {
private readonly _defaultClass = 'loader';
@Input() type: LoaderType;
@Input() size: 's' | 'm' = 'm';
get classes(): string {
get loaderClass(): string {
if (this.type) {
return `${this._defaultClass} ${this._defaultClass}--${this.type as string}`;
}
return this._defaultClass;
}
get spinnerClass(): string {
return `${this._defaultClass}__spinner ${this._defaultClass}__spinner--${this.size}`;
}
}

View File

@@ -41,7 +41,6 @@ export function mapAvropResponseToAvrop(data: AvropResponse): Avrop {
sprakstod,
adress,
sparkod,
sparNamn,
genomforandeReferens,
deltagandeGrad,
utforandeverksamhet,

View File

@@ -3,14 +3,13 @@ import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { AvropFilterResponse } from '@msfa-models/api/avrop-filter.response.model';
import { AvropApiResponse } from '@msfa-models/api/avrop.response.model';
import { HandledareResponse } from '@msfa-models/api/handledare.response.model';
import { Params } from '@msfa-models/api/params.model';
import { AvropFilter, mapResponseToAvropFilter } from '@msfa-models/avrop-filter.model';
import { AvropCompact, AvropCompactData, mapAvropResponseToAvrop } from '@msfa-models/avrop.model';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error';
import { Handledare, mapHandledareResponseToHandledare } from '@msfa-models/handledare.model';
import { Handledare } from '@msfa-models/handledare.model';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, filter, map, tap } from 'rxjs/operators';
import { catchError, filter, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
@@ -46,30 +45,6 @@ export class AvropApiService {
);
}
fetchAvailableHandledare$(avrop: AvropCompact[]): Observable<Handledare[]> {
const lockedAvropIsEqual = this.lockedAvropValue?.every(
(lockedAvrop, index) => lockedAvrop.id === avrop[index]?.id
);
// Checking to see if we really need to make a new api-request
if (lockedAvropIsEqual) {
return of(this._availableHandledareSnapshot$.getValue());
}
this._lockedAvropSnapshot$.next(avrop);
return this.httpClient
.get<{ data: HandledareResponse[] }>(`${this._apiBaseUrl}/handledare`, {
params: { avropIds: avrop.map(a => a.id) },
})
.pipe(
map(({ data }) => data.map(handledare => mapHandledareResponseToHandledare(handledare))),
tap(handledare => {
this._availableHandledareSnapshot$.next(handledare);
})
);
}
fetchAvailableTjanster$(params: Params): Observable<AvropFilter[]> {
return this.httpClient
.get<{ data: AvropFilterResponse[] }>(`${this._apiBaseUrl}/tjanster`, { params })

View File

@@ -0,0 +1,43 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { HandledareResponse } from '@msfa-models/api/handledare.response.model';
import { Params } from '@msfa-models/api/params.model';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error';
import { Handledare, mapHandledareResponseToHandledare } from '@msfa-models/handledare.model';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class HandledareApiService {
private _apiBaseUrl = `${environment.api.url}/avrop/handledare`;
constructor(private httpClient: HttpClient) {}
public fetchAvailableHandledare$(avropIds: string[]): Observable<Handledare[]> {
return this.httpClient
.get<{ data: HandledareResponse[] }>(`${this._apiBaseUrl}`, {
params: { avropIds },
})
.pipe(
map(({ data }) => data.map(handledare => mapHandledareResponseToHandledare(handledare))),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta handledare.\n\n${error.message}` })
);
})
);
}
async assignHandledare(avropIds: string[], handledare: Handledare): Promise<void> {
const params: Params = {
avropIds,
ciamUserId: handledare.ciamUserId,
};
return this.httpClient
.patch<void>(`${this._apiBaseUrl}/assign`, null, { params })
.toPromise();
}
}

View File

@@ -6,6 +6,7 @@ import { AvropApiService } from '@msfa-services/api/avrop-api.service';
import { MultiselectFilterOption } from '@msfa-shared/components/multiselect/multiselect-filter-option';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { HandledareApiService } from './api/handledare.api.service';
type Step = 1 | 2 | 3 | 4;
@@ -103,7 +104,9 @@ export class AvropService {
public availableHandledare$: Observable<Handledare[]> = this._lockedAvrop$.pipe(
filter(lockedAvrop => !!lockedAvrop?.length),
switchMap(lockedAvrop => this.avropApiService.fetchAvailableHandledare$(lockedAvrop))
map(lockedAvrop => lockedAvrop.map(avrop => avrop.id)),
switchMap(lockedAvrop => this.handledareApiService.fetchAvailableHandledare$(lockedAvrop)),
shareReplay(1)
);
public currentStep$: Observable<Step> = combineLatest([
@@ -194,7 +197,7 @@ export class AvropService {
this._selectedAvrop$.next(deltagare);
}
constructor(private avropApiService: AvropApiService) {}
constructor(private avropApiService: AvropApiService, private handledareApiService: HandledareApiService) {}
public resetError(): void {
this._error$.next(null);

View File

@@ -1,46 +0,0 @@
import { Injectable } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { ContactInformation } from '@msfa-models/contact-information.model';
import { Disability } from '@msfa-models/disability.model';
import { DriversLicense } from '@msfa-models/drivers-license.model';
import { Education } from '@msfa-models/education.model';
import { HighestEducation } from '@msfa-models/highest-education.model';
import { ReportsData } from '@msfa-models/reports.model';
import { WorkExperience } from '@msfa-models/work-experience.model';
import { Observable } from 'rxjs';
import { DeltagareApiService } from './api/deltagare.api.service';
@Injectable({
providedIn: 'root',
})
export class DeltagareCardService {
constructor(private deltagareApiService: DeltagareApiService) {}
public fetchContactInformation$(genomforandeReferens: number): Observable<ContactInformation> {
return this.deltagareApiService.fetchContactInformation$(genomforandeReferens);
}
public fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
}
public fetchWorkExperiences$(genomforandeReferens: number): Observable<WorkExperience[]> {
return this.deltagareApiService.fetchWorkExperiences$(genomforandeReferens);
}
public fetchHighestEducation$(genomforandeReferens: number): Observable<HighestEducation> {
return this.deltagareApiService.fetchHighestEducation$(genomforandeReferens);
}
public fetchEducations$(genomforandeReferens: number): Observable<Education[]> {
return this.deltagareApiService.fetchEducations$(genomforandeReferens);
}
public fetchDriversLicense$(genomforandeReferens: number): Observable<DriversLicense> {
return this.deltagareApiService.fetchDriversLicense$(genomforandeReferens);
}
public fetchWorkLanguages$(genomforandeReferens: number): Observable<string[]> {
return this.deltagareApiService.fetchWorkLanguages$(genomforandeReferens);
}
public fetchDisabilities$(genomforandeReferens: number): Observable<Disability[]> {
return this.deltagareApiService.fetchDisabilities$(genomforandeReferens);
}
public fetchReports$(limit: number, page: number, genomforandeReferens: number): Observable<ReportsData> {
return this.deltagareApiService.fetchReports$(limit, page, genomforandeReferens);
}
}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { errorToCustomError } from '@msfa-models/error/custom-error';
import { Handledare } from '@msfa-models/handledare.model';
import { BehaviorSubject, Observable } from 'rxjs';
import { HandledareApiService } from './api/handledare.api.service';
import { ErrorService } from './error.service';
@Injectable({
providedIn: 'root',
})
export class HandledareService {
private _lastSavedHandledare$ = new BehaviorSubject<Handledare>(null);
public lastSavedHandledare$: Observable<Handledare> = this._lastSavedHandledare$.asObservable();
private _submitHandledareLoading$ = new BehaviorSubject<boolean>(false);
public submitHandledareLoading$: Observable<boolean> = this._submitHandledareLoading$.asObservable();
constructor(private handledareApiService: HandledareApiService, private errorService: ErrorService) {}
public resetLastSavedHandledare(): void {
this._lastSavedHandledare$.next(null);
}
public fetchAvailableHandledare$(avropIds: string[]): Observable<Handledare[]> {
return this.handledareApiService.fetchAvailableHandledare$(avropIds);
}
public assignHandledare(avropIds: string[], handledare: Handledare): void {
this._submitHandledareLoading$.next(true);
this.handledareApiService
.assignHandledare(avropIds, handledare)
.then(() => {
this._lastSavedHandledare$.next(handledare);
})
.catch((error: Error) => {
this.errorService.add(
errorToCustomError({ ...error, message: `Kunde inte tilldela handledare.\n\n${error.message}` })
);
})
.finally(() => {
this._submitHandledareLoading$.next(false);
});
}
}