Added my-account information and possibility to switch organizations

This commit is contained in:
Erik Tiekstra
2021-09-09 09:04:17 +02:00
parent 0dbd471bce
commit 74b0630905
20 changed files with 260 additions and 87 deletions

View File

@@ -84,6 +84,7 @@ routes.push({
const options: ExtraOptions = {
useHash: false,
scrollPositionRestoration: 'enabled',
onSameUrlNavigation: 'reload',
};
@NgModule({

View File

@@ -23,7 +23,7 @@
<div class="employee-card__contents">
<div class="employee-card__block">
<h2>Personuppgifter</h2>
<dl class="employee-card__description-list">
<dl>
<dt>Förnamn</dt>
<dd *ngIf="employee.firstName; else emptyDD">{{ employee.firstName }}</dd>
<dt>Efternamn</dt>

View File

@@ -12,7 +12,6 @@
}
&__block {
width: 100%;
max-width: var(--digi--typography--text--max-width);
}
@@ -48,19 +47,6 @@
margin-top: $digi--layout--gutter--l;
}
&__description-list {
dd {
margin: 0;
}
dt {
font-weight: var(--digi--typography--font-weight--semibold);
}
dd {
margin-bottom: 0.5rem;
}
}
&__list {
@include msfa__reset-list;

View File

@@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { Employee } from '@msfa-models/employee.model';
import { Role } from '@msfa-models/role.model';
import { EmployeeService } from '@msfa-services/api/employee.service';
import { RoleService } from '@msfa-services/role.service';
import { BehaviorSubject, Observable } from 'rxjs';
@Component({
@@ -16,10 +17,14 @@ export class EmployeeCardComponent implements OnDestroy {
private _pendingSelectedParticipants$ = new BehaviorSubject<string[]>([]);
employee$: Observable<Employee> = this.employeeService.employee$;
lastUpdatedEmployeeId$: Observable<string> = this.employeeService.lastUpdatedEmployeeId$;
allRoles: Role[] = this.employeeService.allRoles;
allRoles: Role[] = this.roleService.allRoles;
accordionsExpanded = [];
constructor(private activatedRoute: ActivatedRoute, private employeeService: EmployeeService) {
constructor(
private activatedRoute: ActivatedRoute,
private employeeService: EmployeeService,
private roleService: RoleService
) {
this.employeeService.setCurrentEmployeeId(this.employeeId);
}

View File

@@ -11,7 +11,6 @@
}
&__block {
width: 100%;
max-width: var(--digi--typography--text--max-width);
}

View File

@@ -8,6 +8,7 @@ import { Tjanst } from '@msfa-models/tjanst.model';
import { UtforandeVerksamhet } from '@msfa-models/utforande-verksamhet.model';
import { EmployeeService } from '@msfa-services/api/employee.service';
import { TjanstService } from '@msfa-services/api/tjanst.service';
import { RoleService } from '@msfa-services/role.service';
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
@@ -30,10 +31,11 @@ export class EmployeeFormComponent implements OnInit {
switchMap(selectedTjanstIds => this.utforandeVerksamheterService.fetchUtforandeVerksamheter$(selectedTjanstIds))
);
availableRoles: Role[] = this.employeeService.allRoles;
availableRoles: Role[] = this.roleService.allRoles;
constructor(
private employeeService: EmployeeService,
private roleService: RoleService,
private tjanstService: TjanstService,
private utforandeVerksamheterService: UtforandeVerksamheterService,
private activatedRoute: ActivatedRoute,

View File

@@ -2,23 +2,94 @@
<digi-typography>
<section class="my-account">
<header class="my-account__header">
<h1>Mitt konto</h1>
<a class="my-account__logout" routerLink="/logga-ut">
<msfa-icon [icon]="IconType.LOGOUT"></msfa-icon>
Logga ut
</a>
<div class="my-account__heading-wrapper">
<h1>Mitt konto</h1>
<a class="my-account__logout" routerLink="/logga-ut">
<msfa-icon [icon]="IconType.LOGOUT"></msfa-icon>
Logga ut
</a>
</div>
<p>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ad nulla cumque pariatur quia eveniet, reprehenderit
voluptate dicta aspernatur ipsa quam, dolor reiciendis et dolores consectetur laborum voluptatem quis impedit
aliquam.
</p>
</header>
<main class="my-account__contents" *ngIf="user$ | async as user; else loadingRef">
<div class="my-account__block">
<h2>Personuppgifter</h2>
<dl>
<dt>Förnamn</dt>
<dd>
<ng-container *ngIf="user.firstName; else emptyText">{{ user.firstName }}</ng-container>
</dd>
<dt>Efternamn</dt>
<dd>
<ng-container *ngIf="user.lastName; else emptyText">{{ user.lastName }}</ng-container>
</dd>
<dt>Personnummer</dt>
<dd>
<msfa-hide-text
*ngIf="user.ssn; else emptyText"
symbols="********-****"
[changingText]="user.ssn"
ariaLabelType="personnummer"
></msfa-hide-text>
</dd>
<dt>E-postadress</dt>
<dd>
<a *ngIf="user.email; else emptyText" href="mailto:{{user.email}}">{{user.email}}</a>
</dd>
</dl>
</div>
<main *ngIf="user$ | async as user; else loadingRef">
<p>Här kan du se dina uppgifter.</p>
<div class="my-account__block" *ngIf="selectedOrganization$ | async as selectedOrganization">
<h2>Organisation</h2>
<p>
Du nuvarande organisation är: {{selectedOrganization.name}}, {{selectedOrganization.organizationNumber}}.
</p>
<ng-container *ngIf="otherOrganizations$ | async as otherOrganizations">
<p>Du tillhör flera organisationer. Du kan byta organisation i formuläret.</p>
<h2>Mina roller</h2>
<ul class="my-account__roles">
<li class="my-account__role" *ngFor="let role of user.roles">
<digi-icon-check-circle class="msfa__digi-icon my-account__authorization-icon"></digi-icon-check-circle>
{{role.name}}
</li>
</ul>
<msfa-organization-picker-form
[compact]="true"
[organizations]="organizations$ | async"
[selectedOrganization]="selectedOrganization"
label="Byt organisation"
submitText="Byt organisation"
(selectedOrganizationChanged)="loginWithOrganization($event)"
></msfa-organization-picker-form>
</ng-container>
</div>
<div class="my-account__block">
<h2>Behörigheter</h2>
<p>Här kan du se dina behörigheter i systemet.</p>
</div>
<div class="my-account__block">
<h3>Roller</h3>
<p>
Här ser du dina specifika roller i systemet. Tänk på att rollen i systemet är begränsad till de utförande
verksamheter och adresser som användaren hör till. Användaren kan därför endast utföra uppgifter och se
information inom den/de utförande adresser som tilldelats användaren.
<msfa-roles-dialog></msfa-roles-dialog>.
</p>
<ul class="my-account__list">
<li class="my-account__list-item" *ngFor="let role of allRoles">
<digi-icon-check-circle
*ngIf="userHasRole(user.roles, role); else unauthorized"
class="msfa__digi-icon my-account__authorization-icon my-account__authorization-icon--authorized"
></digi-icon-check-circle>
<ng-template #unauthorized>
<digi-icon-x-button
class="msfa__digi-icon my-account__authorization-icon my-account__authorization-icon--unauthorized"
></digi-icon-x-button>
</ng-template>
{{role.name}}
</li>
</ul>
</div>
</main>
</section>
</digi-typography>
@@ -27,3 +98,8 @@
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar kontoinformation"></digi-ng-skeleton-base>
</ng-template>
<ng-template #emptyText>
<span aria-hidden="true">-</span>
<span class="msfa__a11y-sr-only">Info saknas</span>
</ng-template>

View File

@@ -3,7 +3,7 @@
@import 'variables/gutters';
.my-account {
&__header {
&__heading-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
@@ -13,20 +13,37 @@
@include msfa__button('secondary');
}
&__roles {
&__contents {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
margin-top: $digi--layout--gutter--l;
}
&__block {
max-width: var(--digi--typography--text--max-width);
}
&__list {
@include msfa__reset-list;
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--s;
}
&__role {
&__list-item {
display: flex;
align-items: center;
gap: $digi--layout--gutter--s;
}
&__authorization-icon {
color: var(--digi--ui--color--border--success);
&--authorized {
color: var(--digi--ui--color--border--success);
}
&--unauthorized {
color: var(--digi--ui--color--border--error);
}
}
}

View File

@@ -1,8 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { IconType } from '@msfa-enums/icon-type.enum';
import { Organization } from '@msfa-models/organization.model';
import { Role } from '@msfa-models/role.model';
import { User } from '@msfa-models/user.model';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
import { RoleService } from '@msfa-services/role.service';
import { combineLatest, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Component({
selector: 'msfa-my-account',
@@ -12,7 +17,27 @@ import { Observable } from 'rxjs';
})
export class MyAccountComponent {
user$: Observable<User> = this.userService.user$;
selectedOrganization$: Observable<Organization> = this.userService.selectedOrganization$;
organizations$: Observable<Organization[]> = this.userService.organizations$;
otherOrganizations$: Observable<Organization[]> = combineLatest([
this.organizations$,
this.selectedOrganization$,
]).pipe(
filter(([organizations, selectedOrganization]) => !!(organizations?.length && selectedOrganization)),
map(([organizations, selectedOrganization]) =>
organizations.filter(organization => organization.organizationNumber !== selectedOrganization.organizationNumber)
)
);
allRoles: Role[] = this.roleService.allRoles;
readonly IconType = IconType;
constructor(private userService: UserService) {}
constructor(private userService: UserService, private roleService: RoleService, private router: Router) {}
userHasRole(userRoles: Role[], currentRole: Role): boolean {
return userRoles.some(role => role.type === currentRole.type);
}
loginWithOrganization(organization: Organization): void {
this.userService.setSelectedOrganization(organization);
}
}

View File

@@ -4,6 +4,8 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { IconModule } from '@msfa-shared/components/icon/icon.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { OrganizationPickerFormModule } from '@msfa-shared/components/organization-picker-form/organization-picker-form.module';
import { RolesDialogModule } from '@msfa-shared/components/roles-dialog/roles-dialog.module';
import { MyAccountComponent } from './my-account.component';
@NgModule({
@@ -15,6 +17,8 @@ import { MyAccountComponent } from './my-account.component';
LayoutModule,
IconModule,
DigiNgSkeletonBaseModule,
OrganizationPickerFormModule,
RolesDialogModule,
],
})
export class MyAccountModule {}

View File

@@ -1,25 +0,0 @@
<form
class="organization-picker-form"
*ngIf="organizationPickerFormGroup"
[formGroup]="organizationPickerFormGroup"
(ngSubmit)="onFormSubmitted()"
>
<div class="organization-picker-form__content">
<digi-ng-form-select
[formControlName]="organizationFormControlName"
[afLabel]="'Välj organisation'"
[afPlaceholder]="'Välj organisation'"
[afSelectItems]="selectableOrganizations"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="organizationFormControl.invalid && organizationFormControl.touched"
></digi-ng-form-select>
<digi-form-validation-message
af-variation="error"
*ngIf="organizationFormControl.invalid && organizationFormControl.touched"
>
Du måste välja en organisation för att kunna logga in
</digi-form-validation-message>
</div>
<digi-button af-type="submit">Logga in</digi-button>
</form>

View File

@@ -1,5 +0,0 @@
@import 'variables/gutters';
.organization-picker-form__content {
margin-bottom: $digi--layout--gutter--l;
}

View File

@@ -1,14 +1,12 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { OrganizationPickerFormModule } from '@msfa-shared/components/organization-picker-form/organization-picker-form.module';
import { OrganizationPickerRoutingModule } from './organization-picker-routing.module';
import { OrganizationPickerComponent } from './organization-picker.component';
import { OrganizationPickerFormComponent } from './organization-picker-form/organization-picker-form.component';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [OrganizationPickerComponent, OrganizationPickerFormComponent],
imports: [CommonModule, OrganizationPickerRoutingModule, ReactiveFormsModule, DigiNgFormSelectModule],
declarations: [OrganizationPickerComponent],
imports: [CommonModule, OrganizationPickerRoutingModule, OrganizationPickerFormModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class OrganizationPickerModule {}

View File

@@ -0,0 +1,28 @@
<form
class="organization-picker-form"
[ngClass]="{'organization-picker-form--compact': compact}"
*ngIf="organizationPickerFormGroup"
[formGroup]="organizationPickerFormGroup"
(ngSubmit)="onFormSubmitted()"
>
<digi-ng-form-select
class="organization-picker-form__select"
[formControl]="organizationFormControl"
[afLabel]="label"
[afPlaceholder]="label"
[afSelectItems]="selectableOrganizations"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="organizationFormControl.invalid && organizationFormControl.touched"
></digi-ng-form-select>
<digi-button class="organization-picker-form__submit" af-type="submit">{{submitText}}</digi-button>
<digi-form-validation-message
class="organization-picker-form__error"
af-variation="error"
*ngIf="organizationFormControl.invalid && organizationFormControl.touched"
>
Du måste välja en organisation för att kunna logga in
</digi-form-validation-message>
</form>

View File

@@ -0,0 +1,40 @@
@import 'variables/gutters';
.organization-picker-form {
display: grid;
grid-template-columns: auto 1fr;
column-gap: $digi--layout--gutter;
row-gap: $digi--layout--gutter--s;
grid-template-areas:
'select select'
'error error'
'submit .';
&--compact {
grid-template-areas:
'select submit'
'error error';
.organization-picker-form__submit {
margin-top: 0;
}
}
&__select {
grid-area: select;
}
&__submit {
grid-area: submit;
margin-top: $digi--layout--gutter--l;
align-self: end;
}
&__error {
grid-area: error;
}
::ng-deep .digi-ng-form-select__footer {
display: none !important;
}
}

View File

@@ -1,13 +1,13 @@
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
import {
Component,
OnInit,
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
SimpleChanges,
OnInit,
Output,
EventEmitter,
SimpleChanges,
} from '@angular/core';
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { Organization } from '@msfa-models/organization.model';
@@ -20,6 +20,10 @@ import { Organization } from '@msfa-models/organization.model';
})
export class OrganizationPickerFormComponent implements OnInit, OnChanges {
@Input() organizations: Array<Organization> | null = null;
@Input() selectedOrganization: Organization;
@Input() label = 'Välj organisation';
@Input() submitText = 'Logga in';
@Input() compact = false;
@Output() selectedOrganizationChanged = new EventEmitter<Organization>();
readonly organizationFormControlName = 'organization';
@@ -44,8 +48,7 @@ export class OrganizationPickerFormComponent implements OnInit, OnChanges {
private setupOrganizationPickerFormGroup(): void {
this.organizationPickerFormGroup = new FormGroup({
// eslint-disable-next-line
organization: new FormControl(null, [Validators.required]),
organization: new FormControl(this.selectedOrganization?.organizationNumber || null, [Validators.required]),
});
}

View File

@@ -0,0 +1,13 @@
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { OrganizationPickerFormComponent } from './organization-picker-form.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [OrganizationPickerFormComponent],
imports: [CommonModule, ReactiveFormsModule, DigiNgFormSelectModule],
exports: [OrganizationPickerFormComponent],
})
export class OrganizationPickerFormModule {}

View File

@@ -1,7 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { RoleEnum } from '@msfa-enums/role.enum';
import { SortOrder } from '@msfa-enums/sort-order.enum';
import { environment } from '@msfa-environment';
import { EmployeeEditRequest } from '@msfa-models/api/employee-edit.request.model';
@@ -19,7 +18,6 @@ import {
mapResponseToEmployeeCompact,
} from '@msfa-models/employee.model';
import { errorToCustomError } from '@msfa-models/error/custom-error';
import { mapResponseToRoles, Role } from '@msfa-models/role.model';
import { Sort } from '@msfa-models/sort.model';
import { ErrorService } from '@msfa-services/error.service';
import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs';
@@ -192,8 +190,4 @@ export class EmployeeService extends UnsubscribeDirective {
})
);
}
public get allRoles(): Role[] {
return mapResponseToRoles(Object.keys(RoleEnum) as RoleEnum[]);
}
}

View File

@@ -0,0 +1,12 @@
import { Injectable } from '@angular/core';
import { RoleEnum } from '@msfa-enums/role.enum';
import { mapResponseToRoles, Role } from '@msfa-models/role.model';
@Injectable({
providedIn: 'root',
})
export class RoleService {
public get allRoles(): Role[] {
return mapResponseToRoles(Object.keys(RoleEnum) as RoleEnum[]);
}
}