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

@@ -11,9 +11,7 @@
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{ "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
]
"depConstraints": [{ "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }]
}
]
}

View File

@@ -14,6 +14,15 @@
],
"parserOptions": { "project": ["apps/mina-sidor-fa/tsconfig.*?.json"] },
"rules": {
"max-len": [
1,
140,
2,
{
"ignorePattern": "^import\\s.+\\sfrom\\s.+;$",
"ignoreUrls": true
}
],
"@angular-eslint/directive-selector": [
"error",
{

View File

@@ -42,7 +42,7 @@ activeFeatures.forEach(feature => {
case Feature.ADMINISTRATION:
routes.push({
path: 'administration',
data: { title: 'Administration', expectedRole: RoleEnum.MSFA_AuthAdmin },
data: { title: 'Administration', expectedRoles: [RoleEnum.MSFA_AuthAdmin] },
loadChildren: () => import('./pages/administration/administration.module').then(m => m.AdministrationModule),
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
});
@@ -50,7 +50,7 @@ activeFeatures.forEach(feature => {
case Feature.AVROP:
routes.push({
path: 'nya-deltagare',
data: { title: 'Nya deltagare', expectedRole: RoleEnum.MSFA_ReceiveDeltagare },
data: { title: 'Nya deltagare', expectedRoles: [RoleEnum.MSFA_ReceiveDeltagare] },
loadChildren: () => import('./pages/avrop/avrop.module').then(m => m.AvropModule),
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
});
@@ -58,7 +58,7 @@ activeFeatures.forEach(feature => {
case Feature.DELTAGARE:
routes.push({
path: 'deltagare',
data: { title: 'Deltagare', expectedRole: RoleEnum.MSFA_ReportAndPlanning },
data: { title: 'Deltagare', expectedRoles: [RoleEnum.MSFA_ReportAndPlanning, RoleEnum.MSFA_ReceiveDeltagare] },
loadChildren: () => import('./pages/deltagare/deltagare.module').then(m => m.DeltagareModule),
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
});

View File

@@ -1,9 +1,9 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { environment } from '@msfa-environment';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { filter, map, mergeMap, switchMap } from 'rxjs/operators';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { environment } from '@msfa-environment';
import { filter, map, switchMap } from 'rxjs/operators';
@Component({
selector: 'msfa-root',

View File

@@ -50,6 +50,7 @@
max-width: 400px !important;
margin: 0;
overflow-wrap: break-word;
white-space: pre-wrap;
}
&__close-button {

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DeltagareCompact, DeltagareCompactData } from '@msfa-models/deltagare.model';
import { Sort } from '@msfa-models/sort.model';
import { DeltagareService } from '@msfa-services/api/deltagare.service';
import { DeltagareService } from '@msfa-services/deltagare.service';
import { Observable } from 'rxjs';
@Component({

View File

@@ -0,0 +1,83 @@
<div class="deltagare-tab-experiences">
<div class="deltagare-tab-experiences__tab-column">
<h2>Arbetslivserfarenhet</h2>
<ng-container *ngIf="firstVisibleWorkExperiences;">
<ul
class="deltagare-tab-experiences__experience-list"
*ngIf="firstVisibleWorkExperiences.length; else emptyText;"
>
<li *ngFor="let workExperience of firstVisibleWorkExperiences">
<h3 class="deltagare-tab-experiences__subheading">{{ workExperience.employer }}</h3>
<digi-typography-time [afDateTime]="workExperience.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="workExperience.dateTo"></digi-typography-time><br />
{{ workExperience.profession }}
<p>{{ workExperience.description }}</p>
</li>
</ul>
</ng-container>
<ng-container *ngIf="hiddenWorkExperiences">
<digi-ng-layout-expansion-panel
class="deltagare-tab-experiences__accordion"
[afExpanded]="accordionExpanded"
(click)="toggleAccordionExpanded()"
*ngIf="hiddenWorkExperiences.length"
>
<span class="deltagare-tab-experiences__accordion-trigger" data-slot-trigger
>{{ accordionExpanded ? 'Dölj' : 'Visa' }} fler arbetsgivare</span
>
<ul class="deltagare-tab-experiences__experience-list">
<li *ngFor="let workExperience of hiddenWorkExperiences">
<h3 class="deltagare-tab-experiences__subheading">{{ workExperience.employer }}</h3>
<digi-typography-time [afDateTime]="workExperience.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="workExperience.dateTo"></digi-typography-time><br />
{{ workExperience.profession }}
<p>{{ workExperience.description }}</p>
</li>
</ul>
</digi-ng-layout-expansion-panel>
</ng-container>
</div>
<div class="deltagare-tab-experiences__tab-column">
<h2>Utbildning</h2>
<dl *ngIf="highestEducation">
<dt>Högsta utbildningsnivå:</dt>
<dd>
<ng-container *ngIf="highestEducation.level; else emptyText">
{{ highestEducation.level.description }}: {{ highestEducation.sunKod.description }}
</ng-container>
</dd>
<ng-container *ngIf="educations">
<h3 class="deltagare-tab-experiences__subheading deltagare-tab-experiences__subheading--with-margin">
Utbildningar:
</h3>
<ul class="deltagare-tab-experiences__experience-list" *ngIf="educations.length; else emptyText">
<li *ngFor="let education of educations">
<h4 class="deltagare-tab-experiences__subheading">{{ education.organizer }}</h4>
<digi-typography-time [afDateTime]="education.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="education.dateFrom"></digi-typography-time><br />
{{ education.education}}
<p>{{ education.description }}</p>
</li>
</ul>
</ng-container>
</dl>
</div>
<div class="deltagare-tab-experiences__tab-column" *ngIf="driversLicense">
<h2>Körkort</h2>
<dl>
<dt>Har körkort</dt>
<dd>{{driversLicense.licenses.length ? 'Ja' : 'Nej'}}</dd>
<ng-container *ngIf="driversLicense.licenses.length">
<dt>Körkortsklasser</dt>
<dd>{{driversLicense.licenses.join(', ')}}</dd>
<dt>Tillgång till bil</dt>
<dd>{{driversLicense.accessToCar ? 'Ja' : 'Nej'}}</dd>
</ng-container>
</dl>
</div>
</div>
<ng-template #emptyText>
<span>Info saknas</span>
</ng-template>

View File

@@ -0,0 +1,34 @@
@import 'mixins/list';
.deltagare-tab-experiences {
display: contents;
&__tab-column {
flex-grow: 1;
flex-basis: 0;
}
&__subheading {
font-size: var(--digi--typography--font-size--desktop);
font-weight: var(--digi--typography--font-weight--semibold);
margin: var(--digi--layout--gutter--s) 0 0;
&--with-margin {
font-size: var(--digi--typography--font-size--h3);
margin-bottom: var(--digi--layout--gutter--s);
}
}
&__experience-list {
@include msfa__reset-list;
}
&__accordion {
display: block;
margin-top: var(--digi--layout--gutter);
}
&__accordion-trigger {
font-weight: var(--digi--typography--font-weight--semibold);
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeltagareTabExperiencesComponent } from './deltagare-tab-experiences.component';
describe('DeltagareTabExperiencesComponent', () => {
let component: DeltagareTabExperiencesComponent;
let fixture: ComponentFixture<DeltagareTabExperiencesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DeltagareTabExperiencesComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareTabExperiencesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,32 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { DriversLicense } from '@msfa-models/drivers-license.model';
import { Education } from '@msfa-models/education.model';
import { HighestEducation } from '@msfa-models/highest-education.model';
import { WorkExperience } from '@msfa-models/work-experience.model';
@Component({
selector: 'msfa-deltagare-tab-experiences',
templateUrl: './deltagare-tab-experiences.component.html',
styleUrls: ['./deltagare-tab-experiences.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareTabExperiencesComponent {
@Input() workExperiences: WorkExperience[];
@Input() highestEducation: HighestEducation;
@Input() educations: Education[];
@Input() driversLicense: DriversLicense;
accordionExpanded = false;
get firstVisibleWorkExperiences(): WorkExperience[] {
return this.workExperiences?.slice(0, 2);
}
get hiddenWorkExperiences(): WorkExperience[] {
return this.workExperiences?.slice(2);
}
toggleAccordionExpanded(): void {
this.accordionExpanded = !this.accordionExpanded;
}
}

View File

@@ -0,0 +1,76 @@
<div class="deltagare-tab-personal-information">
<div *ngIf="contactInformation" class="deltagare-tab-personal-information__tab-column">
<h2>Personuppgifter</h2>
<dl>
<dt>Namn:</dt>
<dd *ngIf="contactInformation.fullName; else emptyDD">{{ contactInformation.fullName }}</dd>
<dt>Personnummer:</dt>
<dd *ngIf="contactInformation.ssn; else emptyDD">
<msfa-hide-text
symbols="********-****"
[changingText]="contactInformation.ssn"
ariaLabelType="personnummer"
></msfa-hide-text>
</dd>
<ng-container *ngFor="let address of contactInformation.addresses">
<dt>{{address.type}}:</dt>
<dd>
<address>
{{ address.street }}<br />
{{ address.postalCode }} {{ address.city }}
</address>
</dd>
</ng-container>
<dt>Telefon:</dt>
<ng-container *ngIf="contactInformation.phoneNumbers?.length; else emptyDD">
<ng-container *ngFor="let phoneNumber of contactInformation.phoneNumbers">
<dd>{{ phoneNumber.type }}: {{phoneNumber.number}}</dd>
</ng-container>
</ng-container>
<dt>E-postadress:</dt>
<dd *ngIf="contactInformation.email; else emptyDD">
<a href="mailto:{{contactInformation.email}}">{{ contactInformation.email }}</a>
</dd>
</dl>
</div>
<ng-container *ngIf="avropInformation">
<div class="deltagare-tab-personal-information__tab-column">
<h2>Om tjänsten</h2>
<dl>
<dt>Tillhörande tjänst:</dt>
<dd *ngIf="avropInformation.tjanst; else emptyDD">{{ avropInformation.tjanst }}</dd>
<dt>Datum för tjänstens början:</dt>
<dd *ngIf="avropInformation.startDate; else emptyDD">
<digi-typography-time [afDateTime]="avropInformation.startDate"></digi-typography-time>
</dd>
<dt>Datum för tjänstens slut:</dt>
<dd *ngIf="avropInformation.endDate; else emptyDD">
<digi-typography-time [afDateTime]="avropInformation.endDate"></digi-typography-time>
</dd>
<dt>Deltagandefrekvens:</dt>
<dd *ngIf="avropInformation.participationFrequency; else emptyDD">
{{ avropInformation.participationFrequency }}
</dd>
<dt>Nivå:</dt>
<dd *ngIf="avropInformation.participationFrequency; else emptyDD">{{ avropInformation.trackName }}</dd>
<dt>Utförande verksamhet:</dt>
<dd *ngIf="avropInformation.utforandeVerksamhet; else emptyDD">{{ avropInformation.utforandeVerksamhet }}</dd>
<dt>Utförande adress:</dt>
<dd *ngIf="avropInformation.utforandeAdress; else emptyDD">{{ avropInformation.utforandeAdress }}</dd>
<dt>Genomförandereferens:</dt>
<dd *ngIf="avropInformation.genomforandeReferens; else emptyDD">{{ avropInformation.genomforandeReferens }}</dd>
</dl>
</div>
<div class="deltagare-tab-personal-information__tab-column">
<h2>Handledare</h2>
<dl>
<dt>Tilldelad handledare:</dt>
<dd *ngIf="avropInformation.handledare; else emptyDD">{{ avropInformation.handledare }}</dd>
</dl>
</div>
</ng-container>
</div>
<ng-template #emptyDD>
<dd>Info saknas</dd>
</ng-template>

View File

@@ -0,0 +1,8 @@
.deltagare-tab-personal-information {
display: contents;
&__tab-column {
flex-grow: 1;
flex-basis: 0;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeltagareTabPersonalInformationComponent } from './deltagare-tab-personal-information.component';
describe('DeltagareTabPersonalInformationComponent', () => {
let component: DeltagareTabPersonalInformationComponent;
let fixture: ComponentFixture<DeltagareTabPersonalInformationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DeltagareTabPersonalInformationComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareTabPersonalInformationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { ContactInformation } from '@msfa-models/contact-information.model';
@Component({
selector: 'msfa-deltagare-tab-personal-information',
templateUrl: './deltagare-tab-personal-information.component.html',
styleUrls: ['./deltagare-tab-personal-information.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareTabPersonalInformationComponent {
@Input() contactInformation: ContactInformation;
@Input() avropInformation: Avrop;
}

View File

@@ -0,0 +1,37 @@
<div *ngIf="reportsData$ | async as reportsData; else loadingRef" class="deltagare-tab-reports">
<form [formGroup]="reportPickerFormGroup" class="deltagare-tab-reports__form">
<h3>Skapa ny rapport</h3>
<p>Här kan du skicka rapporter om deltagaren till arbetsförmedlingen.</p>
<digi-ng-form-select
[formControlName]="reportsFormControlName"
afLabel="Välj rapporttyp"
afPlaceholder="Välj rapporttyp"
[afSelectItems]="selectableReportTypes"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="reportsFormControl.invalid && reportsFormControl.touched"
>
</digi-ng-form-select>
<digi-form-validation-message af-variation="error" *ngIf="reportsFormControl.invalid && reportsFormControl.touched">
Du måste välja en rapporttyp
</digi-form-validation-message>
</form>
<div class="deltagare-tab-reports__cta-wrapper" *ngIf="currentDeltagareId$ | async as currentDeltagareId">
<digi-ng-link-button
afText="Skapa ny rapport"
(click)="onFormSubmitted($event, reportsFormControl.value, currentDeltagareId)"
></digi-ng-link-button>
</div>
<div>
<h3>Inskickade rapporter</h3>
<msfa-reports
[reports]="reportsData.data"
[paginationMeta]="reportsData.meta"
(paginated)="setNewPage($event)"
></msfa-reports>
</div>
</div>
<ng-template #loadingRef>
<msfa-loader type="padded"></msfa-loader>
</ng-template>

View File

@@ -0,0 +1,12 @@
@import 'variables/gutters';
.deltagare-tab-reports {
&__form {
max-width: var(--digi--typography--text--max-width);
}
&__cta-wrapper {
margin-top: $digi--layout--gutter--l;
margin-bottom: $digi--layout--gutter--xl;
}
}

View File

@@ -0,0 +1,28 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { LoaderModule } from '@msfa-shared/components/loader/loader.module';
import { DeltagareTabReportsComponent } from './deltagare-tab-reports.component';
describe('DeltagareTabReportsComponent', () => {
let component: DeltagareTabReportsComponent;
let fixture: ComponentFixture<DeltagareTabReportsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DeltagareTabReportsComponent],
imports: [RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule, LoaderModule],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareTabReportsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,92 @@
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ReportsData } from '@msfa-models/reports.model';
import { DeltagareCardService } from '@msfa-services/deltagare-card.service';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators';
@Component({
selector: 'msfa-deltagare-tab-reports',
templateUrl: './deltagare-tab-reports.component.html',
styleUrls: ['./deltagare-tab-reports.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareTabReportsComponent {
private _limit$ = new BehaviorSubject<number>(20);
private _page$ = new BehaviorSubject<number>(1);
private _type$ = new BehaviorSubject<FormSelectItem>(null);
readonly reportsFormControlName = 'reports';
public currentDeltagareId$: Observable<string> = this.activatedRoute.params.pipe(
map(params => params.deltagareId as string),
distinctUntilChanged(([prevDeltagareId], [currDeltagareId]) => prevDeltagareId === currDeltagareId)
);
reportsData$: Observable<ReportsData> = combineLatest([this.currentDeltagareId$, this._limit$, this._page$]).pipe(
switchMap(([deltagareId, limit, page]) => this.deltagareCardService.fetchReports$(limit, page, deltagareId)),
shareReplay(1)
);
reportPickerFormGroup: FormGroup = this.formBuilder.group({
// eslint-disable-next-line @typescript-eslint/unbound-method
reports: this.formBuilder.control('', [Validators.required]),
});
selectableReportTypes: Array<FormSelectItem> = [
{ name: 'Avvikelse', value: 'avvikelse' },
{ name: 'Gemensam Planering', value: 'planering' },
];
selectedReportType: FormSelectItem;
constructor(
private activatedRoute: ActivatedRoute,
private deltagareCardService: DeltagareCardService,
private formBuilder: FormBuilder,
private router: Router
) {}
get reportsFormControl(): AbstractControl | null {
return this.reportPickerFormGroup?.get(this.reportsFormControlName);
}
onFormSubmitted(event: Event, reportType: string, deltagareId: string): void {
event.preventDefault();
switch (reportType) {
case 'planering':
if (this.reportsFormControl.valid) {
this.router.navigate(['/deltagare/planering', deltagareId]).catch(error => {
console.error(error);
});
}
break;
case 'avvikelse':
if (this.reportsFormControl.valid) {
this.router.navigate(['/deltagare/rapportera', deltagareId]).catch(error => {
console.error(error);
});
}
break;
default:
return;
}
this.reportsFormControl.markAsTouched();
if (!this.selectableReportTypes || this.reportPickerFormGroup.invalid) {
return;
}
const selectedReportType = this.selectableReportTypes.find(report => {
return report.value === this.reportsFormControl.value;
});
this._type$.next(selectedReportType);
}
setNewPage(page: number): void {
this._page$.next(page);
}
}

View File

@@ -0,0 +1,36 @@
<div class="deltagare-tab-sensitive-information">
<div class="deltagare-tab-sensitive-information__tab-column" *ngIf="disabilities">
<h2>Funktionsnedsättningar</h2>
<dl *ngIf="disabilities.length; else noDisabilities">
<ng-container *ngFor="let disability of disabilities; let index = index">
<dt>Funktionsnedsättning {{index + 1}}</dt>
<dd>
<span>{{ disability.title }}</span>
<digi-ng-popover
*ngIf="disability.description"
class="deltagare-tab-sensitive-information__popover"
[afRelativeIconSize]="true"
>{{ disability.description }}</digi-ng-popover
>
</dd>
</ng-container>
</dl>
<ng-template #noDisabilities>
<p>Deltagaren har inga funktionsnedsättningar registrerad.</p>
</ng-template>
</div>
<div class="deltagare-tab-sensitive-information__tab-column" *ngIf="avropInformation">
<h2>Språk</h2>
<dl>
<dt>Behov av tolk:</dt>
<dd>{{avropInformation.tolkbehov ? 'Ja (' + avropInformation.tolkbehov + ')' : 'Nej'}}</dd>
<dt>Behov av språkstöd:</dt>
<dd>{{avropInformation.sprakstod ? 'Ja (' + avropInformation.sprakstod + ')' : 'Nej'}}</dd>
<ng-container *ngIf="workLanguages">
<dt>Språk som kan användas på jobbet:</dt>
<dd>{{ workLanguages.length ? workLanguages.join(', ') : 'Info saknas'}}</dd>
</ng-container>
</dl>
</div>
</div>

View File

@@ -0,0 +1,19 @@
@import 'variables/z-index';
.deltagare-tab-sensitive-information {
display: contents;
&__tab-column {
flex-grow: 1;
flex-basis: 0;
}
&__popover {
display: inline-block;
margin-left: var(--digi--layout--gutter--s);
::ng-deep .digi-ng-popover__container {
z-index: $msfa__z-index-popover;
}
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeltagareTabSensitiveInformationComponent } from './deltagare-tab-sensitive-information.component';
describe('DeltagareTabSensitiveInformationComponent', () => {
let component: DeltagareTabSensitiveInformationComponent;
let fixture: ComponentFixture<DeltagareTabSensitiveInformationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DeltagareTabSensitiveInformationComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareTabSensitiveInformationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { Disability } from '@msfa-models/disability.model';
@Component({
selector: 'msfa-deltagare-tab-sensitive-information',
templateUrl: './deltagare-tab-sensitive-information.component.html',
styleUrls: ['./deltagare-tab-sensitive-information.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareTabSensitiveInformationComponent {
@Input() avropInformation: Avrop;
@Input() workLanguages: string[];
@Input() disabilities: Disability[];
}

View File

@@ -1,240 +1,70 @@
<msfa-layout>
<digi-typography>
<section *ngIf="deltagare$ | async as deltagare; else loadingRef" class="deltagare-card">
<section class="deltagare-card" *ngIf="activeTab$ | async as activeTab">
<header class="deltagare-card__header">
<msfa-back-link [route]="['/deltagare']">Tillbaka till deltagarlistan</msfa-back-link>
<h1>Deltagarinformation</h1>
</header>
<digi-navigation-tabs af-aria-label="Deltagarinformation">
<digi-navigation-tab af-aria-label="Deltagare & tjänst" af-id="deltagare-card-personuppgifter">
<div class="deltagare-card__tab-contents">
<div class="deltagare-card__tab-column">
<h2>Personuppgifter</h2>
<dl>
<dt>Namn:</dt>
<dd *ngIf="deltagare.fullName; else emptyDD">{{ deltagare.fullName }}</dd>
<dt>Personnummer:</dt>
<dd *ngIf="deltagare.ssn; else emptyDD">
<msfa-hide-text
symbols="********-****"
[changingText]="deltagare.ssn"
ariaLabelType="personnummer"
></msfa-hide-text>
</dd>
<ng-container *ngFor="let address of deltagare.addresses">
<dt>{{address.type}}:</dt>
<dd>
<address>
{{ address.street }}<br />
{{ address.postalCode }} {{ address.city }}
</address>
</dd>
<digi-navigation-tab
(afOnToggle)="setActiveTab(0)"
af-aria-label="Deltagare & tjänst"
af-id="deltagare-card-personal-information"
*ngIf="deltagareTjanstVisible"
>
<ng-container *ngIf="activeTab === '0'">
<msfa-deltagare-tab-personal-information
*ngIf="(tab0Loading$ | async) === false; else loadingRef"
class="deltagare-card__tab-contents"
[contactInformation]="contactInformation$ | async"
[avropInformation]="avropInformation$ | async"
></msfa-deltagare-tab-personal-information>
</ng-container>
<dt>Telefon:</dt>
<ng-container *ngIf="deltagare.phoneNumbers?.length; else emptyDD">
<ng-container *ngFor="let phoneNumber of deltagare.phoneNumbers">
<dd>{{ phoneNumber.type }}: {{phoneNumber.number}}</dd>
</ng-container>
</ng-container>
<dt>E-postadress:</dt>
<dd *ngIf="deltagare.email; else emptyDD">
<a href="mailto:{{deltagare.email}}">{{ deltagare.email }}</a>
</dd>
</dl>
</div>
<div class="deltagare-card__tab-column">
<h2>Om tjänsten</h2>
<dl>
<dt>Tillhörande tjänst:</dt>
<dd *ngIf="deltagare.avropInformation.tjanst; else emptyDD">{{ deltagare.avropInformation.tjanst }}</dd>
<dt>Datum för tjänstens början:</dt>
<dd *ngIf="deltagare.avropInformation.startDate; else emptyDD">
<digi-typography-time [afDateTime]="deltagare.avropInformation.startDate"></digi-typography-time>
</dd>
<dt>Datum för tjänstens slut:</dt>
<dd *ngIf="deltagare.avropInformation.endDate; else emptyDD">
<digi-typography-time [afDateTime]="deltagare.avropInformation.endDate"></digi-typography-time>
</dd>
<dt>Deltagandefrekvens:</dt>
<dd *ngIf="deltagare.avropInformation.participationFrequency; else emptyDD">
{{ deltagare.avropInformation.participationFrequency }}
</dd>
<dt>Nivå:</dt>
<dd *ngIf="deltagare.avropInformation.participationFrequency; else emptyDD">
{{ deltagare.avropInformation.trackName }}
</dd>
<dt>Utförande verksamhet:</dt>
<dd *ngIf="deltagare.avropInformation.utforandeVerksamhet; else emptyDD">
{{ deltagare.avropInformation.utforandeVerksamhet }}
</dd>
<dt>Utförande adress:</dt>
<dd *ngIf="deltagare.avropInformation.utforandeAdress; else emptyDD">
{{ deltagare.avropInformation.utforandeAdress }}
</dd>
<dt>Genomförandereferens:</dt>
<dd *ngIf="deltagare.avropInformation.genomforandeReferens; else emptyDD">
{{ deltagare.avropInformation.genomforandeReferens }}
</dd>
</dl>
</div>
<div class="deltagare-card__tab-column">
<h2>Handledare</h2>
<dl>
<dt>Tilldelad handledare:</dt>
<dd *ngIf="deltagare.avropInformation.handledare; else emptyDD">
{{ deltagare.avropInformation.handledare }}
</dd>
</dl>
</div>
</div>
</digi-navigation-tab>
<digi-navigation-tab af-aria-label="Rapportering" af-id="deltagare-card-rapportering">
<div class="deltagare-card__select-report">
<h3>Skapa ny rapport</h3>
<p>Här kan du skicka rapporter om deltagaren till arbetsförmedlingen.</p>
<form [formGroup]="reportPickerFormGroup">
<digi-ng-form-select
[formControlName]="reportsFormControlName"
afLabel="Välj rapporttyp"
afPlaceholder="Välj rapporttyp"
[afSelectItems]="selectableReportTypes"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="reportsFormControl.invalid && reportsFormControl.touched"
>
</digi-ng-form-select>
<digi-form-validation-message
af-variation="error"
*ngIf="reportsFormControl.invalid && reportsFormControl.touched"
>
Du måste välja en rapporttyp
</digi-form-validation-message>
</form>
</div>
<div class="deltagare-card__cta-wrapper">
<digi-ng-link-button
afText="Skapa ny rapport"
(click)="onFormSubmitted($event, reportsFormControl.value, deltagare.id)"
></digi-ng-link-button>
</div>
<div>
<h3>Inskickade rapporter</h3>
<msfa-reports
*ngIf="reportsData$ | async as reportsData, else loadingRef"
[reports]="reportsData.data"
[paginationMeta]="reportsData?.meta"
(paginated)="setNewPage($event)"
></msfa-reports>
</div>
</digi-navigation-tab>
<digi-navigation-tab af-aria-label="Erfarenheter" af-id="deltagare-card-matchningsuppgifter">
<div class="deltagare-card__tab-contents">
<div class="deltagare-card__tab-column">
<h2>Arbetslivserfarenhet</h2>
<ng-container *ngIf="firstVisibleWorkExperiences$ | async as firstVisibleWorkExperiences;">
<ul class="deltagare-card__experience-list" *ngIf="firstVisibleWorkExperiences.length; else emptyText;">
<li *ngFor="let workExperience of firstVisibleWorkExperiences">
<h3 class="deltagare-card__subheading">{{ workExperience.employer }}</h3>
<digi-typography-time [afDateTime]="workExperience.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="workExperience.dateTo"></digi-typography-time><br />
{{ workExperience.profession }}
<p>{{ workExperience.description }}</p>
</li>
</ul>
</ng-container>
<ng-container *ngIf="hiddenWorkExperiences$ | async as hiddenWorkExperiences">
<digi-ng-layout-expansion-panel
class="deltagare-card__accordion"
[afExpanded]="accordionExpanded"
(click)="toggleAccordionExpanded()"
*ngIf="hiddenWorkExperiences.length"
>
<span class="deltagare-card__accordion-trigger" data-slot-trigger
>{{ accordionExpanded ? 'Dölj' : 'Visa' }} fler arbetsgivare</span
>
<ul class="deltagare-card__experience-list">
<li *ngFor="let workExperience of hiddenWorkExperiences">
<h3 class="deltagare-card__subheading">{{ workExperience.employer }}</h3>
<digi-typography-time [afDateTime]="workExperience.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="workExperience.dateTo"></digi-typography-time><br />
{{ workExperience.profession }}
<p>{{ workExperience.description }}</p>
</li>
</ul>
</digi-ng-layout-expansion-panel>
</ng-container>
</div>
<div class="deltagare-card__tab-column">
<h2>Utbildning</h2>
<dl>
<dt>Högsta utbildningsnivå:</dt>
<dd *ngIf="deltagare.highestEducation.level; else emptyDD">
{{ deltagare.highestEducation.level.description }}: {{ deltagare.highestEducation.sunKod.description
}}
</dd>
<h3 class="deltagare-card__subheading deltagare-card__subheading--with-margin">Utbildningar:</h3>
<ul class="deltagare-card__experience-list" *ngIf="deltagare.educations.length; else emptyText">
<li *ngFor="let education of deltagare.educations">
<h4 class="deltagare-card__subheading">{{ education.organizer }}</h4>
<digi-typography-time [afDateTime]="education.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="education.dateFrom"></digi-typography-time><br />
{{ education.education}}
<p>{{ education.description }}</p>
</li>
</ul>
</dl>
</div>
<div class="deltagare-card__tab-column">
<h2>Körkort</h2>
<dl>
<dt>Har körkort</dt>
<dd>{{deltagare.driversLicense.licenses.length ? 'Ja' : 'Nej'}}</dd>
<ng-container *ngIf="deltagare.driversLicense.licenses.length">
<dt>Körkortsklasser</dt>
<dd>{{deltagare.driversLicense.licenses.join(', ')}}</dd>
<dt>Tillgång till bil</dt>
<dd>{{deltagare.driversLicense.accessToCar ? 'Ja' : 'Nej'}}</dd>
</ng-container>
</dl>
</div>
</div>
</digi-navigation-tab>
<digi-navigation-tab af-aria-label="Känsliga uppgifter">
<div class="deltagare-card__tab-contents">
<div class="deltagare-card__tab-column">
<h2>Funktionsnedsättningar</h2>
<dl *ngIf="deltagare.disabilities.length; else emptyText">
<ng-container *ngFor="let disability of deltagare.disabilities; let index = index">
<dt>Funktionsnedsättning {{index + 1}}</dt>
<dd>
<span>{{ disability.title }}</span>
<digi-ng-popover
*ngIf="disability.description"
class="deltagare-card__popover"
[afRelativeIconSize]="true"
>{{ disability.description }}</digi-ng-popover
<digi-navigation-tab
(afOnToggle)="setActiveTab(1)"
af-aria-label="Rapportering"
af-id="deltagare-card-reports"
*ngIf="reportingTabVisible"
>
</dd>
<ng-container *ngIf="activeTab === '1'">
<msfa-deltagare-tab-reports></msfa-deltagare-tab-reports>
</ng-container>
</digi-navigation-tab>
<digi-navigation-tab
(afOnToggle)="setActiveTab(2)"
af-aria-label="Erfarenheter"
af-id="deltagare-card-experiences"
*ngIf="experiencesVisible"
>
<ng-container *ngIf="activeTab === '2'">
<msfa-deltagare-tab-experiences
*ngIf="(tab2Loading$ | async) === false; else loadingRef"
class="deltagare-card__tab-contents"
[workExperiences]="workExperiences$ | async"
[highestEducation]="highestEducation$ | async"
[driversLicense]="driversLicense$ | async"
></msfa-deltagare-tab-experiences>
</ng-container>
</digi-navigation-tab>
<digi-navigation-tab
(afOnToggle)="setActiveTab(3)"
af-aria-label="Känsliga uppgifter"
af-id="deltagare-card-sensitive-information"
*ngIf="sensitiveDataVisible"
>
<ng-container *ngIf="activeTab === '3'">
<msfa-deltagare-tab-sensitive-information
*ngIf="(tab3Loading$ | async) === false; else loadingRef"
class="deltagare-card__tab-contents"
[avropInformation]="avropInformation$ | async"
[workLanguages]="workLanguages$ | async"
[disabilities]="disabilities$ | async"
></msfa-deltagare-tab-sensitive-information>
</ng-container>
</dl>
</div>
<div class="deltagare-card__tab-column">
<h2>Språk</h2>
<dl>
<dt>Behov av tolk:</dt>
<dd>
{{deltagare.avropInformation.tolkbehov ? 'Ja (' + deltagare.avropInformation.tolkbehov + ')' : 'Nej'}}
</dd>
<dt>Behov av språkstöd:</dt>
<dd>
{{deltagare.avropInformation.sprakstod ? 'Ja (' + deltagare.avropInformation.sprakstod + ')' : 'Nej'}}
</dd>
<dt>Språk som kan användas på jobbet:</dt>
<dd *ngIf="deltagare.workLanguages.length else emptyDD">{{ deltagare.workLanguages.join(', ')}}</dd>
</dl>
</div>
</div>
</digi-navigation-tab>
</digi-navigation-tabs>
</section>
@@ -242,15 +72,5 @@
</msfa-layout>
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar deltagarinformation"></digi-ng-skeleton-base>
</ng-template>
<ng-template #emptyDD>
<dd>
<span aria-hidden="true">-</span>
<span class="msfa__a11y-sr-only">Info saknas</span>
</dd>
</ng-template>
<ng-template #emptyText>
<span aria-hidden="true">-</span>
<span class="msfa__a11y-sr-only">Info saknas</span>
<msfa-loader type="padded"></msfa-loader>
</ng-template>

View File

@@ -1,6 +1,4 @@
@import 'mixins/list';
@import 'variables/gutters';
@import 'variables/z-index';
.deltagare-card {
&__tab-contents {
@@ -9,68 +7,10 @@
margin: 0 $digi--layout--gutter--l;
}
&__tab-column {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
}
&__select-report {
max-width: var(--digi--typography--text--max-width);
}
&__cta-wrapper {
margin-top: $digi--layout--gutter--l;
margin-bottom: $digi--layout--gutter--xl;
}
dd {
margin: 0 0 1rem;
}
dt,
&__subheading {
font-size: var(--digi--typography--font-size--desktop);
font-weight: var(--digi--typography--font-weight--semibold);
margin: var(--digi--layout--gutter--s) 0 0;
&--with-margin {
font-size: var(--digi--typography--font-size--h3);
margin-bottom: var(--digi--layout--gutter--s);
}
}
&__experience-list {
@include msfa__reset-list;
}
&__accordion {
display: block;
margin-top: var(--digi--layout--gutter);
}
&__accordion-trigger {
font-weight: var(--digi--typography--font-weight--semibold);
}
&__popover {
display: inline-block;
margin-left: var(--digi--layout--gutter--s);
::ng-deep .digi-ng-popover__container {
z-index: $msfa__z-index-popover;
}
}
&__header,
&__footer {
&__header {
display: flex;
flex-direction: row-reverse;
align-items: center;
justify-content: space-between;
}
&__footer {
margin-top: $digi--layout--gutter--l;
}
}

View File

@@ -1,7 +1,6 @@
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { DeltagareCardComponent } from './deltagare-card.component';
@@ -14,7 +13,8 @@ describe('DeltagareCardComponent', () => {
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [DeltagareCardComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule, DigiNgSkeletonBaseModule, ReactiveFormsModule],
imports: [RouterTestingModule, HttpClientTestingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
})
);

View File

@@ -1,14 +1,21 @@
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { Feature } from '@msfa-enums/feature.enum';
import { IconType } from '@msfa-enums/icon-type.enum';
import { Deltagare } from '@msfa-models/deltagare.model';
import { ReportsData } from '@msfa-models/reports.model';
import { RoleEnum } from '@msfa-enums/role.enum';
import { environment } from '@msfa-environment';
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 { Role } from '@msfa-models/role.model';
import { WorkExperience } from '@msfa-models/work-experience.model';
import { DeltagareService } from '@msfa-services/api/deltagare.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { UserService } from '@msfa-services/api/user.service';
import { DeltagareCardService } from '@msfa-services/deltagare-card.service';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
@Component({
selector: 'msfa-deltagare-card',
@@ -17,85 +24,110 @@ import { map } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareCardComponent {
deltagare$: Observable<Deltagare> = this.deltagareService.deltagare$;
reportsData$: Observable<ReportsData> = this.deltagareService.reportsData$;
private _activeFeatures: Feature[] = environment.activeFeatures;
private _activeTab$ = new BehaviorSubject<string>('0');
private _userRoles: Role[] = this.userService.userRolesSnapshot;
readonly reportsFormControlName = 'reports';
reportPickerFormGroup: FormGroup = this.formBuilder.group({
// eslint-disable-next-line @typescript-eslint/unbound-method
reports: this.formBuilder.control('', [Validators.required]),
});
selectableReportTypes: Array<FormSelectItem> = [
{ name: 'Avvikelse', value: 'avvikelse' },
{ name: 'Gemensam Planering', value: 'planering' },
];
selectedReportType: FormSelectItem;
firstVisibleWorkExperiences$: Observable<WorkExperience[]> = this.deltagare$.pipe(
map(deltagare => deltagare.workExperiences.slice(0, 2))
public activeTab$: Observable<string> = this._activeTab$.asObservable();
public currentDeltagareId$: Observable<string> = this.activatedRoute.params.pipe(
map(params => params.deltagareId as string),
distinctUntilChanged(([prevDeltagareId], [currDeltagareId]) => prevDeltagareId === currDeltagareId)
);
hiddenWorkExperiences$: Observable<WorkExperience[]> = this.deltagare$.pipe(
map(deltagare => deltagare.workExperiences.slice(2))
contactInformation$: Observable<ContactInformation> = combineLatest([this.currentDeltagareId$]).pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchContactInformation$(deltagareId)),
shareReplay(1)
);
avropInformation$: Observable<Avrop> = combineLatest([this.currentDeltagareId$, this._activeTab$]).pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchAvropInformation$(deltagareId)),
shareReplay(1)
);
workExperiences$: Observable<WorkExperience[]> = this.currentDeltagareId$.pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchWorkExperiences$(deltagareId)),
shareReplay(1)
);
highestEducation$: Observable<HighestEducation> = this.currentDeltagareId$.pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchHighestEducation$(deltagareId)),
shareReplay(1)
);
educations$: Observable<Education[]> = this.currentDeltagareId$.pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchEducations$(deltagareId)),
shareReplay(1)
);
driversLicense$: Observable<DriversLicense> = this.currentDeltagareId$.pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchDriversLicense$(deltagareId)),
shareReplay(1)
);
workLanguages$: Observable<string[]> = this.currentDeltagareId$.pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchWorkLanguages$(deltagareId)),
shareReplay(1)
);
disabilities$: Observable<Disability[]> = this.currentDeltagareId$.pipe(
switchMap(([deltagareId]) => this.deltagareCardService.fetchDisabilities$(deltagareId)),
shareReplay(1)
);
tab0Loading$: Observable<boolean> = combineLatest([this.contactInformation$, this.avropInformation$]).pipe(
map(([contactInformation, avropInformation]) => !(contactInformation && avropInformation)),
startWith(true)
);
tab2Loading$: Observable<boolean> = combineLatest([
this.workExperiences$,
this.highestEducation$,
this.educations$,
this.driversLicense$,
]).pipe(
map(
([workExperiences, highestEducation, educations, driversLicense]) =>
!(workExperiences && highestEducation && educations && driversLicense)
),
startWith(true)
);
tab3Loading$: Observable<boolean> = combineLatest([
this.disabilities$,
this.avropInformation$,
this.workLanguages$,
]).pipe(
map(([disabilities, avropInformation, workLanguages]) => !(disabilities && avropInformation && workLanguages)),
startWith(true)
);
get deltagareTjanstVisible(): boolean {
return this._userRoles?.some(
role => role.type === RoleEnum.MSFA_ReportAndPlanning || role.type === RoleEnum.MSFA_ReceiveDeltagare
);
}
get reportingTabVisible(): boolean {
return (
this._activeFeatures.includes(Feature.REPORTING) &&
this._userRoles?.some(role => role.type === RoleEnum.MSFA_ReportAndPlanning)
);
}
get experiencesVisible(): boolean {
return this._userRoles?.some(role => role.type === RoleEnum.MSFA_ReportAndPlanning);
}
get sensitiveDataVisible(): boolean {
return this._userRoles?.some(role => role.type === RoleEnum.MSFA_ReportAndPlanning);
}
firstVisibleWorkExperiences$: Observable<WorkExperience[]> = this.workExperiences$.pipe(
filter(workExperiences => !!workExperiences),
map(workExperiences => workExperiences.slice(0, 2))
);
hiddenWorkExperiences$: Observable<WorkExperience[]> = this.workExperiences$.pipe(
filter(workExperiences => !!workExperiences),
map(workExperiences => workExperiences.slice(2))
);
iconType = IconType;
accordionExpanded = false;
constructor(
private activatedRoute: ActivatedRoute,
private deltagareService: DeltagareService,
private formBuilder: FormBuilder,
private router: Router
) {
this.deltagareService.setCurrentDeltagareId(this.activatedRoute.snapshot.params.deltagareId);
}
private deltagareCardService: DeltagareCardService,
private userService: UserService
) {}
get reportsFormControl(): AbstractControl | null {
return this.reportPickerFormGroup?.get(this.reportsFormControlName);
}
toggleAccordionExpanded(): void {
this.accordionExpanded = !this.accordionExpanded;
}
setNewPage(page: number): void {
this.deltagareService.setPage(page);
}
onFormSubmitted(event: Event, reportType: string, deltagareId: string): void {
event.preventDefault();
switch (reportType) {
case 'planering':
if (this.reportsFormControl.valid) {
this.router.navigate(['/deltagare/planering', deltagareId]).catch(error => {
console.error(error);
});
}
break;
case 'avvikelse':
if (this.reportsFormControl.valid) {
this.router.navigate(['/deltagare/rapportera', deltagareId]).catch(error => {
console.error(error);
});
}
break;
default:
return;
}
this.reportsFormControl.markAsTouched();
if (!this.selectableReportTypes || this.reportPickerFormGroup.invalid) {
return;
}
const selectedReportType = this.selectableReportTypes.find(report => {
return report.value === this.reportsFormControl.value;
});
this.deltagareService.setReportType(selectedReportType);
setActiveTab(tab: number): void {
this._activeTab$.next(tab.toString());
}
}

View File

@@ -1,39 +1,45 @@
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgLayoutExpansionPanelModule } from '@af/digi-ng/_layout/layout-expansion-panel';
import { DigiNgLinkButtonModule } from '@af/digi-ng/_link/link-button';
import { DigiNgLinkInternalModule } from '@af/digi-ng/_link/link-internal';
import { DigiNgPopoverModule } from '@af/digi-ng/_popover/popover';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
import { IconModule } from '@msfa-shared/components/icon/icon.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { LoaderModule } from '@msfa-shared/components/loader/loader.module';
import { DeltagareTabExperiencesComponent } from './components/deltagare-tab-experiences/deltagare-tab-experiences.component';
import { DeltagareTabPersonalInformationComponent } from './components/deltagare-tab-personal-information/deltagare-tab-personal-information.component';
import { DeltagareTabReportsComponent } from './components/deltagare-tab-reports/deltagare-tab-reports.component';
import { DeltagareTabSensitiveInformationComponent } from './components/deltagare-tab-sensitive-information/deltagare-tab-sensitive-information.component';
import { ReportsModule } from './components/reports/reports.module';
import { DeltagareCardComponent } from './deltagare-card.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareCardComponent],
declarations: [
DeltagareCardComponent,
DeltagareTabExperiencesComponent,
DeltagareTabReportsComponent,
DeltagareTabPersonalInformationComponent,
DeltagareTabSensitiveInformationComponent,
],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: DeltagareCardComponent }]),
ReactiveFormsModule,
ReportsModule,
LayoutModule,
DigiNgLinkInternalModule,
IconModule,
BackLinkModule,
DigiNgLayoutExpansionPanelModule,
HideTextModule,
DigiNgSkeletonBaseModule,
LoaderModule,
DigiNgLayoutExpansionPanelModule,
DigiNgPopoverModule,
DigiNgLinkButtonModule,
DigiNgFormSelectModule,
ReactiveFormsModule
],
exports: [DeltagareCardComponent],
})
export class DeltagareCardModule { }
export class DeltagareCardModule {}

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);
}
}

View File

@@ -5,6 +5,8 @@ export const ACTIVE_FEATURES_PROD: Feature[] = [
Feature.MY_ACCOUNT,
Feature.MY_ORGANIZATION,
Feature.ACCESSIBILITY_REPORT,
Feature.DELTAGARE,
Feature.AVROP,
];
export const ACTIVE_FEATURES_TEST: Feature[] = [
@@ -16,4 +18,5 @@ export const ACTIVE_FEATURES_TEST: Feature[] = [
Feature.RELEASES,
Feature.VERSION_INFO,
Feature.ACCESSIBILITY_REPORT,
Feature.REPORTING,
];

View File

@@ -57,6 +57,7 @@ function generateAvrop(amount = 10, deltagare, handledare) {
sparkod: track.kod,
sparNamn: track.name,
handledareCiamUserId: null,
handledare: null,
recievedTimestamp: faker.date.recent(),
hasAvbrott: currentDeltagare.hasAvbrott,
});