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,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ 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

View File

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

View File

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

View File

@@ -62,4 +62,10 @@
padding: var(--digi--layout--gutter--s);
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 { ErrorSeverity } from '@msfa-enums/error-severity.enum';
import { UiIconType } from '@ui/icon/icon-type.enum';
import { CustomError } from '@msfa-models/error/custom-error';
import { UiIconSize } from '@ui/icon/icon-size.enum';
import { UiIconType } from '@ui/icon/icon-type.enum';
@Component({
selector: 'msfa-toast',
@@ -19,11 +19,11 @@ export class ToastComponent implements AfterViewInit {
ErrorSeverity = ErrorSeverity;
ngAfterViewInit(): void {
if (this.error.removeAfter) {
setTimeout(() => {
this.closeToast.emit(this.error);
}, this.error.removeAfter);
}
// if (this.error.removeAfter) {
// setTimeout(() => {
// this.closeToast.emit(this.error);
// }, this.error.removeAfter);
// }
}
get className(): string {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,27 @@
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 { environment } from '@msfa-environment';
import { CustomError } from '@msfa-models/error/custom-error';
import { ErrorService } from '@msfa-services/error.service';
interface ApmError {
id: string;
name: string;
message: string;
type: string;
method: string;
timestamp: Date;
}
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
private _elasticConfig = environment.elastic;
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 {
if (!customError.avoidToast) {
@@ -17,7 +29,11 @@ export class CustomErrorHandler implements ErrorHandler {
}
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 { ErrorType } from '@msfa-enums/error-type.enum';
@@ -7,15 +8,20 @@ export class CustomError implements Error {
message: string;
stack: string;
type: ErrorType;
data: string;
method: string;
severity: ErrorSeverity;
timestamp: Date;
error: Error;
error: Error | HttpErrorResponse;
removeAfter: number;
avoidToast?: boolean;
constructor(args: {
error: Error;
error: Error | HttpErrorResponse;
type?: ErrorType;
name?: string;
data?: unknown;
method?: string;
message?: string;
severity?: ErrorSeverity;
stack?: string;
@@ -23,7 +29,10 @@ export class CustomError implements Error {
}) {
this.timestamp = new Date();
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.severity = args.severity || ErrorSeverity.HIGH;
this.stack = args.stack || CustomError.getStack(args.error);

View File

@@ -11,6 +11,7 @@ import {
import { environment } from '@msfa-environment';
import { AuthenticationResponse } from '@msfa-models/api/authentication.response.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 { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
@@ -103,12 +104,22 @@ export class AuthenticationService {
login$(authorizationCodeFromCiam: string): Observable<Authentication> {
this.removeLocalStorageData();
const apiUrl = AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam);
return this.httpClient
.get<{ data: AuthenticationResponse }>(AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam))
.pipe(
map(({ data }) => mapAuthApiResponseToAuthenticationResult(data)),
tap(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 { AvropFilter, mapResponseToAvropFilter } from '@msfa-models/avrop-filter.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 { catchError, filter, map } from 'rxjs/operators';
@@ -27,7 +27,7 @@ export class AvropApiService {
fetchAvrop$(params: Params): Observable<AvropAndMeta | null> {
return this.httpClient
.get<AvropAndMetaResponse>(`${this._apiBaseUrl}`, { params })
.get<AvropAndMetaResponse>(this._apiBaseUrl, { params })
.pipe(
map(({ data, meta }) => ({ data: data.map(avrop => mapResponseToAvrop(avrop)), meta })),
catchError((error: Error & { status: number }) => {
@@ -35,48 +35,68 @@ export class AvropApiService {
this._showUnauthorizedError$.next(true);
return of(null as null);
} else {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta nya deltagare.\n\n${error.message}` })
);
throw new CustomError({
error,
message: `Kunde inte hämta nya deltagare.\n\n${error.message}`,
name: `GET ${this._apiBaseUrl}`,
method: 'AvropApiService.fetchAvrop$',
});
}
})
);
}
fetchAvailableTjanster$(params: Params): Observable<AvropFilter[]> {
const apiUrl = `${this._apiBaseUrl}/tjanster`;
return this.httpClient
.get<{ data: AvropFilterResponse[] }>(`${this._apiBaseUrl}/tjanster`, { params })
.get<{ data: AvropFilterResponse[] }>(apiUrl, { params })
.pipe(
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[]> {
const apiUrl = `${this._apiBaseUrl}/utforandeverksamheter`;
return this.httpClient
.get<{ data: AvropFilterResponse[] }>(`${this._apiBaseUrl}/utforandeverksamheter`, { params })
.get<{ data: AvropFilterResponse[] }>(apiUrl, { params })
.pipe(
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[]> {
const apiUrl = `${this._apiBaseUrl}/kommuner`;
return this.httpClient
.get<{ data: AvropFilterResponse[] }>(`${this._apiBaseUrl}/kommuner`, { params })
.get<{ data: AvropFilterResponse[] }>(apiUrl, { params })
.pipe(
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 { Injectable } from '@angular/core';
import { ErrorType } from '@msfa-enums/error-type.enum';
import { environment } from '@msfa-environment';
import { AvvikelseQuestionsResponse } from '@msfa-models/api/avvikelse-question.response.model';
import { AvvikelseReasonResponse } from '@msfa-models/api/avvikelse-reason.response.model';
@@ -20,26 +19,47 @@ export class AvvikelseApiService {
constructor(private httpClient: HttpClient) {}
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),
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[]> {
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),
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> {
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) => {
throw new CustomError({
error,
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,
mapDeltagareHandelseApiResponse,
} 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 { map } from 'rxjs/operators';
import { catchError, filter, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
@@ -19,26 +21,31 @@ export class DeltagareHandelserApiService {
constructor(private httpClient: HttpClient) {}
fetchDeltagareHandelser$(
genomforandeReferens: number,
genomforandereferens: number,
handelserParams: PaginationParams
): Observable<DeltagareHandelseData> {
if (!genomforandeReferens) {
if (!genomforandereferens) {
throw new Error('Genomförandereferens kunde inte hittas.');
}
const params: Params = { page: handelserParams.page.toString() };
const apiUrl = `${this._apiBaseUrl}/deltagare/${genomforandereferens}/handelser`;
return this.httpClient
.get<DeltagareHandelserApiResponse>(`${this._apiBaseUrl}/deltagare/${genomforandeReferens}/handelser`, { params })
.get<DeltagareHandelserApiResponse>(apiUrl, { params })
.pipe(
map(({ data, meta }) => {
if (data) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
return {
data: data.map(genomforandeHandelse => mapDeltagareHandelseApiResponse(genomforandeHandelse)),
meta,
};
}
filter(({ data }) => !!data),
map(({ data, 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 { DriversLicense, mapResponseToDriversLicense } from '@msfa-models/drivers-license.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 { mapResponseToReport, ReportsData } from '@msfa-models/report.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 { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@@ -69,18 +70,21 @@ export class DeltagareApiService {
this._showUnauthorizedError$.next(true);
return of(null as null);
} else {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta deltagare.\n\n${error.message}` })
);
throw new CustomError({
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 params: { [param: string]: string | string[] } = {
genomforandeReferens: genomforandeReferens.toString(),
genomforandeReferens: genomforandereferens.toString(),
page: page.toString(),
limit: limit.toString(),
};
@@ -90,143 +94,170 @@ export class DeltagareApiService {
.pipe(
map(({ data, meta }) => ({ data: data.map(report => mapResponseToReport(report)), meta })),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta rapporter.\n\n${error.message}` })
);
throw new CustomError({
error,
message: `Kunde inte hämta rapporter.\n\n${error.message}`,
name: `GET ${this._apiReportUrl}`,
method: 'DeltagareApiService.fetchReports$',
});
})
);
}
public fetchContactInformation$(genomforandeReferens: number): Observable<ContactInformation> {
return this.httpClient
.get<{ data: ContactInformationResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/contact`)
.pipe(
map(({ data }) => mapResponseToContactInformation(data)),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta kontaktinformation.\n\n${error.message}` })
);
})
);
public fetchContactInformation$(genomforandereferens: number): Observable<ContactInformation> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/contact`;
return this.httpClient.get<{ data: ContactInformationResponse }>(apiUrl).pipe(
map(({ data }) => mapResponseToContactInformation(data)),
catchError((error: Error) => {
throw new CustomError({
error,
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> {
return this.httpClient
.get<{ data: DriversLicenseResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/driverlicense`)
.pipe(
map(({ data }) => mapResponseToDriversLicense(data)),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta körkortsinformation.\n\n${error.message}` })
);
})
);
public fetchDriversLicense$(genomforandereferens: number): Observable<DriversLicense> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/driverlicense`;
return this.httpClient.get<{ data: DriversLicenseResponse }>(apiUrl).pipe(
map(({ data }) => mapResponseToDriversLicense(data)),
catchError((error: Error) => {
throw new CustomError({
error,
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> {
return this.httpClient
.get<{ data: HighestEducationResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/educationlevels/highest`)
.pipe(
map(({ data }) => mapResponseToHighestEducation(data)),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta högsta utbildning.\n\n${error.message}` })
);
})
);
public fetchHighestEducation$(genomforandereferens: number): Observable<HighestEducation> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/educationlevels/highest`;
return this.httpClient.get<{ data: HighestEducationResponse }>(apiUrl).pipe(
map(({ data }) => mapResponseToHighestEducation(data)),
catchError((error: Error) => {
throw new CustomError({
error,
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[]> {
return this.httpClient
.get<{ data: EducationsResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/educations`)
.pipe(
map(({ data }) =>
data.utbildningar
? 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) =>
public fetchEducations$(genomforandereferens: number): Observable<Education[]> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/educations`;
return this.httpClient.get<{ data: EducationsResponse }>(apiUrl).pipe(
map(({ data }) =>
data.utbildningar
? data.utbildningar.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(
errorToCustomError({ ...error, message: `Kunde inte hämta arbetslivserfarenheter.\n\n${error.message}` })
);
})
);
)
: []
),
map(educations => educations.map(utbildning => mapResponseToEducation(utbildning))),
catchError((error: Error) => {
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> {
return this.httpClient
.get<{ data: DeltagareAvropResponse }>(`${this._apiBaseUrl}/${genomforandeReferens}/avrop`)
.pipe(
map(({ data }) => mapResponseToDeltagareAvrop(data)),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta avropsinformation.\n\n${error.message}` })
);
})
);
public fetchTranslator$(genomforandereferens: number): Observable<string> {
const apiUrl = `${this._apiBaseUrl}/${genomforandereferens}/translator`;
return this.httpClient.get<{ data: TranslatorResponse }>(apiUrl).pipe(
map(({ data }) => data.sprak?.beskrivning || null),
catchError((error: Error) => {
throw new CustomError({
error,
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,
mapResponseToEmployeeCompact,
} 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 { 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';
const DEFAULT_PARAMS: EmployeeParams = {
@@ -119,16 +119,30 @@ export class EmployeeService extends UnsubscribeDirective {
map(({ data, meta }) => {
this._employeesLoading$.next(false);
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>> {
return this.httpClient.get<{ data: EmployeeResponse }>(`${this._apiBaseUrl}/${id}`).pipe(
private _fetchEmployee$(ciamUserId: string): Observable<Employee> {
const apiUrl = `${this._apiBaseUrl}/${ciamUserId}`;
return this.httpClient.get<{ data: EmployeeResponse }>(apiUrl).pipe(
map(({ data }) => mapResponseToEmployee(data)),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
catchError((error: Error) => {
throw new CustomError({
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);
}
public deleteEmployee(employee: Employee): Observable<Employee | Partial<Employee>> {
public deleteEmployee(employee: Employee): Observable<Employee> {
return this.httpClient.delete<void>(`${this._apiBaseUrl}/${employee.id}`).pipe(
tap(() => {
this._lastDeletedEmployee$.next(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> {
const apiUrl = `${this._apiBaseUrl}/invite`;
return this.httpClient
.patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, { emails })
.patch<{ data: EmployeeInviteResponse }>(apiUrl, { emails })
.pipe(
take(1),
map(({ data }) => data),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return throwError(errorToCustomError(error));
catchError((error: Error) => {
throw new CustomError({
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> {
return this.httpClient.put<boolean>(`${this._apiBaseUrl}/${id}`, data).pipe(
public updateEmployee$(ciamUserId: string, data: EmployeeEditRequest): Observable<boolean> {
return this.httpClient.put<boolean>(`${this._apiBaseUrl}/${ciamUserId}`, data).pipe(
take(1),
tap(() => {
this._employee$.next(null);
this._lastUpdatedEmployeeId$.next(id);
this._lastUpdatedEmployeeId$.next(ciamUserId);
}),
map(() => true),
catchError(error => {
return throwError(errorToCustomError(error));
catchError((error: 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 { Injectable } from '@angular/core';
import { ApmService } from '@elastic/apm-rum-angular';
import { SELECTED_ORGANIZATION_NUMBER_KEY } from '@msfa-constants/local-storage-keys';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { environment } from '@msfa-environment';
@@ -38,7 +39,11 @@ export class UserService extends UnsubscribeDirective {
return this._userRoles$.getValue();
}
constructor(private httpClient: HttpClient, private authenticationService: AuthenticationService) {
constructor(
private httpClient: HttpClient,
private authenticationService: AuthenticationService,
private apmService: ApmService
) {
super();
this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);
super.unsubscribeOnDestroy(
@@ -64,6 +69,8 @@ export class UserService extends UnsubscribeDirective {
this._user$.next(currentUser);
this._userLoading$.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 { Handledare } from '@msfa-models/handledare.model';
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 { distinctUntilChanged, filter, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { HandledareApiService } from './api/handledare.api.service';
import { MultiselectFilterOption } from '@ui/multiselect/multiselect-filter-option';
type Step = 1 | 2 | 3 | 4;
@@ -249,7 +249,12 @@ export class AvropService {
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);
}

View File

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