feat(deltagare): Implemented role-check and fetching data when needed. (TV-639)

Squashed commit of the following:

commit be46ec00569f3fa23a439d4fc40bfa8dd2f30ea7
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Sep 24 10:18:33 2021 +0200

    Fixed error-handling for deltagare

commit e18fe76f68f3894198887bf7fe8793dd34905674
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Sep 24 08:35:40 2021 +0200

    Updated tests

commit c8fa577236c1e3a797046d884d91e12e2c1f2c4a
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Sep 24 08:20:18 2021 +0200

    Fixed styling and some functionality

commit bfdcaef5c01edbee584ec0a1c1704983578ff6e5
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Thu Sep 23 16:00:10 2021 +0200

    refactor

commit 5be380af3aaca3c158dcfb1d084e449558f4e720
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Thu Sep 23 15:59:41 2021 +0200

    Update deltagare-tab-reports.component.ts

commit 96c4e36f0ce1a1f607e67ec3a99f18a85221f1d3
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Thu Sep 23 15:34:27 2021 +0200

    break up into several components. remove activeTab-observable etc

commit ce2145f09438240d786e58f60e286e6e6f8e7a29
Merge: afc3989 14739fb
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Thu Sep 23 14:05:05 2021 +0200

    Merged develop and resolved conflicts

commit afc39892ea33d2e1add92b2d5cb0d9f23b963666
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Thu Sep 23 13:39:01 2021 +0200

    Added handledare information to avrop-data and removed id from deltagare-card

commit 1f7454a3cb4af09d3fdcb60c1298677d01a5a64a
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Thu Sep 23 12:59:47 2021 +0200

    Implemented more logic inside component instead of service

commit 5af4a9a9f74707169892ce9fe02f7c93285f48cc
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Thu Sep 23 10:50:14 2021 +0200

    Added part of role-check to be able to access deltagare-card

commit 7cf7c1d379583788e5fcbef5fff44b158d028f76
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 22 15:07:40 2021 +0200

    WIP

commit 8466394d617fa573663f3d199414354394d22b31
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 22 11:42:32 2021 +0200

    Moved around content for deltagare-card
This commit is contained in:
Erik Tiekstra
2021-09-24 10:45:51 +02:00
parent 9bedbd37f8
commit 62fb35ca7e
44 changed files with 1097 additions and 991 deletions

View File

@@ -31,7 +31,9 @@ export class SidebarComponent {
get deltagareVisible(): boolean {
return (
this.activeFeatures.includes(Feature.DELTAGARE) &&
this.userRoles?.some(role => role.type === RoleEnum.MSFA_ReportAndPlanning)
this.userRoles?.some(
role => role.type === RoleEnum.MSFA_ReportAndPlanning || role.type === RoleEnum.MSFA_ReceiveDeltagare
)
);
}
}

View File

@@ -1,6 +1,5 @@
import { NavigationBreadcrumbsItem } from '@af/digi-ng/_navigation/navigation-breadcrumbs';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { Employee } from '@msfa-models/employee.model';

View File

@@ -1,4 +1,5 @@
@import 'mixins/backdrop';
@import 'variables/gutters';
@import 'variables/z-index';
@keyframes spinning {
@@ -12,6 +13,10 @@
align-items: center;
justify-content: center;
&--padded {
padding: $digi--layout--gutter--l;
}
&--absolute {
@include msfa__backdrop($msfa__z-index-backdrop, false);
}

View File

@@ -8,4 +8,5 @@ export enum Feature {
MOCK_LOGIN,
VERSION_INFO,
ACCESSIBILITY_REPORT,
REPORTING,
}

View File

@@ -1,4 +1,5 @@
export enum LoaderType {
FULL_SCREEN = 'fullscreen',
ABSOLUTE = 'absolute',
PADDED = 'padded',
}

View File

@@ -12,12 +12,12 @@ export class RoleGuard implements CanActivate {
constructor(private router: Router, private userService: UserService) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const expectedRole: RoleEnum = route.data.expectedRole as RoleEnum;
const expectedRoles: RoleEnum[] = route.data.expectedRoles as RoleEnum[];
return this.userService.userRoles$.pipe(
filter(roles => !!roles),
map(roles => {
const userHasRole = roles.some(role => role.type === expectedRole);
const userHasRole = roles.some(role => expectedRoles.includes(role.type));
if (userHasRole) {
return true;

View File

@@ -27,7 +27,8 @@ export interface AvropResponse {
sprakstod: string;
sparkod: string;
sparNamn: string;
supervisorId: number;
handledareCiamUserId: string;
handledare: string;
recievedTimestamp: string;
}

View File

@@ -19,6 +19,8 @@ export interface Avrop extends AvropCompact {
genomforandeReferens: number; // genomforandeReferens
participationFrequency: number; // deltagandeGrad
utforandeVerksamhet: string; // utforandeverksamhet
handledareCiamUserId: string;
handledare: string;
}
export interface AvropCompactData {
@@ -42,6 +44,8 @@ export function mapAvropResponseToAvrop(data: AvropResponse): Avrop {
genomforandeReferens,
deltagandeGrad,
utforandeverksamhet,
handledareCiamUserId,
handledare,
} = data;
return {
@@ -59,5 +63,7 @@ export function mapAvropResponseToAvrop(data: AvropResponse): Avrop {
genomforandeReferens,
participationFrequency: deltagandeGrad,
utforandeVerksamhet: utforandeverksamhet,
handledareCiamUserId,
handledare,
};
}

View File

@@ -52,7 +52,7 @@ export class CustomError implements Error {
}
}
export function errorToCustomError(error: Error & { ngDebugContext: unknown }): CustomError {
export function errorToCustomError(error: Error & { ngDebugContext?: unknown }): CustomError {
const type = CustomError.getErrorType(error);
const message = error.message || error;
const severity = ErrorSeverity.HIGH;

View File

@@ -1,267 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { SortOrder } from '@msfa-enums/sort-order.enum';
import { environment } from '@msfa-environment';
import { AvropResponse } from '@msfa-models/api/avrop.response.model';
import { ContactInformationResponse } from '@msfa-models/api/contact-information.response.model';
import { DeltagareCompactApiResponse } from '@msfa-models/api/deltagare.response.model';
import { DisabilityResponse } from '@msfa-models/api/disability.response.model';
import { DriversLicenseResponse } from '@msfa-models/api/drivers-license.response.model';
import { EducationsResponse } from '@msfa-models/api/educations.response.model';
import { HighestEducationResponse } from '@msfa-models/api/highest-education.response.model';
import { Params } from '@msfa-models/api/params.model';
import { TranslatorResponse } from '@msfa-models/api/translator.response.model';
import { WorkExperiencesResponse } from '@msfa-models/api/work-experiences.response.model';
import { WorkLanguagesResponse } from '@msfa-models/api/work-languages.response.model';
import { Avrop, mapAvropResponseToAvrop } from '@msfa-models/avrop.model';
import { ContactInformation, mapResponseToContactInformation } from '@msfa-models/contact-information.model';
import {
Deltagare,
DeltagareCompact,
DeltagareCompactData,
mapResponseToDeltagareCompact
} from '@msfa-models/deltagare.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 { errorToCustomError } from '@msfa-models/error/custom-error';
import { HighestEducation, mapResponseToHighestEducation } from '@msfa-models/highest-education.model';
import { Sort } from '@msfa-models/sort.model';
import { mapResponseToWorkExperience, WorkExperience } from '@msfa-models/work-experience.model';
import { ErrorService } from '@msfa-services/error.service';
import { sortFromToDates } from '@msfa-utils/sort.util';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { catchError, filter, map, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DeltagareApiService extends UnsubscribeDirective {
private _apiBaseUrl = `${environment.api.url}/deltagare`;
private _currentDeltagareId$ = new BehaviorSubject<string>(null);
private _limit$ = new BehaviorSubject<number>(20);
private _page$ = new BehaviorSubject<number>(1);
private _sort$ = new BehaviorSubject<Sort<keyof DeltagareCompact>>({ key: 'fullName', order: SortOrder.ASC });
public sort$: Observable<Sort<keyof DeltagareCompact>> = this._sort$.asObservable();
constructor(private httpClient: HttpClient, private errorService: ErrorService) {
super();
super.unsubscribeOnDestroy(
this._currentDeltagareId$
.pipe(
filter(currentDeltagareId => !!currentDeltagareId),
switchMap(currentDeltagareId => this._fetchDeltagare$(currentDeltagareId))
)
.subscribe(deltagare => {
this._deltagare$.next(deltagare);
})
);
}
private _deltagare$ = new BehaviorSubject<Deltagare>(null);
public deltagare$: Observable<Deltagare> = this._deltagare$.asObservable();
public allDeltagareData$: Observable<DeltagareCompactData> = combineLatest([
this._limit$,
this._page$,
this._sort$,
]).pipe(switchMap(([limit, page, sort]) => this._fetchAllDeltagare$(limit, page, sort)));
public setSort(newSortKey: keyof DeltagareCompact): void {
const currentSort = this._sort$.getValue();
const order =
currentSort.key === newSortKey && currentSort.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
this._sort$.next({ key: newSortKey, order });
}
public setPage(page: number): void {
this._page$.next(page);
}
private _fetchAllDeltagare$(
limit: number,
page: number,
sort: Sort<keyof DeltagareCompact>
): Observable<DeltagareCompactData> {
const params: Params = {
sort: sort.key as string,
order: sort.order as string,
limit: limit.toString(),
page: page.toString(),
};
return this.httpClient
.get<DeltagareCompactApiResponse>(this._apiBaseUrl, {
params,
})
.pipe(
map(({ data, meta }) => {
return { data: data.map(deltagare => mapResponseToDeltagareCompact(deltagare)), meta };
})
);
}
public setCurrentDeltagareId(currentDeltagareId: string): void {
this._deltagare$.next(null);
this._currentDeltagareId$.next(currentDeltagareId);
}
private _fetchContactInformation$(id: string): Observable<ContactInformation | Partial<ContactInformation>> {
return this.httpClient.get<{ data: ContactInformationResponse }>(`${this._apiBaseUrl}/${id}/contact`).pipe(
map(({ data }) => mapResponseToContactInformation(data)),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
private _fetchDriversLicense$(id: string): Observable<DriversLicense | Partial<DriversLicense>> {
return this.httpClient.get<{ data: DriversLicenseResponse }>(`${this._apiBaseUrl}/${id}/driverlicense`).pipe(
map(({ data }) => mapResponseToDriversLicense(data)),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
private _fetchHighestEducation$(id: string): Observable<HighestEducation | Partial<HighestEducation>> {
return this.httpClient
.get<{ data: HighestEducationResponse }>(`${this._apiBaseUrl}/${id}/educationlevels/highest`)
.pipe(
map(({ data }) => mapResponseToHighestEducation(data)),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
private _fetchEducations$(id: string): Observable<Education[]> {
return this.httpClient.get<{ data: EducationsResponse }>(`${this._apiBaseUrl}/${id}/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 => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchTranslator$(id: string): Observable<string> {
return this.httpClient.get<{ data: TranslatorResponse }>(`${this._apiBaseUrl}/${id}/translator`).pipe(
map(({ data }) => data.sprak?.beskrivning || null),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of('');
})
);
}
private _fetchWorkLanguages$(id: string): Observable<string[]> {
return this.httpClient.get<{ data: WorkLanguagesResponse }>(`${this._apiBaseUrl}/${id}/work/languages`).pipe(
map(({ data }) => data?.sprak?.map(sprak => sprak.beskrivning) || []),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchDisabilities$(id: string): Observable<Disability[]> {
return this.httpClient.get<{ data: DisabilityResponse[] }>(`${this._apiBaseUrl}/${id}/work/disabilities`).pipe(
map(({ data }) => data?.map(funktionsnedsattning => mapResponseToDisability(funktionsnedsattning)) || []),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchWorkExperiences$(id: string): Observable<WorkExperience[]> {
return this.httpClient.get<{ data: WorkExperiencesResponse }>(`${this._apiBaseUrl}/${id}/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 })
) || []
),
map(workExperiences => workExperiences.map(erfarenhet => mapResponseToWorkExperience(erfarenhet))),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchAvropInformation$(id: string): Observable<Avrop | Partial<Avrop>> {
return this.httpClient.get<{ data: AvropResponse }>(`${this._apiBaseUrl}/${id}/avrop`).pipe(
map(({ data }) => (data ? mapAvropResponseToAvrop(data) : {})),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
// As TypeScript has some limitations regarding combining Observables this way,
// we need to type it manually when exceeding 6 Observables inside a combineLatest.
// Read: https://github.com/ReactiveX/rxjs/issues/3601#issuecomment-384711601
private _fetchDeltagare$(id: string): Observable<Deltagare> {
return combineLatest([
this._fetchContactInformation$(id),
this._fetchDriversLicense$(id),
this._fetchHighestEducation$(id),
this._fetchEducations$(id),
this._fetchTranslator$(id),
this._fetchWorkLanguages$(id),
this._fetchDisabilities$(id),
this._fetchWorkExperiences$(id),
this._fetchAvropInformation$(id),
]).pipe(
map(
([
contactInformation,
driversLicense,
highestEducation,
educations,
translator,
workLanguages,
disabilities,
workExperiences,
avropInformation,
]: [
ContactInformation,
DriversLicense,
HighestEducation,
Education[],
string,
string[],
Disability[],
WorkExperience[],
Avrop
]) => ({
id,
...contactInformation,
driversLicense,
highestEducation,
educations,
translator,
workLanguages,
disabilities,
workExperiences,
avropInformation,
})
)
);
}
}

View File

@@ -0,0 +1,210 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { AvropResponse } from '@msfa-models/api/avrop.response.model';
import { ContactInformationResponse } from '@msfa-models/api/contact-information.response.model';
import { DeltagareCompactApiResponse } from '@msfa-models/api/deltagare.response.model';
import { DisabilityResponse } from '@msfa-models/api/disability.response.model';
import { DriversLicenseResponse } from '@msfa-models/api/drivers-license.response.model';
import { EducationsResponse } from '@msfa-models/api/educations.response.model';
import { HighestEducationResponse } from '@msfa-models/api/highest-education.response.model';
import { Params } from '@msfa-models/api/params.model';
import { TranslatorResponse } from '@msfa-models/api/translator.response.model';
import { WorkExperiencesResponse } from '@msfa-models/api/work-experiences.response.model';
import { WorkLanguagesResponse } from '@msfa-models/api/work-languages.response.model';
import { Avrop, mapAvropResponseToAvrop } from '@msfa-models/avrop.model';
import { ContactInformation, mapResponseToContactInformation } from '@msfa-models/contact-information.model';
import { DeltagareCompact, DeltagareCompactData, mapResponseToDeltagareCompact } from '@msfa-models/deltagare.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 { HighestEducation, mapResponseToHighestEducation } from '@msfa-models/highest-education.model';
import { ReportsData } from '@msfa-models/reports.model';
import { Sort } from '@msfa-models/sort.model';
import { mapResponseToWorkExperience, WorkExperience } from '@msfa-models/work-experience.model';
import { sortFromToDates } from '@msfa-utils/sort.util';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DeltagareApiService {
private _apiBaseUrl = `${environment.api.url}/deltagare`;
constructor(private httpClient: HttpClient) {}
public fetchAllDeltagare$(
limit: number,
page: number,
sort: Sort<keyof DeltagareCompact>,
onlyMyDeltagare?: boolean
): Observable<DeltagareCompactData> {
const params: Params = {
sort: sort.key as string,
order: sort.order as string,
limit: limit.toString(),
page: page.toString(),
};
if (onlyMyDeltagare) {
params.onlyMyDeltagare = onlyMyDeltagare.toString();
}
return this.httpClient
.get<DeltagareCompactApiResponse>(this._apiBaseUrl, {
params,
})
.pipe(
map(({ data, meta }) => {
return { data: data.map(deltagare => mapResponseToDeltagareCompact(deltagare)), meta };
}),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta deltagare.\n\n${error.message}` })
);
})
);
}
public fetchReports$(limit: number, page: number, deltagareId: string): Observable<ReportsData> {
return of({ data: [], meta: null });
// TODO: When the API/Mock-API has implemented the endpoint, we can remove use following code
// to make API-requests.
// const params: { [param: string]: string | string[] } = {
// id: deltagareId.toString(),
// limit: limit.toString(),
// page: page.toString(),
// };
// return this.httpClient
// .get<ReportResponse>(`${this._apiBaseUrl}/report`, {
// params,
// })
// .pipe(
// map(({ data, meta }) => {
// data.sort((reportA, reportB) => (+reportA.sendDate < +reportB.sendDate ? 1 : -1));
// return { data: data.map(report => mapReportsResponseToReport(report)), meta };
// })
// );
}
public fetchContactInformation$(id: string): Observable<ContactInformation> {
return this.httpClient.get<{ data: ContactInformationResponse }>(`${this._apiBaseUrl}/${id}/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 fetchDriversLicense$(id: string): Observable<DriversLicense> {
return this.httpClient.get<{ data: DriversLicenseResponse }>(`${this._apiBaseUrl}/${id}/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 fetchHighestEducation$(id: string): Observable<HighestEducation> {
return this.httpClient
.get<{ data: HighestEducationResponse }>(`${this._apiBaseUrl}/${id}/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 fetchEducations$(id: string): Observable<Education[]> {
return this.httpClient.get<{ data: EducationsResponse }>(`${this._apiBaseUrl}/${id}/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$(id: string): Observable<string> {
return this.httpClient.get<{ data: TranslatorResponse }>(`${this._apiBaseUrl}/${id}/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$(id: string): Observable<string[]> {
return this.httpClient.get<{ data: WorkLanguagesResponse }>(`${this._apiBaseUrl}/${id}/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$(id: string): Observable<Disability[]> {
return this.httpClient.get<{ data: DisabilityResponse[] }>(`${this._apiBaseUrl}/${id}/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$(id: string): Observable<WorkExperience[]> {
return this.httpClient.get<{ data: WorkExperiencesResponse }>(`${this._apiBaseUrl}/${id}/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 })
) || []
),
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}` })
);
})
);
}
public fetchAvropInformation$(id: string): Observable<Avrop | Partial<Avrop>> {
return this.httpClient.get<{ data: AvropResponse }>(`${this._apiBaseUrl}/${id}/avrop`).pipe(
map(({ data }) => (data ? mapAvropResponseToAvrop(data) : {})),
catchError((error: Error) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta avropsinformation.\n\n${error.message}` })
);
})
);
}
}

View File

@@ -1,316 +0,0 @@
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { SortOrder } from '@msfa-enums/sort-order.enum';
import { environment } from '@msfa-environment';
import { AvropResponse } from '@msfa-models/api/avrop.response.model';
import { ContactInformationResponse } from '@msfa-models/api/contact-information.response.model';
import { DeltagareCompactApiResponse } from '@msfa-models/api/deltagare.response.model';
import { DisabilityResponse } from '@msfa-models/api/disability.response.model';
import { DriversLicenseResponse } from '@msfa-models/api/drivers-license.response.model';
import { EducationsResponse } from '@msfa-models/api/educations.response.model';
import { HighestEducationResponse } from '@msfa-models/api/highest-education.response.model';
import { ReportResponse } from '@msfa-models/api/report.response.model';
import { Params } from '@msfa-models/api/params.model';
import { TranslatorResponse } from '@msfa-models/api/translator.response.model';
import { WorkExperiencesResponse } from '@msfa-models/api/work-experiences.response.model';
import { WorkLanguagesResponse } from '@msfa-models/api/work-languages.response.model';
import { Avrop, mapAvropResponseToAvrop } from '@msfa-models/avrop.model';
import { ContactInformation, mapResponseToContactInformation } from '@msfa-models/contact-information.model';
import {
Deltagare,
DeltagareCompact,
DeltagareCompactData,
mapResponseToDeltagareCompact
} from '@msfa-models/deltagare.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 { errorToCustomError } from '@msfa-models/error/custom-error';
import { HighestEducation, mapResponseToHighestEducation } from '@msfa-models/highest-education.model';
import { mapReportsResponseToReport, ReportsData } from '@msfa-models/reports.model';
import { Sort } from '@msfa-models/sort.model';
import { mapResponseToWorkExperience, WorkExperience } from '@msfa-models/work-experience.model';
import { ErrorService } from '@msfa-services/error.service';
import { sortFromToDates } from '@msfa-utils/sort.util';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { catchError, filter, map, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DeltagareService extends UnsubscribeDirective {
private _apiBaseUrl = `${environment.api.url}/deltagare`;
private _currentDeltagareId$ = new BehaviorSubject<string>(null);
private _limit$ = new BehaviorSubject<number>(20);
private _page$ = new BehaviorSubject<number>(1);
private _sort$ = new BehaviorSubject<Sort<keyof DeltagareCompact>>({ key: 'fullName', order: SortOrder.ASC });
public sort$: Observable<Sort<keyof DeltagareCompact>> = this._sort$.asObservable();
private _reportType$ = new BehaviorSubject<FormSelectItem>(null);
reportType$: Observable<FormSelectItem> = this._reportType$.asObservable();
private _onlyMyDeltagare$ = new BehaviorSubject<boolean>(false);
public onlyMyDeltagare$: Observable<boolean> = this._onlyMyDeltagare$.asObservable();
constructor(private httpClient: HttpClient, private errorService: ErrorService) {
super();
super.unsubscribeOnDestroy(
this._currentDeltagareId$
.pipe(
filter(currentDeltagareId => !!currentDeltagareId),
switchMap(currentDeltagareId => this._fetchDeltagare$(currentDeltagareId))
)
.subscribe(deltagare => {
this._deltagare$.next(deltagare);
})
);
}
private _deltagare$ = new BehaviorSubject<Deltagare>(null);
public deltagare$: Observable<Deltagare> = this._deltagare$.asObservable();
public allDeltagareData$: Observable<DeltagareCompactData> = combineLatest([
this._limit$,
this._page$,
this._sort$,
this._onlyMyDeltagare$,
]).pipe(
switchMap(([limit, page, sort, onlyMyDeltagare]) => this._fetchAllDeltagare$(limit, page, sort, onlyMyDeltagare))
);
public reportsData$: Observable<ReportsData> = combineLatest([
this._limit$,
this._page$
]).pipe(switchMap(([limit, page]) => this._fetchReports$(limit, page)));
public setSort(newSortKey: keyof DeltagareCompact): void {
const currentSort = this._sort$.getValue();
const order =
currentSort.key === newSortKey && currentSort.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
this._sort$.next({ key: newSortKey, order });
}
public setPage(page: number): void {
this._page$.next(page);
}
private _fetchAllDeltagare$(
limit: number,
page: number,
sort: Sort<keyof DeltagareCompact>,
onlyMyDeltagare?: boolean
): Observable<DeltagareCompactData> {
const params: Params = {
sort: sort.key as string,
order: sort.order as string,
limit: limit.toString(),
page: page.toString(),
};
if (onlyMyDeltagare) {
params.onlyMyDeltagare = onlyMyDeltagare.toString();
}
return this.httpClient
.get<DeltagareCompactApiResponse>(this._apiBaseUrl, {
params,
})
.pipe(
map(({ data, meta }) => {
return { data: data.map(deltagare => mapResponseToDeltagareCompact(deltagare)), meta };
})
);
}
private _fetchReports$(
limit: number,
page: number
): Observable<ReportsData> {
const params: { [param: string]: string | string[] } = {
limit: limit.toString(),
page: page.toString()
};
return this.httpClient
.get<ReportResponse>(`${this._apiBaseUrl}/report`, {
params
})
.pipe(
map(({ data, meta }) => {
data.sort((reportA, reportB) =>
+reportA.sendDate < +reportB.sendDate ? 1 : -1)
return { data: data.map(report => mapReportsResponseToReport(report)), meta };
})
);
}
public setReportType(reportType: FormSelectItem): void {
this._reportType$.next(reportType);
}
public setCurrentDeltagareId(currentDeltagareId: string): void {
this._deltagare$.next(null);
this._currentDeltagareId$.next(currentDeltagareId);
}
public setOnlyMyDeltagare(value: boolean): void {
this._onlyMyDeltagare$.next(value);
}
private _fetchContactInformation$(id: string): Observable<ContactInformation | Partial<ContactInformation>> {
return this.httpClient.get<{ data: ContactInformationResponse }>(`${this._apiBaseUrl}/${id}/contact`).pipe(
map(({ data }) => mapResponseToContactInformation(data)),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
private _fetchDriversLicense$(id: string): Observable<DriversLicense | Partial<DriversLicense>> {
return this.httpClient.get<{ data: DriversLicenseResponse }>(`${this._apiBaseUrl}/${id}/driverlicense`).pipe(
map(({ data }) => mapResponseToDriversLicense(data)),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
private _fetchHighestEducation$(id: string): Observable<HighestEducation | Partial<HighestEducation>> {
return this.httpClient
.get<{ data: HighestEducationResponse }>(`${this._apiBaseUrl}/${id}/educationlevels/highest`)
.pipe(
map(({ data }) => mapResponseToHighestEducation(data)),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
private _fetchEducations$(id: string): Observable<Education[]> {
return this.httpClient.get<{ data: EducationsResponse }>(`${this._apiBaseUrl}/${id}/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 => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchTranslator$(id: string): Observable<string> {
return this.httpClient.get<{ data: TranslatorResponse }>(`${this._apiBaseUrl}/${id}/translator`).pipe(
map(({ data }) => data.sprak?.beskrivning || null),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of('');
})
);
}
private _fetchWorkLanguages$(id: string): Observable<string[]> {
return this.httpClient.get<{ data: WorkLanguagesResponse }>(`${this._apiBaseUrl}/${id}/work/languages`).pipe(
map(({ data }) => data?.sprak?.map(sprak => sprak.beskrivning) || []),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchDisabilities$(id: string): Observable<Disability[]> {
return this.httpClient.get<{ data: DisabilityResponse[] }>(`${this._apiBaseUrl}/${id}/work/disabilities`).pipe(
map(({ data }) => data?.map(funktionsnedsattning => mapResponseToDisability(funktionsnedsattning)) || []),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchWorkExperiences$(id: string): Observable<WorkExperience[]> {
return this.httpClient.get<{ data: WorkExperiencesResponse }>(`${this._apiBaseUrl}/${id}/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 })
) || []
),
map(workExperiences => workExperiences.map(erfarenhet => mapResponseToWorkExperience(erfarenhet))),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of([]);
})
);
}
private _fetchAvropInformation$(id: string): Observable<Avrop | Partial<Avrop>> {
return this.httpClient.get<{ data: AvropResponse }>(`${this._apiBaseUrl}/${id}/avrop`).pipe(
map(({ data }) => (data ? mapAvropResponseToAvrop(data) : {})),
catchError(error => {
this.errorService.add(errorToCustomError(error));
return of({});
})
);
}
// As TypeScript has some limitations regarding combining Observables this way,
// we need to type it manually when exceeding 6 Observables inside a combineLatest.
// Read: https://github.com/ReactiveX/rxjs/issues/3601#issuecomment-384711601
private _fetchDeltagare$(id: string): Observable<Deltagare> {
return combineLatest([
this._fetchContactInformation$(id),
this._fetchDriversLicense$(id),
this._fetchHighestEducation$(id),
this._fetchEducations$(id),
this._fetchTranslator$(id),
this._fetchWorkLanguages$(id),
this._fetchDisabilities$(id),
this._fetchWorkExperiences$(id),
this._fetchAvropInformation$(id),
]).pipe(
map(
([
contactInformation,
driversLicense,
highestEducation,
educations,
translator,
workLanguages,
disabilities,
workExperiences,
avropInformation,
]: [
ContactInformation,
DriversLicense,
HighestEducation,
Education[],
string,
string[],
Disability[],
WorkExperience[],
Avrop
]) => ({
id,
...contactInformation,
driversLicense,
highestEducation,
educations,
translator,
workLanguages,
disabilities,
workExperiences,
avropInformation,
})
)
);
}
}

View File

@@ -34,6 +34,10 @@ export class UserService extends UnsubscribeDirective {
public userRoles$: Observable<Role[]> = this._userRoles$.asObservable();
private _selectedOrganizationNumber$ = new BehaviorSubject<string>(null);
public get userRolesSnapshot(): Role[] {
return this._userRoles$.getValue();
}
constructor(private httpClient: HttpClient, private authenticationService: AuthenticationService) {
super();
this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);

View File

@@ -0,0 +1,46 @@
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$(deltagareId: string): Observable<ContactInformation> {
return this.deltagareApiService.fetchContactInformation$(deltagareId);
}
public fetchAvropInformation$(deltagareId: string): Observable<Avrop> {
return this.deltagareApiService.fetchAvropInformation$(deltagareId) as Observable<Avrop>;
}
public fetchWorkExperiences$(deltagareId: string): Observable<WorkExperience[]> {
return this.deltagareApiService.fetchWorkExperiences$(deltagareId);
}
public fetchHighestEducation$(deltagareId: string): Observable<HighestEducation> {
return this.deltagareApiService.fetchHighestEducation$(deltagareId) as Observable<HighestEducation>;
}
public fetchEducations$(deltagareId: string): Observable<Education[]> {
return this.deltagareApiService.fetchEducations$(deltagareId);
}
public fetchDriversLicense$(deltagareId: string): Observable<DriversLicense> {
return this.deltagareApiService.fetchDriversLicense$(deltagareId) as Observable<DriversLicense>;
}
public fetchWorkLanguages$(deltagareId: string): Observable<string[]> {
return this.deltagareApiService.fetchWorkLanguages$(deltagareId);
}
public fetchDisabilities$(deltagareId: string): Observable<Disability[]> {
return this.deltagareApiService.fetchDisabilities$(deltagareId);
}
public fetchReports$(limit: number, page: number, deltagareId: string): Observable<ReportsData> {
return this.deltagareApiService.fetchReports$(limit, page, deltagareId);
}
}

View File

@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { SortOrder } from '@msfa-enums/sort-order.enum';
import { DeltagareCompact, DeltagareCompactData } from '@msfa-models/deltagare.model';
import { Sort } from '@msfa-models/sort.model';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { DeltagareApiService } from './api/deltagare.api.service';
@Injectable({
providedIn: 'root',
})
export class DeltagareService {
private _limit$ = new BehaviorSubject<number>(20);
private _page$ = new BehaviorSubject<number>(1);
private _sort$ = new BehaviorSubject<Sort<keyof DeltagareCompact>>({ key: 'fullName', order: SortOrder.ASC });
public sort$: Observable<Sort<keyof DeltagareCompact>> = this._sort$.asObservable();
private _onlyMyDeltagare$ = new BehaviorSubject<boolean>(false);
public onlyMyDeltagare$: Observable<boolean> = this._onlyMyDeltagare$.asObservable();
public allDeltagareData$: Observable<DeltagareCompactData> = combineLatest([
this._limit$,
this._page$,
this._sort$,
this._onlyMyDeltagare$,
]).pipe(
switchMap(([limit, page, sort, onlyMyDeltagare]) =>
this.deltagareApiService.fetchAllDeltagare$(limit, page, sort, onlyMyDeltagare)
)
);
constructor(private deltagareApiService: DeltagareApiService) {}
public setSort(newSortKey: keyof DeltagareCompact): void {
const currentSort = this._sort$.getValue();
const order =
currentSort.key === newSortKey && currentSort.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
this._sort$.next({ key: newSortKey, order });
}
public setPage(page: number): void {
this._page$.next(page);
}
public setOnlyMyDeltagare(value: boolean): void {
this._onlyMyDeltagare$.next(value);
}
}