feat(felhantering): Ändrat felhantering och loggning. (TV-945)

Merge in TEA/mina-sidor-fa-web from feature/TV-945-felhantering to develop

Squashed commit of the following:

commit b621bd7d9dd0a03a22476f196521f2535731fa12
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Dec 3 16:01:06 2021 +0100

    Added better error-handling to employee

commit 876ed3caf6ff1ffb98bb16491526e4417086cba9
Merge: 02607a5f ec63435f
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Dec 3 15:24:31 2021 +0100

    Merge branch 'develop' into feature/TV-945-felhantering

commit 02607a5f007dc7e46d61460fc71a1b27bdda9392
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Dec 3 08:25:49 2021 +0100

    Added better error-handling to deltagare händelser

commit 30c2726ccebc73a2ca9a0c72cdc564cad2ac82aa
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Dec 3 08:17:22 2021 +0100

    Updated deltagare error handling with data

commit 893de8478e5a2919c684667eb31afd35986cb396
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Dec 3 08:05:50 2021 +0100

    Added better error-handling to avvikelse

commit 5c64b8c10a7f3fb2cec5cab2c8d86073169a6033
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Dec 3 07:47:59 2021 +0100

    Added better error-handling to authentication

commit 8fa187d4da0b75d2bb62bc16cdcf540064bd4433
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Dec 3 07:47:43 2021 +0100

    Added better error-handling to avrop

commit 3bd23e6ad642e95caa5bd88215442281495f970c
Merge: f941d144 938014ab
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Thu Dec 2 13:02:08 2021 +0100

    Merge branch 'develop' into feature/TV-945-felhantering

commit f941d14435e1ed3e371cee84ef85d508ed70b2ce
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Dec 1 16:08:00 2021 +0100

    Added improved error-handling to deltagare-api.service

commit 3889b398d9ce0e5e1b6498e10794a946b65c2a47
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Dec 1 15:45:36 2021 +0100

    Added better error-handling connected to APM
This commit is contained in:
Erik Tiekstra
2021-12-06 09:54:02 +01:00
parent ec63435fc5
commit d270119e93
22 changed files with 387 additions and 255 deletions

View File

@@ -19,6 +19,7 @@ const providers: Provider[] = [
ApmErrorHandler, ApmErrorHandler,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: LOCALE_ID, useValue: 'sv-SE' }, { provide: LOCALE_ID, useValue: 'sv-SE' },
{ provide: ErrorHandler, useClass: CustomErrorHandler },
]; ];
// Skip error handler in Dev until "Uncaught Error: ApplicationRef.tick is called recursively" is fixed // Skip error handler in Dev until "Uncaught Error: ApplicationRef.tick is called recursively" is fixed

View File

@@ -8,7 +8,7 @@
<ng-container *ngIf="errors.length"> <ng-container *ngIf="errors.length">
<h3>{{ errors.length }} fel har uppstått!</h3> <h3>{{ errors.length }} fel har uppstått!</h3>
<ul> <ul>
<li *ngFor="let error of errors">{{ error.name }}: {{ error.message }}</li> <li *ngFor="let error of errors">{{ error.type }}: {{ error.message }}</li>
</ul> </ul>
</ng-container> </ng-container>
</div> </div>

View File

@@ -16,7 +16,8 @@
<button class="toast__close-button" aria-label="Stäng meddelandet" (click)="emitCloseEvent()"> <button class="toast__close-button" aria-label="Stäng meddelandet" (click)="emitCloseEvent()">
<ui-icon [uiType]="UiIconType.X" [uiSize]="UiIconSize.L"></ui-icon> <ui-icon [uiType]="UiIconType.X" [uiSize]="UiIconSize.L"></ui-icon>
</button> </button>
<h3 class="toast__heading">{{ error.name }}</h3> <h3 class="toast__heading">{{ error.type }}</h3>
<p class="toast__message">{{ error.message }}</p> <p class="toast__message">{{ error.message }}</p>
<span class="toast__error-id">Error id: {{error.id}}</span>
</div> </div>
</div> </div>

View File

@@ -62,4 +62,10 @@
padding: var(--digi--layout--gutter--s); padding: var(--digi--layout--gutter--s);
color: var(--digi--typography--color--text); color: var(--digi--typography--color--text);
} }
&__error-id {
align-self: flex-end;
font-size: var(--digi--typography--font-size--s);
margin-top: var(--digi--layout--gutter--s);
}
} }

View File

@@ -1,8 +1,8 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ErrorSeverity } from '@msfa-enums/error-severity.enum'; import { ErrorSeverity } from '@msfa-enums/error-severity.enum';
import { UiIconType } from '@ui/icon/icon-type.enum';
import { CustomError } from '@msfa-models/error/custom-error'; import { CustomError } from '@msfa-models/error/custom-error';
import { UiIconSize } from '@ui/icon/icon-size.enum'; import { UiIconSize } from '@ui/icon/icon-size.enum';
import { UiIconType } from '@ui/icon/icon-type.enum';
@Component({ @Component({
selector: 'msfa-toast', selector: 'msfa-toast',
@@ -19,11 +19,11 @@ export class ToastComponent implements AfterViewInit {
ErrorSeverity = ErrorSeverity; ErrorSeverity = ErrorSeverity;
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this.error.removeAfter) { // if (this.error.removeAfter) {
setTimeout(() => { // setTimeout(() => {
this.closeToast.emit(this.error); // this.closeToast.emit(this.error);
}, this.error.removeAfter); // }, this.error.removeAfter);
} // }
} }
get className(): string { get className(): string {

View File

@@ -10,6 +10,7 @@ import { environment } from '@msfa-environment';
export class LoggingModule { export class LoggingModule {
private _elasticConfig = environment.elastic; private _elasticConfig = environment.elastic;
private _activeFeatures = environment.activeFeatures; private _activeFeatures = environment.activeFeatures;
private _version = environment.version;
constructor(private apmService: ApmService) { constructor(private apmService: ApmService) {
if (this._elasticConfig && this._activeFeatures.includes(Feature.LOGGING)) { if (this._elasticConfig && this._activeFeatures.includes(Feature.LOGGING)) {
@@ -18,6 +19,7 @@ export class LoggingModule {
serviceName, serviceName,
serverUrl, serverUrl,
environment: this.currentEnvironment, environment: this.currentEnvironment,
serviceVersion: this._version,
}); });
} }
} }

View File

@@ -59,8 +59,9 @@ export class EmployeeFormComponent implements OnInit {
next: () => { next: () => {
void this.router.navigateByUrl(`/administration/personal/${this.employeeId}`); void this.router.navigateByUrl(`/administration/personal/${this.employeeId}`);
}, },
error: error => { error: (error: CustomError) => {
this._errorWhileUpdating$.next(error); this._errorWhileUpdating$.next(error);
throw error;
}, },
complete: () => { complete: () => {
updateEmployeeSubscription.unsubscribe(); updateEmployeeSubscription.unsubscribe();

View File

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
import { Avrop, AvropAndMeta } from '@msfa-models/avrop.model'; import { Avrop, AvropAndMeta } from '@msfa-models/avrop.model';
import { Handledare } from '@msfa-models/handledare.model'; import { Handledare } from '@msfa-models/handledare.model';
import { AvropService } from '@msfa-services/avrop.service'; import { AvropService } from '@msfa-services/avrop.service';
import { Observable, BehaviorSubject } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
@Component({ @Component({
selector: 'msfa-avrop', selector: 'msfa-avrop',

View File

@@ -14,9 +14,10 @@ export class AvvikelseReportFormService {
.fetchAvvikelseQuestions$() .fetchAvvikelseQuestions$()
.pipe(shareReplay(1)); .pipe(shareReplay(1));
fetchAvvikelseReasons$: Observable<AvvikelseReason[]> = this.avvikelseApiService fetchAvvikelseReasons$: Observable<AvvikelseReason[]> = this.avvikelseApiService.fetchAvvikelseReasons$().pipe(
.fetchAvvikelseReasons$() map(reasons => sortAvvikelseReasons(reasons)),
.pipe(map(reasons => sortAvvikelseReasons(reasons))); shareReplay(1)
);
constructor(private avvikelseApiService: AvvikelseApiService, private deltagareApiService: DeltagareApiService) {} constructor(private avvikelseApiService: AvvikelseApiService, private deltagareApiService: DeltagareApiService) {}

View File

@@ -1,15 +1,27 @@
import { ErrorHandler, Injectable } from '@angular/core'; import { ErrorHandler, Injectable } from '@angular/core';
import { ApmErrorHandler } from '@elastic/apm-rum-angular'; import { ApmErrorHandler, ApmService } from '@elastic/apm-rum-angular';
import { Feature } from '@msfa-enums/feature.enum'; import { Feature } from '@msfa-enums/feature.enum';
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
import { CustomError } from '@msfa-models/error/custom-error'; import { CustomError } from '@msfa-models/error/custom-error';
import { ErrorService } from '@msfa-services/error.service'; import { ErrorService } from '@msfa-services/error.service';
interface ApmError {
id: string;
name: string;
message: string;
type: string;
method: string;
timestamp: Date;
}
@Injectable() @Injectable()
export class CustomErrorHandler implements ErrorHandler { export class CustomErrorHandler implements ErrorHandler {
private _elasticConfig = environment.elastic; private _elasticConfig = environment.elastic;
private _activeFeatures = environment.activeFeatures; private _activeFeatures = environment.activeFeatures;
constructor(private errorService: ErrorService, public apmErrorHandler: ApmErrorHandler) {} constructor(
private errorService: ErrorService,
public apmErrorHandler: ApmErrorHandler,
private apmService: ApmService
) {}
handleError(customError: CustomError & { ngDebugContext: unknown }): void { handleError(customError: CustomError & { ngDebugContext: unknown }): void {
if (!customError.avoidToast) { if (!customError.avoidToast) {
@@ -17,7 +29,11 @@ export class CustomErrorHandler implements ErrorHandler {
} }
if (this._elasticConfig && this._activeFeatures.includes(Feature.LOGGING)) { if (this._elasticConfig && this._activeFeatures.includes(Feature.LOGGING)) {
this.apmErrorHandler.handleError(customError); const { id, method, name, type, message, timestamp, data } = customError;
const apmError: ApmError = { id, method, name, type, message, timestamp };
this.apmService.apm.addLabels({ id, method, data });
this.apmService.apm.captureError(apmError);
} }
} }
} }

View File

@@ -1,21 +0,0 @@
export interface Authorization {
id: string;
name: string;
}
export interface AuthorizationApiResponse {
data: AuthorizationApiResponseData[];
}
export interface AuthorizationApiResponseData {
id: string;
name: string;
}
export function mapAuthorizationApiResponseToAuthorization(data: AuthorizationApiResponseData): Authorization {
const { id, name } = data;
return {
id,
name,
};
}

View File

@@ -1,3 +1,4 @@
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorSeverity } from '@msfa-enums/error-severity.enum'; import { ErrorSeverity } from '@msfa-enums/error-severity.enum';
import { ErrorType } from '@msfa-enums/error-type.enum'; import { ErrorType } from '@msfa-enums/error-type.enum';
@@ -7,15 +8,20 @@ export class CustomError implements Error {
message: string; message: string;
stack: string; stack: string;
type: ErrorType; type: ErrorType;
data: string;
method: string;
severity: ErrorSeverity; severity: ErrorSeverity;
timestamp: Date; timestamp: Date;
error: Error; error: Error | HttpErrorResponse;
removeAfter: number; removeAfter: number;
avoidToast?: boolean; avoidToast?: boolean;
constructor(args: { constructor(args: {
error: Error; error: Error | HttpErrorResponse;
type?: ErrorType; type?: ErrorType;
name?: string;
data?: unknown;
method?: string;
message?: string; message?: string;
severity?: ErrorSeverity; severity?: ErrorSeverity;
stack?: string; stack?: string;
@@ -23,7 +29,10 @@ export class CustomError implements Error {
}) { }) {
this.timestamp = new Date(); this.timestamp = new Date();
this.id = this.timestamp.getTime().toString(); this.id = this.timestamp.getTime().toString();
this.type = this.name = args.type || CustomError.getErrorType(args.error); this.type = args.type || CustomError.getErrorType(args.error);
this.name = args.name || args.error?.name || this.type;
this.method = args.method || '';
this.data = JSON.stringify(args.data);
this.message = args.message || args.error.message; this.message = args.message || args.error.message;
this.severity = args.severity || ErrorSeverity.HIGH; this.severity = args.severity || ErrorSeverity.HIGH;
this.stack = args.stack || CustomError.getStack(args.error); this.stack = args.stack || CustomError.getStack(args.error);

View File

@@ -11,6 +11,7 @@ import {
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model'; import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model';
import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model'; import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model';
import { CustomError } from '@msfa-models/error/custom-error';
import { add, isAfter, isBefore, sub } from 'date-fns'; import { add, isAfter, isBefore, sub } from 'date-fns';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
@@ -103,12 +104,22 @@ export class AuthenticationService {
login$(authorizationCodeFromCiam: string): Observable<Authentication> { login$(authorizationCodeFromCiam: string): Observable<Authentication> {
this.removeLocalStorageData(); this.removeLocalStorageData();
const apiUrl = AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam);
return this.httpClient return this.httpClient
.get<{ data: AuthenticationResponse }>(AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam)) .get<{ data: AuthenticationResponse }>(AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam))
.pipe( .pipe(
map(({ data }) => mapAuthApiResponseToAuthenticationResult(data)), map(({ data }) => mapAuthApiResponseToAuthenticationResult(data)),
tap(authenticationResult => { tap(authenticationResult => {
this._setSession(authenticationResult); this._setSession(authenticationResult);
}),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte logga in.\n\n${error.message}`,
name: `GET ${apiUrl}`,
method: 'AuthenticationService.login$',
});
}) })
); );
} }

View File

@@ -1,22 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import {
Authorization,
AuthorizationApiResponse,
mapAuthorizationApiResponseToAuthorization,
} from '@msfa-models/authorization.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class AuthorizationService {
private _apiBaseUrl = `${environment.api.url}/authorizations`;
public authorizations$: Observable<Authorization[]> = this.httpClient
.get<AuthorizationApiResponse>(this._apiBaseUrl)
.pipe(map(({ data }) => data.map(authorization => mapAuthorizationApiResponseToAuthorization(authorization))));
constructor(private httpClient: HttpClient) {}
}

View File

@@ -6,7 +6,7 @@ import { AvropAndMetaResponse } from '@msfa-models/api/avrop.response.model';
import { Params } from '@msfa-models/api/params.model'; import { Params } from '@msfa-models/api/params.model';
import { AvropFilter, mapResponseToAvropFilter } from '@msfa-models/avrop-filter.model'; import { AvropFilter, mapResponseToAvropFilter } from '@msfa-models/avrop-filter.model';
import { Avrop, AvropAndMeta, mapResponseToAvrop } from '@msfa-models/avrop.model'; import { Avrop, AvropAndMeta, mapResponseToAvrop } from '@msfa-models/avrop.model';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error'; import { CustomError } from '@msfa-models/error/custom-error';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, filter, map } from 'rxjs/operators'; import { catchError, filter, map } from 'rxjs/operators';
@@ -27,7 +27,7 @@ export class AvropApiService {
fetchAvrop$(params: Params): Observable<AvropAndMeta | null> { fetchAvrop$(params: Params): Observable<AvropAndMeta | null> {
return this.httpClient return this.httpClient
.get<AvropAndMetaResponse>(`${this._apiBaseUrl}`, { params }) .get<AvropAndMetaResponse>(this._apiBaseUrl, { params })
.pipe( .pipe(
map(({ data, meta }) => ({ data: data.map(avrop => mapResponseToAvrop(avrop)), meta })), map(({ data, meta }) => ({ data: data.map(avrop => mapResponseToAvrop(avrop)), meta })),
catchError((error: Error & { status: number }) => { catchError((error: Error & { status: number }) => {
@@ -35,48 +35,68 @@ export class AvropApiService {
this._showUnauthorizedError$.next(true); this._showUnauthorizedError$.next(true);
return of(null as null); return of(null as null);
} else { } else {
throw new CustomError( throw new CustomError({
errorToCustomError({ ...error, message: `Kunde inte hämta nya deltagare.\n\n${error.message}` }) error,
); message: `Kunde inte hämta nya deltagare.\n\n${error.message}`,
name: `GET ${this._apiBaseUrl}`,
method: 'AvropApiService.fetchAvrop$',
});
} }
}) })
); );
} }
fetchAvailableTjanster$(params: Params): Observable<AvropFilter[]> { fetchAvailableTjanster$(params: Params): Observable<AvropFilter[]> {
const apiUrl = `${this._apiBaseUrl}/tjanster`;
return this.httpClient return this.httpClient
.get<{ data: AvropFilterResponse[] }>(`${this._apiBaseUrl}/tjanster`, { params }) .get<{ data: AvropFilterResponse[] }>(apiUrl, { params })
.pipe( .pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => data.map(tjanster => mapResponseToAvropFilter(tjanster))) map(({ data }) => data.map(tjanster => mapResponseToAvropFilter(tjanster))),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta tjänster.\n\n${error.message}`,
name: `GET ${apiUrl}`,
method: 'AvropApiService.fetchAvailableTjanster$',
});
})
); );
} }
fetchAvailableUtforandeVerksamheter$(params: Params): Observable<AvropFilter[]> { fetchAvailableUtforandeVerksamheter$(params: Params): Observable<AvropFilter[]> {
const apiUrl = `${this._apiBaseUrl}/utforandeverksamheter`;
return this.httpClient return this.httpClient
.get<{ data: AvropFilterResponse[] }>(`${this._apiBaseUrl}/utforandeverksamheter`, { params }) .get<{ data: AvropFilterResponse[] }>(apiUrl, { params })
.pipe( .pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => data.map(utforandeverksamheter => mapResponseToAvropFilter(utforandeverksamheter))) map(({ data }) => data.map(utforandeverksamheter => mapResponseToAvropFilter(utforandeverksamheter))),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta utförande verksamheter.\n\n${error.message}`,
name: `GET ${apiUrl}`,
method: 'AvropApiService.fetchAvailableUtforandeVerksamheter$',
});
})
); );
} }
fetchAvailableKommuner$(params: Params): Observable<AvropFilter[]> { fetchAvailableKommuner$(params: Params): Observable<AvropFilter[]> {
const apiUrl = `${this._apiBaseUrl}/kommuner`;
return this.httpClient return this.httpClient
.get<{ data: AvropFilterResponse[] }>(`${this._apiBaseUrl}/kommuner`, { params }) .get<{ data: AvropFilterResponse[] }>(apiUrl, { params })
.pipe( .pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => data.map(kommun => mapResponseToAvropFilter(kommun))) map(({ data }) => data.map(kommun => mapResponseToAvropFilter(kommun))),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta kommuner.\n\n${error.message}`,
name: `GET ${apiUrl}`,
method: 'AvropApiService.fetchAvailableKommuner$',
});
})
); );
} }
async assignHandledare(avrop: Avrop[], handledareId: string): Promise<void> {
const params: Params = {
avropIds: avrop.map(deltagare => deltagare.id),
ciamUserId: handledareId,
};
return this.httpClient
.patch<void>(`${this._apiBaseUrl}/handledare/assign`, null, { params })
.toPromise();
}
} }

View File

@@ -1,6 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ErrorType } from '@msfa-enums/error-type.enum';
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
import { AvvikelseQuestionsResponse } from '@msfa-models/api/avvikelse-question.response.model'; import { AvvikelseQuestionsResponse } from '@msfa-models/api/avvikelse-question.response.model';
import { AvvikelseReasonResponse } from '@msfa-models/api/avvikelse-reason.response.model'; import { AvvikelseReasonResponse } from '@msfa-models/api/avvikelse-reason.response.model';
@@ -20,26 +19,47 @@ export class AvvikelseApiService {
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
public fetchAvvikelseReasons$(): Observable<AvvikelseReason[]> { public fetchAvvikelseReasons$(): Observable<AvvikelseReason[]> {
return this.httpClient.get<{ data: AvvikelseReasonResponse[] }>(`${this._apiBaseUrl}/orsakskoderavvikelse`).pipe( const apiUrl = `${this._apiBaseUrl}/orsakskoderavvikelse`;
return this.httpClient.get<{ data: AvvikelseReasonResponse[] }>(apiUrl).pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => data.map(avvikelse => mapResponseToAvvikelseReason(avvikelse))) map(({ data }) => data.map(avvikelse => mapResponseToAvvikelseReason(avvikelse))),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta orsaker till avvikelse.\n\n${error.message}`,
name: `GET ${apiUrl}`,
method: 'AvvikelseApiService.fetchAvvikelseReasons$',
});
})
); );
} }
public fetchAvvikelseQuestions$(): Observable<AvvikelseQuestion[]> { public fetchAvvikelseQuestions$(): Observable<AvvikelseQuestion[]> {
return this.httpClient.get<{ data: AvvikelseQuestionsResponse[] }>(`${this._apiBaseUrl}/fragorforavvikelser`).pipe( const apiUrl = `${this._apiBaseUrl}/fragorforavvikelser`;
return this.httpClient.get<{ data: AvvikelseQuestionsResponse[] }>(apiUrl).pipe(
filter(response => !!response?.data), filter(response => !!response?.data),
map(({ data }) => data.map(fraga => mapResponseToAvvikelseQuestion(fraga))) map(({ data }) => data.map(fraga => mapResponseToAvvikelseQuestion(fraga))),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta avvikelse frågor.\n\n${error.message}`,
name: `GET ${apiUrl}`,
method: 'AvvikelseApiService.fetchAvvikelseQuestions$',
});
})
); );
} }
public createAvvikelse$(avvikelse: AvvikelseReportRequest): Observable<unknown> { public createAvvikelse$(avvikelse: AvvikelseReportRequest): Observable<unknown> {
return this.httpClient.post<void>(`${this._apiBaseUrl}/avvikelse`, avvikelse).pipe( const apiUrl = `${this._apiBaseUrl}/avvikelse`;
return this.httpClient.post<void>(apiUrl, avvikelse).pipe(
catchError((error: Error) => { catchError((error: Error) => {
throw new CustomError({ throw new CustomError({
error, error,
message: `Kunde inte spara Avvikelserapport (avvikelse).\n\n${error.message}`, message: `Kunde inte spara Avvikelserapport (avvikelse).\n\n${error.message}`,
type: ErrorType.API, name: `POST ${apiUrl}`,
data: avvikelse,
method: 'AvvikelseApiService.createAvvikelse$',
}); });
}) })
); );

View File

@@ -7,8 +7,10 @@ import {
DeltagareHandelserApiResponse, DeltagareHandelserApiResponse,
mapDeltagareHandelseApiResponse, mapDeltagareHandelseApiResponse,
} from '@msfa-models/deltagare-handelse.model'; } from '@msfa-models/deltagare-handelse.model';
import { CustomError } from '@msfa-models/error/custom-error';
import { replaceGenomforandereferensFromUrl } from '@msfa-shared/utils/replace-genomforandereferens-from-url.util';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { catchError, filter, map } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -19,26 +21,31 @@ export class DeltagareHandelserApiService {
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
fetchDeltagareHandelser$( fetchDeltagareHandelser$(
genomforandeReferens: number, genomforandereferens: number,
handelserParams: PaginationParams handelserParams: PaginationParams
): Observable<DeltagareHandelseData> { ): Observable<DeltagareHandelseData> {
if (!genomforandeReferens) { if (!genomforandereferens) {
throw new Error('Genomförandereferens kunde inte hittas.'); throw new Error('Genomförandereferens kunde inte hittas.');
} }
const params: Params = { page: handelserParams.page.toString() }; const params: Params = { page: handelserParams.page.toString() };
const apiUrl = `${this._apiBaseUrl}/deltagare/${genomforandereferens}/handelser`;
return this.httpClient return this.httpClient
.get<DeltagareHandelserApiResponse>(`${this._apiBaseUrl}/deltagare/${genomforandeReferens}/handelser`, { params }) .get<DeltagareHandelserApiResponse>(apiUrl, { params })
.pipe( .pipe(
map(({ data, meta }) => { filter(({ data }) => !!data),
if (data) { map(({ data, meta }) => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data: data.map(genomforandeHandelse => mapDeltagareHandelseApiResponse(genomforandeHandelse)),
return { meta,
data: data.map(genomforandeHandelse => mapDeltagareHandelseApiResponse(genomforandeHandelse)), })),
meta, catchError((error: Error) => {
}; throw new CustomError({
} error,
message: `Kunde inte hämta händelser.\n\n${error.message}`,
name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
data: { genomforandereferens },
method: 'DeltagareHandelserApiService.fetchDeltagareHandelser$',
});
}) })
); );
} }

View File

@@ -19,10 +19,11 @@ import { DeltagareCompactData, mapResponseToDeltagareCompact } from '@msfa-model
import { Disability, mapResponseToDisability } from '@msfa-models/disability.model'; import { Disability, mapResponseToDisability } from '@msfa-models/disability.model';
import { DriversLicense, mapResponseToDriversLicense } from '@msfa-models/drivers-license.model'; import { DriversLicense, mapResponseToDriversLicense } from '@msfa-models/drivers-license.model';
import { Education, mapResponseToEducation } from '@msfa-models/education.model'; import { Education, mapResponseToEducation } from '@msfa-models/education.model';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error'; import { CustomError } from '@msfa-models/error/custom-error';
import { HighestEducation, mapResponseToHighestEducation } from '@msfa-models/highest-education.model'; import { HighestEducation, mapResponseToHighestEducation } from '@msfa-models/highest-education.model';
import { mapResponseToReport, ReportsData } from '@msfa-models/report.model'; import { mapResponseToReport, ReportsData } from '@msfa-models/report.model';
import { mapResponseToWorkExperience, WorkExperience } from '@msfa-models/work-experience.model'; import { mapResponseToWorkExperience, WorkExperience } from '@msfa-models/work-experience.model';
import { replaceGenomforandereferensFromUrl } from '@msfa-shared/utils/replace-genomforandereferens-from-url.util';
import { sortFromToDates } from '@msfa-utils/sort.util'; import { sortFromToDates } from '@msfa-utils/sort.util';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
@@ -69,18 +70,21 @@ export class DeltagareApiService {
this._showUnauthorizedError$.next(true); this._showUnauthorizedError$.next(true);
return of(null as null); return of(null as null);
} else { } else {
throw new CustomError( throw new CustomError({
errorToCustomError({ ...error, message: `Kunde inte hämta deltagare.\n\n${error.message}` }) error,
); message: `Kunde inte hämta deltagare.\n\n${error.message}`,
name: `GET ${this._apiBaseUrl}`,
method: 'DeltagareApiService.fetchAllDeltagare$',
});
} }
}) })
); );
} }
public fetchReports$(genomforandeReferens: number, paginationParams: PaginationParams): Observable<ReportsData> { public fetchReports$(genomforandereferens: number, paginationParams: PaginationParams): Observable<ReportsData> {
const { page, limit } = paginationParams; const { page, limit } = paginationParams;
const params: { [param: string]: string | string[] } = { const params: { [param: string]: string | string[] } = {
genomforandeReferens: genomforandeReferens.toString(), genomforandeReferens: genomforandereferens.toString(),
page: page.toString(), page: page.toString(),
limit: limit.toString(), limit: limit.toString(),
}; };
@@ -90,143 +94,170 @@ export class DeltagareApiService {
.pipe( .pipe(
map(({ data, meta }) => ({ data: data.map(report => mapResponseToReport(report)), meta })), map(({ data, meta }) => ({ data: data.map(report => mapResponseToReport(report)), meta })),
catchError((error: Error) => { catchError((error: Error) => {
throw new CustomError( throw new CustomError({
errorToCustomError({ ...error, message: `Kunde inte hämta rapporter.\n\n${error.message}` }) error,
); message: `Kunde inte hämta rapporter.\n\n${error.message}`,
name: `GET ${this._apiReportUrl}`,
method: 'DeltagareApiService.fetchReports$',
});
}) })
); );
} }
public fetchContactInformation$(genomforandeReferens: number): Observable<ContactInformation> { public fetchContactInformation$(genomforandereferens: number): Observable<ContactInformation> {
return this.httpClient const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/contact`;
.get<{ data: ContactInformationResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/contact`) return this.httpClient.get<{ data: ContactInformationResponse }>(apiUrl).pipe(
.pipe( map(({ data }) => mapResponseToContactInformation(data)),
map(({ data }) => mapResponseToContactInformation(data)), catchError((error: Error) => {
catchError((error: Error) => { throw new CustomError({
throw new CustomError( error,
errorToCustomError({ ...error, message: `Kunde inte hämta kontaktinformation.\n\n${error.message}` }) message: `Kunde inte hämta kontaktinformation.\n\n${error.message}`,
); name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
}) data: { genomforandereferens },
); method: 'DeltagareApiService.fetchContactInformation$',
});
})
);
} }
public fetchDriversLicense$(genomforandeReferens: number): Observable<DriversLicense> { public fetchDriversLicense$(genomforandereferens: number): Observable<DriversLicense> {
return this.httpClient const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/driverlicense`;
.get<{ data: DriversLicenseResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/driverlicense`) return this.httpClient.get<{ data: DriversLicenseResponse }>(apiUrl).pipe(
.pipe( map(({ data }) => mapResponseToDriversLicense(data)),
map(({ data }) => mapResponseToDriversLicense(data)), catchError((error: Error) => {
catchError((error: Error) => { throw new CustomError({
throw new CustomError( error,
errorToCustomError({ ...error, message: `Kunde inte hämta körkortsinformation.\n\n${error.message}` }) message: `Kunde inte hämta körkortsinformation.\n\n${error.message}`,
); name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
}) data: { genomforandereferens },
); method: 'DeltagareApiService.fetchDriversLicense$',
});
})
);
} }
public fetchHighestEducation$(genomforandeReferens: number): Observable<HighestEducation> { public fetchHighestEducation$(genomforandereferens: number): Observable<HighestEducation> {
return this.httpClient const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/educationlevels/highest`;
.get<{ data: HighestEducationResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/educationlevels/highest`) return this.httpClient.get<{ data: HighestEducationResponse }>(apiUrl).pipe(
.pipe( map(({ data }) => mapResponseToHighestEducation(data)),
map(({ data }) => mapResponseToHighestEducation(data)), catchError((error: Error) => {
catchError((error: Error) => { throw new CustomError({
throw new CustomError( error,
errorToCustomError({ ...error, message: `Kunde inte hämta högsta utbildning.\n\n${error.message}` }) message: `Kunde inte hämta högsta utbildning.\n\n${error.message}`,
); name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
}) data: { genomforandereferens },
); method: 'DeltagareApiService.fetchHighestEducation$',
});
})
);
} }
public fetchEducations$(genomforandeReferens: number): Observable<Education[]> { public fetchEducations$(genomforandereferens: number): Observable<Education[]> {
return this.httpClient const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/educations`;
.get<{ data: EducationsResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/educations`) return this.httpClient.get<{ data: EducationsResponse }>(apiUrl).pipe(
.pipe( map(({ data }) =>
map(({ data }) => data.utbildningar
data.utbildningar ? data.utbildningar.sort((a, b) =>
? data.utbildningar.sort((a, b) =>
sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom })
)
: []
),
map(educations => educations.map(utbildning => mapResponseToEducation(utbildning))),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta utbildningar.\n\n${error.message}` })
);
})
);
}
public fetchTranslator$(genomforandeReferens: number): Observable<string> {
return this.httpClient
.get<{ data: TranslatorResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/translator`)
.pipe(
map(({ data }) => data.sprak?.beskrivning || null),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta tolkinformation.\n\n${error.message}` })
);
})
);
}
public fetchWorkLanguages$(genomforandeReferens: number): Observable<string[]> {
return this.httpClient
.get<{ data: WorkLanguagesResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/work/languages`)
.pipe(
map(({ data }) => data?.sprak?.map(sprak => sprak.beskrivning) || []),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({
...error,
message: `Kunde inte hämta språk som kan användas på jobbet.\n\n${error.message}`,
})
);
})
);
}
public fetchDisabilities$(genomforandeReferens: number): Observable<Disability[]> {
return this.httpClient
.get<{ data: DisabilityResponse[] }>(`${this._apiBaseUrl}/${genomforandeReferens}/work/disabilities`)
.pipe(
map(({ data }) => data?.map(funktionsnedsattning => mapResponseToDisability(funktionsnedsattning)) || []),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta funktionsnedsättningar.\n\n${error.message}` })
);
})
);
}
public fetchWorkExperiences$(genomforandeReferens: number): Observable<WorkExperience[]> {
return this.httpClient
.get<{ data: WorkExperiencesResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/work/experiences`)
.pipe(
map(
({ data }) =>
data?.arbetslivserfarenheter?.sort((a, b) =>
sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom }) sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom })
) || [] )
), : []
map(workExperiences => workExperiences.map(erfarenhet => mapResponseToWorkExperience(erfarenhet))), ),
catchError((error: Error) => { map(educations => educations.map(utbildning => mapResponseToEducation(utbildning))),
throw new CustomError( catchError((error: Error) => {
errorToCustomError({ ...error, message: `Kunde inte hämta arbetslivserfarenheter.\n\n${error.message}` }) throw new CustomError({
); error,
}) message: `Kunde inte hämta utbildningar.\n\n${error.message}`,
); name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
data: { genomforandereferens },
method: 'DeltagareApiService.fetchEducations$',
});
})
);
} }
public fetchAvropInformation$(genomforandeReferens: number): Observable<DeltagareAvrop> { public fetchTranslator$(genomforandereferens: number): Observable<string> {
return this.httpClient const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/translator`;
.get<{ data: DeltagareAvropResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/avrop`) return this.httpClient.get<{ data: TranslatorResponse }>(apiUrl).pipe(
.pipe( map(({ data }) => data.sprak?.beskrivning || null),
map(({ data }) => mapResponseToDeltagareAvrop(data)), catchError((error: Error) => {
catchError((error: Error) => { throw new CustomError({
throw new CustomError( error,
errorToCustomError({ ...error, message: `Kunde inte hämta avropsinformation.\n\n${error.message}` }) message: `Kunde inte hämta tolkinformation.\n\n${error.message}`,
); name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
}) data: { genomforandereferens },
); method: 'DeltagareApiService.fetchTranslator$',
});
})
);
}
public fetchWorkLanguages$(genomforandereferens: number): Observable<string[]> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/work/languages`;
return this.httpClient.get<{ data: WorkLanguagesResponse }>(apiUrl).pipe(
map(({ data }) => data?.sprak?.map(sprak => sprak.beskrivning) || []),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta språk som kan användas på jobbet.\n\n${error.message}`,
name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
data: { genomforandereferens },
method: 'DeltagareApiService.fetchWorkLanguages$',
});
})
);
}
public fetchDisabilities$(genomforandereferens: number): Observable<Disability[]> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/work/disabilities`;
return this.httpClient.get<{ data: DisabilityResponse[] }>(apiUrl).pipe(
map(({ data }) => data?.map(funktionsnedsattning => mapResponseToDisability(funktionsnedsattning)) || []),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta funktionsnedsättningar.\n\n${error.message}`,
name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
data: { genomforandereferens },
method: 'DeltagareApiService.fetchDisabilities$',
});
})
);
}
public fetchWorkExperiences$(genomforandereferens: number): Observable<WorkExperience[]> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/work/experiences`;
return this.httpClient.get<{ data: WorkExperiencesResponse }>(apiUrl).pipe(
map(
({ data }) =>
data?.arbetslivserfarenheter?.sort((a, b) =>
sortFromToDates({ from: a.period_from, to: a.period_tom }, { from: b.period_from, to: b.period_tom })
) || []
),
map(workExperiences => workExperiences.map(erfarenhet => mapResponseToWorkExperience(erfarenhet))),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta arbetslivserfarenheter.\n\n${error.message}`,
name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
data: { genomforandereferens },
method: 'DeltagareApiService.fetchWorkExperiences$',
});
})
);
}
public fetchAvropInformation$(genomforandereferens: number): Observable<DeltagareAvrop> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/avrop`;
return this.httpClient.get<{ data: DeltagareAvropResponse }>(apiUrl).pipe(
map(({ data }) => mapResponseToDeltagareAvrop(data)),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta avropsinformation.\n\n${error.message}`,
name: `GET ${replaceGenomforandereferensFromUrl(apiUrl)}`,
data: { genomforandereferens },
method: 'DeltagareApiService.fetchAvropInformation$',
});
})
);
} }
} }

View File

@@ -17,10 +17,10 @@ import {
mapResponseToEmployee, mapResponseToEmployee,
mapResponseToEmployeeCompact, mapResponseToEmployeeCompact,
} from '@msfa-models/employee.model'; } from '@msfa-models/employee.model';
import { errorToCustomError } from '@msfa-models/error/custom-error'; import { CustomError } from '@msfa-models/error/custom-error';
import { Sort } from '@msfa-models/sort.model'; import { Sort } from '@msfa-models/sort.model';
import { ErrorService } from '@msfa-services/error.service'; import { ErrorService } from '@msfa-services/error.service';
import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { catchError, distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
const DEFAULT_PARAMS: EmployeeParams = { const DEFAULT_PARAMS: EmployeeParams = {
@@ -119,16 +119,30 @@ export class EmployeeService extends UnsubscribeDirective {
map(({ data, meta }) => { map(({ data, meta }) => {
this._employeesLoading$.next(false); this._employeesLoading$.next(false);
return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta }; return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta };
}),
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte hämta personal.\n\n${error.message}`,
name: `GET ${this._apiBaseUrl}`,
method: 'EmployeeService._fetchEmployees$',
});
}) })
); );
} }
private _fetchEmployee$(id: string): Observable<Employee | Partial<Employee>> { private _fetchEmployee$(ciamUserId: string): Observable<Employee> {
return this.httpClient.get<{ data: EmployeeResponse }>(`${this._apiBaseUrl}/${id}`).pipe( const apiUrl = `${this._apiBaseUrl}/${ciamUserId}`;
return this.httpClient.get<{ data: EmployeeResponse }>(apiUrl).pipe(
map(({ data }) => mapResponseToEmployee(data)), map(({ data }) => mapResponseToEmployee(data)),
catchError(error => { catchError((error: Error) => {
this.errorService.add(errorToCustomError(error)); throw new CustomError({
return of({}); error,
message: `Kunde inte hämta personal.\n\n${error.message}`,
name: `GET ${this._apiBaseUrl}/{ciamUserId}`,
data: { ciamUserId },
method: 'EmployeeService._fetchEmployee$',
});
}) })
); );
} }
@@ -153,13 +167,20 @@ export class EmployeeService extends UnsubscribeDirective {
this._employeeToDelete$.next(employee); this._employeeToDelete$.next(employee);
} }
public deleteEmployee(employee: Employee): Observable<Employee | Partial<Employee>> { public deleteEmployee(employee: Employee): Observable<Employee> {
return this.httpClient.delete<void>(`${this._apiBaseUrl}/${employee.id}`).pipe( return this.httpClient.delete<void>(`${this._apiBaseUrl}/${employee.id}`).pipe(
tap(() => { tap(() => {
this._lastDeletedEmployee$.next(employee); this._lastDeletedEmployee$.next(employee);
}), }),
map(() => employee), map(() => employee),
catchError(error => throwError(errorToCustomError(error))) catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte ta bort personal.\n\n${error.message}`,
name: `DELETE ${this._apiBaseUrl}/{ciamUserId}`,
method: 'EmployeeService.deleteEmployee',
});
})
); );
} }
@@ -175,28 +196,40 @@ export class EmployeeService extends UnsubscribeDirective {
} }
public postEmployeeInvitation(emails: string[]): Observable<EmployeeInviteResponse | null> { public postEmployeeInvitation(emails: string[]): Observable<EmployeeInviteResponse | null> {
const apiUrl = `${this._apiBaseUrl}/invite`;
return this.httpClient return this.httpClient
.patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, { emails }) .patch<{ data: EmployeeInviteResponse }>(apiUrl, { emails })
.pipe( .pipe(
take(1), take(1),
map(({ data }) => data), map(({ data }) => data),
catchError(error => { catchError((error: Error) => {
this.errorService.add(errorToCustomError(error)); throw new CustomError({
return throwError(errorToCustomError(error)); error,
message: `Kunde inte bjuda in personal.\n\n${error.message}`,
name: `PATCH ${apiUrl}`,
data: { emails },
method: 'EmployeeService.postEmployeeInvitation',
});
}) })
); );
} }
public updateEmployee$(id: string, data: EmployeeEditRequest): Observable<boolean> { public updateEmployee$(ciamUserId: string, data: EmployeeEditRequest): Observable<boolean> {
return this.httpClient.put<boolean>(`${this._apiBaseUrl}/${id}`, data).pipe( return this.httpClient.put<boolean>(`${this._apiBaseUrl}/${ciamUserId}`, data).pipe(
take(1), take(1),
tap(() => { tap(() => {
this._employee$.next(null); this._employee$.next(null);
this._lastUpdatedEmployeeId$.next(id); this._lastUpdatedEmployeeId$.next(ciamUserId);
}), }),
map(() => true), map(() => true),
catchError(error => { catchError((error: Error) => {
return throwError(errorToCustomError(error)); throw new CustomError({
error,
message: `Kunde inte redigera personal.\n\n${error.message}`,
name: `PATCH ${this._apiBaseUrl}/{ciamUserId}`,
data: { ciamUserId },
method: 'EmployeeService.updateEmployee$',
});
}) })
); );
} }

View File

@@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ApmService } from '@elastic/apm-rum-angular';
import { SELECTED_ORGANIZATION_NUMBER_KEY } from '@msfa-constants/local-storage-keys'; import { SELECTED_ORGANIZATION_NUMBER_KEY } from '@msfa-constants/local-storage-keys';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive'; import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { environment } from '@msfa-environment'; import { environment } from '@msfa-environment';
@@ -38,7 +39,11 @@ export class UserService extends UnsubscribeDirective {
return this._userRoles$.getValue(); return this._userRoles$.getValue();
} }
constructor(private httpClient: HttpClient, private authenticationService: AuthenticationService) { constructor(
private httpClient: HttpClient,
private authenticationService: AuthenticationService,
private apmService: ApmService
) {
super(); super();
this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber); this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);
super.unsubscribeOnDestroy( super.unsubscribeOnDestroy(
@@ -64,6 +69,8 @@ export class UserService extends UnsubscribeDirective {
this._user$.next(currentUser); this._user$.next(currentUser);
this._userLoading$.next(false); this._userLoading$.next(false);
this._userRolesLoading$.next(false); this._userRolesLoading$.next(false);
this.apmService.apm.setUserContext({ id: currentUser.id });
}) })
); );
} }

View File

@@ -3,10 +3,10 @@ import { AvropParams, Params } from '@msfa-models/api/params.model';
import { Avrop, AvropAndMeta } from '@msfa-models/avrop.model'; import { Avrop, AvropAndMeta } from '@msfa-models/avrop.model';
import { Handledare } from '@msfa-models/handledare.model'; import { Handledare } from '@msfa-models/handledare.model';
import { AvropApiService } from '@msfa-services/api/avrop-api.service'; import { AvropApiService } from '@msfa-services/api/avrop-api.service';
import { MultiselectFilterOption } from '@ui/multiselect/multiselect-filter-option';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, switchMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { HandledareApiService } from './api/handledare.api.service'; import { HandledareApiService } from './api/handledare.api.service';
import { MultiselectFilterOption } from '@ui/multiselect/multiselect-filter-option';
type Step = 1 | 2 | 3 | 4; type Step = 1 | 2 | 3 | 4;
@@ -249,7 +249,12 @@ export class AvropService {
return; return;
} }
await this.avropApiService.assignHandledare(this._selectedAvrop$.value, this._selectedHandledareId$.value); const avrop: Avrop[] = this._selectedAvrop$.getValue();
await this.handledareApiService.assignHandledare(
avrop.map(deltagare => deltagare.id),
{ ciamUserId: this._selectedHandledareId$.value } as Handledare
);
this._avropIsSubmitted$.next(true); this._avropIsSubmitted$.next(true);
} }

View File

@@ -0,0 +1,4 @@
export function replaceGenomforandereferensFromUrl(url: string): string {
const regex = /\/\d*\//;
return url.replace(regex, '/{genomforandereferens}/');
}