Merge branch 'develop' of ssh://bitbucket.arbetsformedlingen.se:7999/tea/dafa-web-monorepo into develop

This commit is contained in:
Daniel Appelgren
2021-09-09 09:01:40 +02:00
93 changed files with 978 additions and 692 deletions
@@ -1,56 +1,35 @@
import { NgModule } from '@angular/core';
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
import { RoleEnum } from '@msfa-enums/role.enum';
import { environment } from '@msfa-environment';
import { AuthGuard } from '@msfa-guards/auth.guard';
import { OrganizationGuard } from '@msfa-guards/organization.guard';
import { RoleGuard } from '@msfa-guards/role.guard';
const routes: Routes = [
{
path: '',
data: { title: '' },
loadChildren: () => import('./pages/start/start.module').then(m => m.StartModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard],
},
{
path: 'administration',
data: { title: 'Administration' },
data: { title: 'Administration', expectedRole: RoleEnum.MSFA_AuthAdmin },
loadChildren: () => import('./pages/administration/administration.module').then(m => m.AdministrationModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
},
{
path: 'deltagare',
data: { title: 'Deltagare' },
data: { title: 'Deltagare', expectedRole: RoleEnum.MSFA_ReportAndPlanning },
loadChildren: () => import('./pages/deltagare/deltagare.module').then(m => m.DeltagareModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
},
{
path: 'nya-deltagare',
data: { title: 'Nya deltagare' },
data: { title: 'Nya deltagare', expectedRole: RoleEnum.MSFA_ReceiveDeltagare },
loadChildren: () => import('./pages/avrop/avrop.module').then(m => m.AvropModule),
canActivate: [AuthGuard],
},
{
path: 'meddelanden',
data: { title: 'Meddelanden' },
loadChildren: () => import('./pages/messages/messages.module').then(m => m.MessagesModule),
canActivate: [AuthGuard],
},
{
path: 'statistik',
data: { title: 'Statistik' },
loadChildren: () => import('./pages/statistics/statistics.module').then(m => m.StatisticsModule),
canActivate: [AuthGuard],
},
{
path: 'installningar',
data: { title: 'Inställningar' },
loadChildren: () => import('./pages/settings/settings.module').then(m => m.SettingsModule),
canActivate: [AuthGuard],
},
{
path: 'releases',
data: { title: 'Releaser' },
loadChildren: () => import('./pages/releases/releases.module').then(m => m.ReleasesModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
},
{
path: 'logga-ut',
@@ -63,21 +42,36 @@ const routes: Routes = [
data: { title: 'Välj organisation' },
loadChildren: () =>
import('./pages/organization-picker/organization-picker.module').then(m => m.OrganizationPickerModule),
canActivate: [AuthGuard],
},
{
path: 'mitt-konto',
data: { title: 'Mitt konto' },
loadChildren: () => import('./pages/my-account/my-account.module').then(m => m.MyAccountModule),
canActivate: [AuthGuard, OrganizationGuard],
},
{
path: 'obehorig',
data: { title: 'Saknar behörighet' },
loadChildren: () => import('./pages/unauthorized/unauthorized.module').then(m => m.UnauthorizedModule),
canActivate: [AuthGuard],
},
];
if (!environment.production) {
routes.push({
path: 'mock-login',
data: { title: 'Mock login' },
loadChildren: () => import('./pages/mock-login/mock-login.module').then(m => m.MockLoginModule),
});
routes.push(
{
path: 'mock-login',
data: { title: 'Mock login' },
loadChildren: () => import('./pages/mock-login/mock-login.module').then(m => m.MockLoginModule),
},
{
path: 'releases',
data: { title: 'Releaser' },
loadChildren: () => import('./pages/releases/releases.module').then(m => m.ReleasesModule),
canActivate: [AuthGuard],
}
);
}
routes.push({
@@ -17,11 +17,11 @@
<a class="employee-card__edit-button" [routerLink]="['/administration/redigera-personalkonto', employee.id]"
>Redigera</a
>
<h1>{{ employee.fullName }}</h1>
<h1>Personalkonto</h1>
</header>
<p>Här kan du se och ändra personalkontots behörigheter. Ändra behörighet genom att klicka på redigera.</p>
<p>Här ser ni personalkontot. Ändra behörighet genom att klicka på redigera.</p>
<div class="employee-card__contents">
<div class="employee-card__column">
<div class="employee-card__block">
<h2>Personuppgifter</h2>
<dl class="employee-card__description-list">
<dt>Förnamn</dt>
@@ -42,58 +42,73 @@
</dd>
</dl>
</div>
<div class="employee-card__column">
<h2>Tjänst</h2>
<div class="employee-card__block">
<h2>Behörigheter</h2>
<p>Här kan du se personalkontots behörigheter.</p>
</div>
<div class="employee-card__block">
<h3>Tjänst</h3>
<ul class="employee-card__list" *ngIf="employee.tjanster.length">
<li *ngFor="let tjanst of employee.tjanster">{{ tjanst.name }}</li>
<li *ngFor="let tjanst of employee.tjanster">
<digi-icon-check-circle
class="employee-card__authorization-icon employee-card__authorization-icon--authorized"
></digi-icon-check-circle>
{{ tjanst.name }}
</li>
</ul>
<p *ngIf="!employee.tjanster.length">Kontot har inga registrerade tjänster ännu.</p>
</div>
<div class="employee-card__utforandeverksamheter">
<h2>Utförande verksamheter och utförande adresser</h2>
<div class="employee-card__block">
<h3>Utförande verksamheter och utförande adresser</h3>
<p *ngIf="employee.allaUtforandeVerksamheter; else specificUtforandeVerksamheter">
Kontot har behörighet till alla utförande verksamheter och utförande adresser inom organisationen.
</p>
<ng-template #specificUtforandeVerksamheter>
<p style="color: red">
OBS: BEHÖVER FIXAS, ÄVEN OM MAN HAR UTFÖRANDE VERKSAMHETER SÅ SYNS DOM INTE DÅ VI BARA FÅR UT ID
</p>
<div
<ul
*ngIf="employee.utforandeVerksamheter?.length; else missingUtforandeVerksamheter"
class="employee-card__utforandeverksamheter-cards"
class="employee-card__utforandeverksamheter"
>
<digi-info-card
*ngFor="let utforandeverksamhet of employee.utforandeVerksamhet"
[afHeading]="utforandeverksamhet.namn"
af-heading-level="h2"
af-type="info"
class="employee-card__utforandeverksamheter-card"
>
<digi-ng-layout-expansion-panel *ngIf="utforandeverksamhet.adresser.length > 0">
<span data-slot-trigger>
<!-- vad refererar accordionExpanded till här?? Templaten bygger inte om det inte finns en definition av variabeln.. {{ accordionExpanded ? 'Dölj' : 'Visa' }} {{utforandeverksamhet.adresser.length}} -->
{{utforandeverksamhet.adresser.length === 1 ? 'adress' : 'adresser'}}
</span>
<ul class="employee-card__utforandeverksamheter-address-list">
<li
class="employee-card__utforandeverksamheter-address-list-item"
*ngFor="let address of utforandeverksamhet.adresser"
>
<span>{{address.adressrad}}</span>
<span>{{address.postnummer}}</span>
<span>{{address.postort}}</span>
</li>
</ul>
</digi-ng-layout-expansion-panel>
</digi-info-card>
</div>
<li *ngFor="let utforandeVerksamhet of employee.utforandeVerksamheter">
<digi-info-card
[afHeading]="utforandeVerksamhet.name"
af-heading-level="h4"
af-type="info"
class="employee-card__utforandeverksamhet-card"
>
<p *ngIf="utforandeVerksamhet.allaAdresser">Alla adresser inom utförande verksamheten valda.</p>
<digi-ng-layout-expansion-panel
*ngIf="!utforandeVerksamhet.allaAdresser && utforandeVerksamhet.adresser.length > 0"
[afExpanded]="isAccordionExpanded(utforandeVerksamhet.id)"
(click)="toggleAccordionExpanded(utforandeVerksamhet.id)"
>
<span data-slot-trigger>
{{ isAccordionExpanded(utforandeVerksamhet.id) ? 'Dölj' : 'Visa' }}
{{utforandeVerksamhet.adresser.length}} {{utforandeVerksamhet.adresser.length === 1 ? 'adress' :
'adresser'}}
</span>
<ul class="employee-card__adresser">
<li *ngFor="let address of utforandeVerksamhet.adresser; let last = last">
{{address.name}}{{last ? '' : ','}}
</li>
</ul>
</digi-ng-layout-expansion-panel>
</digi-info-card>
</li>
</ul>
<ng-template #missingUtforandeVerksamheter>
<p>Kontot har inga registrerade utförande verksamheter eller utförande adresser ännu.</p>
</ng-template>
</ng-template>
</div>
<div class="employee-card__column">
<h2>Behörigheter</h2>
<div class="employee-card__block">
<h3>Roller</h3>
<p>
Här ser du användarens 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="employee-card__list">
<li *ngFor="let role of allRoles">
<digi-icon-check-circle
@@ -1,3 +1,4 @@
@import 'functions/rem';
@import 'variables/gutters';
@import 'variables/colors';
@import 'mixins/buttons';
@@ -10,39 +11,31 @@
gap: $digi--layout--gutter--l $digi--layout--gutter--l;
}
&__column {
&__block {
width: 100%;
max-width: var(--digi--typography--text--max-width);
}
&__utforandeverksamheter-cards {
&__utforandeverksamheter {
@include msfa__reset-list;
display: flex;
flex-direction: column;
gap: 1rem;
gap: $digi--layout--gutter;
}
&__utforandeverksamheter-card {
--digi-info-card--padding:
var(--digi--layout--padding--20)
var(--digi--layout--padding--40)
var(--digi--layout--padding--40)
var(--digi--layout--padding--40);
&__utforandeverksamhet-card {
--digi-info-card--padding: 1.5rem 1rem;
::ng-deep .digi-info-card__heading {
margin-top: 0;
}
}
&__utforandeverksamheter-address-list {
&__adresser {
@include msfa__reset-list;
padding-top: var(--digi--layout--padding--10);
}
&__utforandeverksamheter-address-list-item span:not(:empty):not(:last-child):after {
content: ', ';
}
&__utforandeverksamheter {
display: flex;
flex-direction: column;
}
&__header,
&__footer {
display: flex;
@@ -78,12 +71,7 @@
}
&__edit-button {
@include msfa-button-template(
$msfa-button--background--secondary,
$msfa-button--text--secondary,
$msfa-button--hover--secondary
);
width: var(--digi-button--width);
@include msfa__button('secondary');
}
&__authorization-icon {
@@ -17,6 +17,7 @@ export class EmployeeCardComponent implements OnDestroy {
employee$: Observable<Employee> = this.employeeService.employee$;
lastUpdatedEmployeeId$: Observable<string> = this.employeeService.lastUpdatedEmployeeId$;
allRoles: Role[] = this.employeeService.allRoles;
accordionsExpanded = [];
constructor(private activatedRoute: ActivatedRoute, private employeeService: EmployeeService) {
this.employeeService.setCurrentEmployeeId(this.employeeId);
@@ -34,7 +35,19 @@ export class EmployeeCardComponent implements OnDestroy {
return this._pendingSelectedParticipants$.getValue();
}
isAccordionExpanded(id: number): boolean {
return this.accordionsExpanded.includes(id);
}
closeUpdatedNotificationAlert(): void {
this.employeeService.resetLastUpdatedEmployeeId();
}
toggleAccordionExpanded(currentId: number): void {
if (this.accordionsExpanded.includes(currentId)) {
this.accordionsExpanded = this.accordionsExpanded.filter(id => id !== currentId);
} else {
this.accordionsExpanded.push(currentId);
}
}
}
@@ -3,10 +3,11 @@ import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
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 { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { RolesDialogModule } from '@msfa-shared/components/roles-dialog/roles-dialog.module';
import { LocalDatePipeModule } from '@msfa-shared/pipes/local-date/local-date.module';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { EmployeeCardComponent } from './employee-card.component';
@NgModule({
@@ -20,7 +21,8 @@ import { EmployeeCardComponent } from './employee-card.component';
DigiNgLayoutExpansionPanelModule,
LocalDatePipeModule,
HideTextModule,
BackLinkModule
BackLinkModule,
RolesDialogModule,
],
})
export class EmployeeCardModule {}
@@ -6,7 +6,6 @@
(ngSubmit)="onFormSubmitted()"
>
<digi-ng-form-input
class="edit-employee-form__input"
afId="edit-employee-form-email"
afLabel="E-post adress"
afType="email"
@@ -16,94 +15,102 @@
[afInvalid]="emailFormControl.invalid && emailFormControl.touched"
></digi-ng-form-input>
<fieldset>
<legend>Tjänster</legend>
<p>Välj de tjänster du vill ge personalen tillgång till.</p>
<digi-ng-form-select
[formControl]="tjansterFormControl"
afLabel="Välj tjänster"
[afPlaceholder]="'Välj tjänst'"
[afSelectItems]="selectableTjansterFormItems"
[afDisableValidStyle]="true"
[afInvalid]="tjansterFormControl.invalid && tjansterFormControl.touched"
(afOnChange)="toggleTjanst()"
></digi-ng-form-select>
<digi-form-validation-message
*ngIf="tjansterFormControl.invalid && tjansterFormControl.touched"
af-variation="error"
>
Du måste välja minst en tjänst
</digi-form-validation-message>
<!-- Vi får se till att bygga en kontrol för att kunna välja flera tjänster här istället, en digi-ng-select får vara en temporär lösning.. -->
</fieldset>
<div class="edit-employee-form__contents">
<div class="edit-employee-form__block">
<h2>Behörigheter</h2>
<p>Här kan du ändra personalkontots behörigheter.</p>
</div>
<fieldset>
<legend>Utförande verksamheter och adresser</legend>
<p>Välj de utförandeverksamheter och utförande adresser du vill ge personalen behörighet till.</p>
<p *ngIf="!availableUtforandeVerksamheter || availableUtforandeVerksamheter.length === 0">
<strong>Du måste välja en eller flera tjänster för att kunna välja utförande verksamheter.</strong>
</p>
<div class="edit-employee-form__choose_all-utforande-verksamh">
<div class="edit-employee-form__block">
<h3>Tjänster</h3>
<p>Välj de tjänster du vill ge personalen tillgång till.</p>
<!-- Vi får se till att bygga en kontrol för att kunna välja flera tjänster här istället, en digi-ng-select får vara en temporär lösning.. -->
<digi-ng-form-select
[formControl]="tjansterFormControl"
afLabel="Välj tjänster"
[afPlaceholder]="'Välj tjänst'"
[afSelectItems]="selectableTjansterFormItems"
[afDisableValidStyle]="true"
[afInvalid]="tjansterFormControl.invalid && tjansterFormControl.touched"
(afOnChange)="toggleTjanst()"
></digi-ng-form-select>
<digi-form-validation-message
*ngIf="tjansterFormControl.invalid && tjansterFormControl.touched"
af-variation="error"
>
Du måste välja minst en tjänst
</digi-form-validation-message>
</div>
<div class="edit-employee-form__block">
<h3>Utförande verksamheter och adresser</h3>
<p>Välj de utförandeverksamheter och utförande adresser du vill ge personalen behörighet till.</p>
<p *ngIf="!availableUtforandeVerksamheter || availableUtforandeVerksamheter.length === 0">
<strong>Du måste välja en eller flera tjänster för att kunna välja utförande verksamheter.</strong>
</p>
<digi-ng-form-checkbox
class="edit-employee-form__choose-all-utforande-verksamheter"
[formControl]="toggleAllUtforandeVerksamhetFormControl"
[afLabel]="'Välj alla utförande verksamheter och alla utförande adresser'"
(afOnChange)="toggleAllUtforandeVerksamheter($event)"
>
</digi-ng-form-checkbox>
></digi-ng-form-checkbox>
<msfa-tree-nodes-selector
*ngIf="!toggleAllUtforandeVerksamhetFormControl.value"
[headingText]="'Välj utförande verksamheter och adresser'"
[formControlName]="utforandeVerksamhetFormControlName"
[isInvalid]="utforandeVerksamhetFormControl?.invalid"
[showValidation]="utforandeVerksamhetFormControl?.touched"
[validationMessages]="utforandeVerksamhetFormControl.errors?.required ? ['Välj minst en utförande verksamhet'] : []"
(selectedTreeNodesChanged)="updateToggleAllUtforandeVerksamheter()"
></msfa-tree-nodes-selector>
</div>
<msfa-tree-nodes-selector
*ngIf="!toggleAllUtforandeVerksamhetFormControl.value"
[headingText]="'Välj utförande verksamheter och adresser'"
[formControlName]="utforandeVerksamhetFormControlName"
[isInvalid]="utforandeVerksamhetFormControl?.invalid"
[showValidation]="utforandeVerksamhetFormControl?.touched"
[validationMessages]="utforandeVerksamhetFormControl.errors?.required ? ['Välj minst en utförande verksamhet'] : []"
(selectedTreeNodesChanged)="updateToggleAllUtforandeVerksamheter()"
<div class="edit-employee-form__block" *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
<h3>Roller</h3>
<p>
Här tilldelar du specifika roller i systemet. Välj nedan vilka arbetsuppgifter som användaren ska kunna
utföra. 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>
<fieldset class="edit-employee-form__fieldset">
<legend class="msfa__a11y-sr-only">Välj roller</legend>
<ul class="edit-employee-form__roles">
<li class="edit-employee-form__role" *ngFor="let role of availableRoles">
<digi-ng-form-checkbox
[afLabel]="role.name"
[formControlName]="getFormControlName(role)"
></digi-ng-form-checkbox>
</li>
</ul>
</fieldset>
</div>
<digi-notification-alert
*ngIf="errorWhileUpdating"
af-variation="danger"
af-heading="Kunde inte spara"
af-heading-level="h4"
[afCloseable]="true"
(afOnClose)="emitCloseError()"
>
</msfa-tree-nodes-selector>
</fieldset>
<p>Personalkontot för {{employee.fullName}} kunde inte redigeras. Vänligen försök igen.</p>
<p class="msfa__small-text" *ngIf="errorWhileUpdating.message">{{errorWhileUpdating.message}}</p>
</digi-notification-alert>
<fieldset *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
<legend>Behörigheter</legend>
<p>
Här tilldelar du specifika behörigheter i systemet. Välj nedan vilka arbetsuppgifter som användaren ska kunna
utföra. Tänk på att behörigheten 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.
<digi-ng-button
class="edit-employee-form__open-roles-dialog-btn"
[afVariation]="ButtonVariation.TERTIARY"
[afType]="ButtonType.BUTTON"
[afSize]="ButtonSize.S"
afAriaControls="roles-dialog"
[afAriaLabel]="'Öppnar en dialog med information om behörigheter'"
(afOnClick)="openRolesDialog()"
>
Läs mer om behörigheter här
</digi-ng-button>
</p>
<ul class="edit-employee-form__roles">
<li class="edit-employee-form__roles-item" *ngFor="let role of availableRoles">
<digi-ng-form-checkbox
[afLabel]="role.name"
[formControlName]="getFormControlName(role)"
></digi-ng-form-checkbox>
</li>
</ul>
</fieldset>
<digi-notification-alert
*ngIf="errorWhileUpdating"
af-variation="danger"
af-heading="Kunde inte spara"
af-heading-level="h2"
[afCloseable]="true"
(afOnClose)="emitCloseError()"
>
<p>Personalkontot för {{employee.fullName}} kunde inte redigeras. Vänligen försök igen.</p>
<p class="msfa__small-text" *ngIf="errorWhileUpdating.message">{{errorWhileUpdating.message}}</p>
</digi-notification-alert>
<digi-notification-alert
*ngIf="displayPristineWarning"
af-variation="warning"
af-heading="Du har inte gjort några ändringar"
af-heading-level="h4"
[afCloseable]="true"
(afOnClose)="displayPristineWarning = false"
>
<p>Du har inte gjort några ändringar i formuläret. För att spara personalkontot behöver ändringar göras.</p>
</digi-notification-alert>
</div>
<footer class="edit-employee-form__footer">
<a
@@ -122,7 +129,7 @@
(afOnPrimaryClick)="onFormSubmitted(true)"
(afOnSecondaryClick)="abortFormSubmit()"
(afOnInactive)="abortFormSubmit()"
afHeading="Är du säker"
afHeading="Är du säker?"
afHeadingLevel="h2"
afPrimaryButtonText="Ja, spara ändå"
afSecondaryButtonText="Nej, gå tillbaka"
@@ -138,29 +145,29 @@
[afActive]="displayRolesDialog"
(afOnInactive)="closeRolesDialog()"
(afOnPrimaryClick)="closeRolesDialog()"
afHeading="Om behörigheterna i systemet"
afHeading="Om rollerna i systemet"
afHeadingLevel="h2"
afPrimaryButtonText="Stäng"
afSecondaryButtonText=""
>
<p>
Läs beskrivningarna nedan för att lära dig mer om de olika behörigheterna. Personalen kan tilldelas en behörighet,
eller flera behörigheter, beroende på vad de arbetar med. Tänk på att behörigheten endast gäller inom de utförande
verksamheter och adresser som personalen fått behörighet till.
Läs beskrivningarna nedan för att lära dig mer om de olika rollerna. Personalen kan tilldelas en roll, eller flera
roller, beroende på vad de arbetar med. Tänk på att rollen endast gäller inom de utförande verksamheter och adresser
som personalen fått behörighet till.
</p>
<p>
All personal kommer att kunna se sitt eget personalkonto, där de kan se vilka behörigheter och utförande
verksamheter och adresser som tilldelats dem i systemet. De kommer även att se startsidan.
All personal kommer att kunna se sitt eget personalkonto, där de kan se vilka roller och utförande verksamheter och
adresser som tilldelats dem i systemet. De kommer även att se startsidan.
</p>
<h3>Administrera behörigheter</h3>
<p>
Behörigheten passar personal som ska administrera behörigheter i systemet. Behörigheten bör begränsas till ett fåtal
personer och kan användas av exempelvis firmatecknare, behörighetsadministratör, eller annan person som ska kunna
administrera personalens behörigheter. Behörigheten gäller endast inom de utförande verksamheter och adresser som
getts behörighet till.
Rollen passar personal som ska administrera behörigheter i systemet. Rollen bör begränsas till ett fåtal personer
och kan användas av exempelvis firmatecknare, behörighetsadministratör, eller annan person som ska kunna
administrera personalens behörigheter. Rollen gäller endast inom de utförande verksamheter och adresser som getts
behörighet till.
</p>
<p>Behörigheten ger tillgång till följande funktioner:</p>
<p>Rollen ger tillgång till följande funktioner:</p>
<ul>
<li>Skapa nya personalkonton</li>
<li>Se personallista</li>
@@ -170,12 +177,12 @@
<h3>Ta emot nya deltagare</h3>
<p>
Behörigheten passar personal som ska se nya deltagare som inkommit i systemet och som ska tilldela handledare till
nya deltagare. Behörigheten kan exempelvis användas av samordnande roller, handledare, administratörer, eller annan
personal som ska kunna utföra dessa arbetsuppgifter. Behörigheten gäller endast inom de utförande verksamheter och
adresser som getts behörighet till.
Rollen passar personal som ska se nya deltagare som inkommit i systemet och som ska tilldela handledare till nya
deltagare. Rollen kan exempelvis användas av samordnande roller, handledare, administratörer, eller annan personal
som ska kunna utföra dessa arbetsuppgifter. Rollen gäller endast inom de utförande verksamheter och adresser som
getts behörighet till.
</p>
<p>Behörigheten ger tillgång till följande funktioner:</p>
<p>Rollen ger tillgång till följande funktioner:</p>
<ul>
<li>Se lista över nya deltagare som inkommit</li>
<li>Tilldela handledare till nya deltagare</li>
@@ -184,12 +191,12 @@
<h3>Rapportering, planering och information om deltagare</h3>
<p>
Behörigheten passar personal som arbetar nära deltagare. Behörigheten kan användas av exempelvis handledare,
coacher, studie- och yrkesvägledare, lärare eller andra roller som behöver se information om deltagare, planera
aktiviteter med deltagare eller hantera deltagares rapporter. Behörigheten gäller endast inom de utförande
verksamheter och adresser som getts behörighet till.
Rollen passar personal som arbetar nära deltagare. Rollen kan användas av exempelvis handledare, coacher, studie-
och yrkesvägledare, lärare eller andra roller som behöver se information om deltagare, planera aktiviteter med
deltagare eller hantera deltagares rapporter. Rollen gäller endast inom de utförande verksamheter och adresser som
getts behörighet till.
</p>
<p>Behörigheten ger tillgång till följande funktioner:</p>
<p>Rollen ger tillgång till följande funktioner:</p>
<ul>
<li>Se lista över deltagare</li>
@@ -1,38 +1,24 @@
@import 'mixins/buttons';
@import 'mixins/list';
@import 'variables/gutters';
@import '~@digi/core/dist/collection/components/_button/button/button.css';
.edit-employee-form {
max-width: var(--digi--typography--text--max-width);
fieldset {
margin: 2.5rem 0;
padding: 0;
border: 0 none transparent;
}
legend {
width: 100%;
padding: 0;
&__contents {
display: flex;
align-items: center;
font-weight: var(--digi--typography--font-weight--semibold);
font-size: var(--digi--typography--font-size--h2--desktop);
margin-bottom: var(--digi-typography--margin--h2);
flex-direction: column;
gap: $digi--layout--gutter--l $digi--layout--gutter--l;
}
&__service-tag {
padding-top: .5rem;
}
&__service-tag--item {
margin-right: 1rem;
}
&__input {
display: block;
&__block {
width: 100%;
margin-bottom: var(--digi--layout--gutter);
max-width: var(--digi--typography--text--max-width);
}
&__fieldset {
margin: 0;
padding: 0;
border-width: 0;
}
&__roles {
@@ -40,7 +26,7 @@
margin-bottom: var(--digi--layout--gutter);
}
&__roles-item {
&__role {
margin-top: var(--digi--layout--gutter);
::ng-deep label {
@@ -48,7 +34,7 @@
}
}
&__open-roles-dialog-btn {
&__roles-dialog-button {
::ng-deep button {
padding: 0 !important;
}
@@ -61,40 +47,11 @@
}
&__link-btn {
font-family: var(--digi-button--font-family);
background: var(--digi-button--background);
color: var(--digi-button--color);
padding: var(--digi-button--padding);
border-radius: var(--digi-button--border-radius);
border: var(--digi-button--border);
border-color: var(--digi-button--border-color);
font-weight: var(--digi-button--font-weight);
font-size: var(--digi-button--font-size);
outline: var(--digi-button--outline);
text-align: var(--digi-button--text-align);
cursor: pointer;
width: var(--digi-button--width);
display: var(--digi-button--display);
text-align: var(--digi-button--text-align);
text-decoration: none;
@include msfa__button('secondary');
}
&__link-btn--secondary {
--digi-button--background: var(--digi-button--background--secondary);
--digi-button--background--hover: var(--digi-button--background--secondary--hover);
--digi-button--color: var(--digi-button--color--secondary);
--digi-button--color--hover: var(--digi-button--color--secondary);
--digi-button--border-color--disabled: var(--digi-button--border-color--secondary--disabled);
&:focus,
&:hover {
--digi-button--background: var(--digi-button--background--hover);
--digi-button--border-color: var(--digi-button--border-color--hover);
--digi-button--color: var(--digi-button--color--hover);
--digi-button--outline: var(--digi-button--outline--focus);
}
}
&__choose_all-utforande-verksamh {
&__choose-all-utforande-verksamheter {
display: block;
margin: 1.5rem 0;
}
}
@@ -49,10 +49,6 @@ describe('EditEmployeeFormComponent', () => {
tjanster: [],
allaUtforandeVerksamheter: false,
utforandeVerksamheter: [],
tjanstCodes: [],
utforandeVerksamhetIds: [],
utforandeAdressIds: [],
};
fixture.detectChanges();
});
@@ -17,10 +17,8 @@ import { Employee } from '@msfa-models/employee.model';
import { CustomError } from '@msfa-models/error/custom-error';
import { Role } from '@msfa-models/role.model';
import { Tjanst } from '@msfa-models/tjanst.model';
import {
UtforandeVerksamhet,
UtforandeVerksamheterService,
} from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
import { UtforandeVerksamhet } from '@msfa-models/utforande-verksamhet.model';
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
import {
TreeNode,
TreeNodesSelectorService,
@@ -30,13 +28,6 @@ import { RequiredValidator } from '@msfa-utils/validators/required.validator';
import { TreeNodeValidator } from '@msfa-utils/validators/tree-node.validator';
import { EmployeeFormService } from '../services/employee-form.service';
export interface EditEmployeeFormData {
email: string;
tjanster: Tjanst[];
roles: Role[];
utforandeVerksamheter: UtforandeVerksamhet[];
}
@Component({
selector: 'msfa-edit-employee-form',
templateUrl: './edit-employee-form.component.html',
@@ -65,6 +56,7 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
editEmployeeFormGroup: FormGroup | null = null;
displayEditWithoutRolesDialog = false;
displayPristineWarning = false;
displayRolesDialog = false;
@@ -146,16 +138,22 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
}
private initializeEditEmployeeFormGroup(): void {
const currentTjanst = this.employee.tjanster[0];
const tjanstId = currentTjanst
? this.availableTjanster.find(tjanst => tjanst.code === currentTjanst.tjansteKod).tjanstId
: null;
this.editEmployeeFormGroup = new FormGroup({
email: new FormControl(this.employee.email, [RequiredValidator('E-postadress'), EmailValidator()]),
tjanster: new FormControl(this.employee.tjanster[0]?.tjanstId, [RequiredValidator('Tjänst')]),
tjanster: new FormControl(tjanstId, [RequiredValidator('Tjänst')]),
roles: this.employeeFormService.getRolesFormGroup(this.availableRoles, this.employee.roles),
utforandeVerksamheter: new FormControl(
this.utforandeVerksamheterService.getTreeNodeDataFromUtforandeVerksamheter(this.availableUtforandeVerksamheter),
[
TreeNodeValidator.IsValidTreeNode(
this.utforandeVerksamheterService.hasSelectedUtforandeVerksamhet,
'required'
'required',
this.toggleAllUtforandeVerksamhetFormControl
),
]
),
@@ -180,7 +178,12 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
this.editEmployeeFormGroup.markAllAsTouched();
if (this.editEmployeeFormGroup.invalid || this.editEmployeeFormGroup.pristine) {
if (this.editEmployeeFormGroup.invalid) {
return;
}
if (this.editEmployeeFormGroup.pristine) {
this.displayPristineWarning = true;
return;
}
@@ -207,7 +210,6 @@ export class EditEmployeeFormComponent implements OnInit, OnChanges {
this.utforandeVerksamhetFormControl?.value
),
allaUtforandeVerksamheter: !!this.toggleAllUtforandeVerksamhetFormControl.value,
utforandeVerksamhetIds: [],
});
}
@@ -37,6 +37,7 @@
</div>
<div class="employee-form__block">
<msfa-edit-employee-form
*ngIf="employee && (tjanster$ | async)"
[employee]="employee"
[availableRoles]="availableRoles"
[availableTjanster]="tjanster$ | async"
@@ -10,7 +10,6 @@
&__block {
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
&__input {
@@ -5,12 +5,10 @@ import { Employee } from '@msfa-models/employee.model';
import { CustomError } from '@msfa-models/error/custom-error';
import { Role } from '@msfa-models/role.model';
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 {
UtforandeVerksamhet,
UtforandeVerksamheterService,
} from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
@@ -29,7 +27,7 @@ export class EmployeeFormComponent implements OnInit {
tjanster$: Observable<Tjanst[]> = this.tjanstService.tjanster$;
availableUtforandeVerksamheter$: Observable<UtforandeVerksamhet[]> = this._selectedTjanstIds$.pipe(
filter(selectedTjanstIds => !!selectedTjanstIds?.length),
switchMap(selectedTjanstIds => this.utforandeVerksamheterService.getUtforandeVerksamheter(selectedTjanstIds))
switchMap(selectedTjanstIds => this.utforandeVerksamheterService.fetchUtforandeVerksamheter$(selectedTjanstIds))
);
availableRoles: Role[] = this.employeeService.allRoles;
@@ -12,6 +12,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { RolesDialogModule } from '@msfa-shared/components/roles-dialog/roles-dialog.module';
import { TreeNodesSelectorModule } from '@msfa-shared/components/tree-nodes-selector/tree-nodes-selector.module';
import { LocalDatePipeModule } from '@msfa-shared/pipes/local-date/local-date.module';
import { EmployeeDeleteModule } from '../../components/employee-delete/employee-delete.module';
@@ -38,6 +39,7 @@ import { EmployeeFormComponent } from './employee-form.component';
DigiNgDialogModule,
HideTextModule,
TreeNodesSelectorModule,
RolesDialogModule,
],
})
export class EmployeeFormModule {}
@@ -2,7 +2,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 } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { SortOrder } from '@msfa-enums/sort-order.enum';
import { EmployeeCompactResponse } from '@msfa-models/api/employee.response.model';
@@ -13,7 +12,6 @@ import { employeesMock } from './employees-list.mock';
describe('EmployeesListComponent', () => {
let component: EmployeesListComponent;
let fixture: ComponentFixture<EmployeesListComponent>;
const getEmployeeRows = () => fixture.debugElement.queryAll(By.css('.employees-list__row'));
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -20,11 +20,11 @@
<h2>Personallista</h2>
<form class="employees__search-wrapper" (ngSubmit)="setSearchFilter()">
<!-- <digi-form-input-search
af-label="Sök personal"
af-label-description="Sök på namn"
<digi-form-input-search
af-label="Sök personalens namn"
(afOnInput)="setSearchValue($event)"
></digi-form-input-search> -->
></digi-form-input-search>
<digi-form-checkbox
class="employees__only-employees-without-authorization"
af-label="Visa endast personal utan behörigheter"
@@ -12,6 +12,7 @@
&__search-wrapper {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
@@ -33,15 +33,15 @@ export class AvropFiltersComponent {
this.avropService.setSelectedKommuner(filterOptions);
}
removeKommun(kommunToRemove: MultiselectFilterOption) {
removeKommun(kommunToRemove: MultiselectFilterOption): void {
this.avropService.removeKommun(kommunToRemove);
}
removeUtforandeVerksamhet(utforandeVerksamhetToRemove: MultiselectFilterOption) {
removeUtforandeVerksamhet(utforandeVerksamhetToRemove: MultiselectFilterOption): void {
this.avropService.removeUtforandeVerksamhet(utforandeVerksamhetToRemove);
}
removeTjanst(tjanstToRemove: MultiselectFilterOption) {
removeTjanst(tjanstToRemove: MultiselectFilterOption): void {
this.avropService.removeTjanst(tjanstToRemove);
}
}
@@ -1,3 +0,0 @@
<msfa-layout>
<section class="messages">Meddelanden funkar!</section>
</msfa-layout>
@@ -1,29 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { MessagesComponent } from './messages.component';
describe('MessagesComponent', () => {
let component: MessagesComponent;
let fixture: ComponentFixture<MessagesComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [MessagesComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(MessagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,9 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessagesComponent {}
@@ -1,11 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { MessagesComponent } from './messages.component';
@NgModule({
declarations: [MessagesComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: MessagesComponent }]), LayoutModule],
})
export class MessagesModule {}
@@ -1,9 +1,29 @@
<msfa-layout>
<digi-typography>
<section class="my-account">
<h1>Mitt konto</h1>
<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>
</header>
<digi-ng-link-internal afText="Logga ut" afRoute="/logga-ut"></digi-ng-link-internal>
<main *ngIf="user$ | async as user; else loadingRef">
<p>Här kan du se dina uppgifter.</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>
</main>
</section>
</digi-typography>
</msfa-layout>
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar kontoinformation"></digi-ng-skeleton-base>
</ng-template>
@@ -0,0 +1,32 @@
@import 'mixins/buttons';
@import 'mixins/list';
@import 'variables/gutters';
.my-account {
&__header {
display: flex;
justify-content: space-between;
align-items: center;
}
&__logout {
@include msfa__button('secondary');
}
&__roles {
@include msfa__reset-list;
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--s;
}
&__role {
display: flex;
align-items: center;
gap: $digi--layout--gutter--s;
}
&__authorization-icon {
color: var(--digi--ui--color--border--success);
}
}
@@ -1,5 +1,7 @@
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { MyAccountComponent } from './my-account.component';
describe('MyAccountComponent', () => {
@@ -9,8 +11,9 @@ describe('MyAccountComponent', () => {
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [MyAccountComponent],
imports: [HttpClientTestingModule, RouterTestingModule],
}).compileComponents();
})
);
@@ -1,4 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum';
import { User } from '@msfa-models/user.model';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
@Component({
selector: 'msfa-my-account',
@@ -6,4 +10,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
styleUrls: ['./my-account.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyAccountComponent {}
export class MyAccountComponent {
user$: Observable<User> = this.userService.user$;
readonly IconType = IconType;
constructor(private userService: UserService) {}
}
@@ -1,7 +1,8 @@
import { DigiNgLinkInternalModule } from '@af/digi-ng/_link/link-internal';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { CommonModule } from '@angular/common';
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 { MyAccountComponent } from './my-account.component';
@@ -12,7 +13,8 @@ import { MyAccountComponent } from './my-account.component';
CommonModule,
RouterModule.forChild([{ path: '', component: MyAccountComponent }]),
LayoutModule,
DigiNgLinkInternalModule,
IconModule,
DigiNgSkeletonBaseModule,
],
})
export class MyAccountModule {}
@@ -5,7 +5,7 @@ import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { Organization } from '@msfa-models/organization.model';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { filter } from 'rxjs/operators';
@Component({
selector: 'msfa-organization-picker',
@@ -14,9 +14,8 @@ import { filter, map } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OrganizationPickerComponent extends UnsubscribeDirective {
organizations$: Observable<Organization[]> = this.userService.user$.pipe(
filter(user => !!(user && user.organizations?.length)),
map(({ organizations }) => organizations)
organizations$: Observable<Organization[]> = this.userService.organizations$.pipe(
filter(organizations => !!organizations?.length)
);
constructor(
@@ -1,12 +1,9 @@
<msfa-layout>
<msfa-layout [showBreadCrumbs]="false">
<digi-typography>
<section class="page-not-found">
<h1>Oj då! Vi kan inte hitta sidan.</h1>
<p>Det kan bero på att länken du använder är felaktig eller att sidan inte längre finns.</p>
<a class="msfa__link msfa__link--with-icon msfa__link--ignore-visited" routerLink="/">
<digi-icon-arrow-left class="msfa__digi-icon"></digi-icon-arrow-left>
Gå tillbaka till startsidan
</a>
<msfa-back-link route="/">Tillbaka till startsidan</msfa-back-link>
</section>
</digi-typography>
</msfa-layout>
@@ -1,12 +1,18 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { PageNotFoundComponent } from './page-not-found.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [PageNotFoundComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: PageNotFoundComponent }]), LayoutModule],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: PageNotFoundComponent }]),
LayoutModule,
BackLinkModule,
],
})
export class PageNotFoundModule {}
@@ -1,3 +0,0 @@
<msfa-layout>
<section class="settings">Inställningar funkar!</section>
</msfa-layout>
@@ -1,29 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [SettingsComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,9 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SettingsComponent {}
@@ -1,11 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { SettingsComponent } from './settings.component';
@NgModule({
declarations: [SettingsComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: SettingsComponent }]), LayoutModule],
})
export class SettingsModule {}
@@ -1,3 +0,0 @@
<msfa-layout>
<section class="statistics">Statistik funkar!</section>
</msfa-layout>
@@ -1,29 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { StatisticsComponent } from './statistics.component';
describe('StatisticsComponent', () => {
let component: StatisticsComponent;
let fixture: ComponentFixture<StatisticsComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [StatisticsComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(StatisticsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,9 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-statistics',
templateUrl: './statistics.component.html',
styleUrls: ['./statistics.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StatisticsComponent {}
@@ -1,11 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { StatisticsComponent } from './statistics.component';
@NgModule({
declarations: [StatisticsComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: StatisticsComponent }]), LayoutModule],
})
export class StatisticsModule {}
@@ -0,0 +1,12 @@
<msfa-layout>
<digi-typography>
<section class="unauthorized">
<h1>Du saknar behörigheter!</h1>
<p>
Det verkar som att du saknar behörigheter att komma in på sidan. Kontakta verksamhetens
behörighetsadministratör.
</p>
<msfa-back-link route="/">Tillbaka till startsidan</msfa-back-link>
</section>
</digi-typography>
</msfa-layout>
@@ -0,0 +1,29 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { UnauthorizedComponent } from './unauthorized.component';
describe('UnauthorizedComponent', () => {
let component: UnauthorizedComponent;
let fixture: ComponentFixture<UnauthorizedComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [UnauthorizedComponent],
imports: [RouterTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(UnauthorizedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-unauthorized',
templateUrl: './unauthorized.component.html',
styleUrls: ['./unauthorized.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UnauthorizedComponent {}
@@ -0,0 +1,18 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { UnauthorizedComponent } from './unauthorized.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [UnauthorizedComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: UnauthorizedComponent }]),
LayoutModule,
BackLinkModule,
],
})
export class UnauthorizedModule {}
@@ -1,11 +1,5 @@
.back-link {
display: inline-flex;
align-items: center;
text-decoration: none;
font-weight: var(--digi--typography--font-weight--semibold);
gap: var(--digi--layout--gutter--xs);
@import 'mixins/link';
&:hover {
text-decoration: underline;
}
.back-link {
@include msfa__link(true);
}
@@ -25,6 +25,26 @@
></path>
</svg>
<svg
*ngIf="icon === iconType.BUILDING"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
width="25"
height="25"
>
<path
d="M436 480h-20V24c0-13.255-10.745-24-24-24H56C42.745 0 32 10.745 32 24v456H12c-6.627 0-12 5.373-12 12v20h448v-20c0-6.627-5.373-12-12-12zM128 76c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12V76zm0 96c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12v-40zm52 148h-40c-6.627 0-12-5.373-12-12v-40c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40c0 6.627-5.373 12-12 12zm76 160h-64v-84c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v84zm64-172c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12v-40c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40zm0-96c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12v-40c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40zm0-96c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12V76c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40z"
fill="currentColor"
></path>
</svg>
<svg *ngIf="icon === iconType.LOGOUT" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="25" height="25">
<path
d="M497 273L329 441c-15 15-41 4.5-41-17v-96H152c-13.3 0-24-10.7-24-24v-96c0-13.3 10.7-24 24-24h136V88c0-21.4 25.9-32 41-17l168 168c9.3 9.4 9.3 24.6 0 34zM192 436v-40c0-6.6-5.4-12-12-12H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h84c6.6 0 12-5.4 12-12V76c0-6.6-5.4-12-12-12H96c-53 0-96 43-96 96v192c0 53 43 96 96 96h84c6.6 0 12-5.4 12-12z"
fill="currentColor"
></path>
</svg>
<svg
*ngIf="icon === iconType.CLIPBOARD"
xmlns="http://www.w3.org/2000/svg"
@@ -2,7 +2,14 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/c
import { IconSize } from '@msfa-enums/icon-size.enum';
import { IconType } from '@msfa-enums/icon-type.enum';
const CUSTOM_ICONS: IconType[] = [IconType.HOME, IconType.SETTINGS, IconType.PLUS, IconType.CLIPBOARD];
const CUSTOM_ICONS: IconType[] = [
IconType.HOME,
IconType.SETTINGS,
IconType.PLUS,
IconType.CLIPBOARD,
IconType.BUILDING,
IconType.LOGOUT,
];
@Component({
selector: 'msfa-icon',
@@ -1,7 +1,5 @@
<footer class="footer">
<div class="footer__logo-wrapper">
<a class="footer__logo-link" href="/">
<img class="footer__logo" src="/assets/logo/arbetsformedlingen-light.svg" alt="Arbetsförmedlingen" />
</a>
<digi-logo af-color="secondary"></digi-logo>
</div>
</footer>
@@ -1,15 +1,12 @@
@import 'variables/gutters';
.footer {
background-color: var(--digi--ui--color--background--profile);
padding: var(--digi--layout--gutter);
padding: $digi--layout--gutter--l $digi--layout--gutter;
&__logo-wrapper {
height: 100%;
display: flex;
align-items: center;
}
&__logo {
height: 2rem;
vertical-align: middle;
::ng-deep .digi-logo {
--digi-logo--padding: 0;
}
}
}
@@ -1,9 +1,10 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { FooterComponent } from './footer.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [FooterComponent],
imports: [CommonModule, RouterModule],
exports: [FooterComponent],
@@ -1,16 +1,20 @@
<div class="navigation">
<div class="navigation__logo-wrapper">
<a [routerLink]="['/']" aria-label="Till startsidan för FA Mina sidor">
<a class="navigation__logo-link" [routerLink]="['/']" aria-label="Till startsidan för FA Mina sidor">
<digi-logo af-system-name="Mina sidor för fristående aktörer" af-color="secondary"></digi-logo>
</a>
</div>
<ul class="navigation__list msfa__hide-on-print">
<li *ngIf="user" class="navigation__item">
<ul class="navigation__list msfa__hide-on-print" *ngIf="user">
<li class="navigation__item">
<a routerLink="/mitt-konto" class="navigation__link">
<msfa-icon [icon]="iconType.USER" size="l"></msfa-icon>
<span class="navigation__text">{{ user.fullName }}</span>
</a>
</li>
<li *ngIf="selectedOrganization" class="navigation__item navigation__item--without-link">
<msfa-icon [icon]="iconType.BUILDING" size="l"></msfa-icon>
<span class="navigation__text">{{ selectedOrganization.name }}</span>
</li>
<!-- <li class="navigation__item">
<a routerLink="/" class="navigation__link">
<msfa-icon [icon]="iconType.BELL" size="l"></msfa-icon>
@@ -24,6 +24,14 @@
align-items: center;
}
&__logo-link {
text-decoration: none;
::ng-deep .digi-logo {
--digi-logo--padding: 0;
}
}
&__logo {
height: $msfa__navigation-height / 2.5;
vertical-align: middle;
@@ -37,16 +45,15 @@
@include msfa__reset-list;
display: flex;
height: 100%;
gap: $digi--layout--gutter--l;
gap: $digi--layout--gutter;
color: var(--digi--typography--color--text--light);
margin-right: var(--digi--layout--gutter);
}
&__item {
display: flex;
}
&__user,
&__item--without-link,
&__link {
display: flex;
align-items: center;
@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum';
import { Organization } from '@msfa-models/organization.model';
import { User } from '@msfa-models/user.model';
@Component({
@@ -10,5 +11,6 @@ import { User } from '@msfa-models/user.model';
})
export class NavigationComponent {
@Input() user: User;
@Input() selectedOrganization: Organization;
iconType = IconType;
}
@@ -11,20 +11,20 @@
Hem
</a>
</li>
<li class="sidebar__item">
<li class="sidebar__item" *ngIf="isReceiveDeltagare">
<a [routerLink]="['/nya-deltagare']" [routerLinkActive]="['sidebar__link--active']" class="sidebar__link">
<msfa-icon class="sidebar__icon" [icon]="iconType.CLIPBOARD" size="xl"></msfa-icon>
Nya deltagare
</a>
</li>
<li class="sidebar__item">
<li class="sidebar__item" *ngIf="isReportAndPlanning">
<a [routerLink]="['/deltagare']" [routerLinkActive]="['sidebar__link--active']" class="sidebar__link">
<msfa-icon class="sidebar__icon" [icon]="iconType.SOK_KANDIDAT" size="xl"></msfa-icon>
Deltagarlista
</a>
</li>
<li class="sidebar__item">
<li class="sidebar__item" *ngIf="isAuthAdmin">
<a [routerLink]="['/administration']" [routerLinkActive]="['sidebar__link--active']" class="sidebar__link">
<msfa-icon class="sidebar__icon" [icon]="iconType.SETTINGS" size="xl"></msfa-icon>
Administration
@@ -1,5 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum';
import { RoleEnum } from '@msfa-enums/role.enum';
import { Role } from '@msfa-models/role.model';
@Component({
selector: 'msfa-sidebar',
@@ -8,5 +10,16 @@ import { IconType } from '@msfa-enums/icon-type.enum';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SidebarComponent {
@Input() userRoles: Role[];
iconType = IconType;
get isAuthAdmin(): boolean {
return this.userRoles?.some(role => role.type === RoleEnum.MSFA_AuthAdmin);
}
get isReceiveDeltagare(): boolean {
return this.userRoles?.some(role => role.type === RoleEnum.MSFA_ReceiveDeltagare);
}
get isReportAndPlanning(): boolean {
return this.userRoles?.some(role => role.type === RoleEnum.MSFA_ReportAndPlanning);
}
}
@@ -2,12 +2,13 @@
<msfa-skip-to-content mainContentId="msfa-main-content"></msfa-skip-to-content>
<header class="msfa__header">
<msfa-navigation [user]="user$ | async"></msfa-navigation>
<msfa-navigation [user]="user$ | async" [selectedOrganization]="selectedOrganization$ | async"></msfa-navigation>
</header>
<msfa-sidebar class="msfa__sidebar"></msfa-sidebar>
<msfa-sidebar class="msfa__sidebar" [userRoles]="roles$ | async"></msfa-sidebar>
<main id="msfa-main-content" class="msfa__content">
<digi-ng-navigation-breadcrumbs
*ngIf="showBreadCrumbs"
class="msfa__breadcrumbs"
[afItems]="breadcrumbsItems"
></digi-ng-navigation-breadcrumbs>
@@ -43,6 +43,5 @@
&__footer {
grid-area: footer;
background-color: var(--digi--ui--color--primary);
min-height: 10rem;
}
}
@@ -1,14 +1,16 @@
import { NavigationBreadcrumbsItem } from '@af/digi-ng/_navigation/navigation-breadcrumbs';
import { ChangeDetectionStrategy, Component } from '@angular/core';
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 { Organization } from '@msfa-models/organization.model';
import { Role } from '@msfa-models/role.model';
import { User } from '@msfa-models/user.model';
import { AuthenticationService } from '@msfa-services/api/authentication.service';
import { UserService } from '@msfa-services/api/user.service';
import { mapPathsToBreadcrumbs } from '@msfa-utils/map-paths-to-breadcrumbs.util';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { filter, map } from 'rxjs/operators';
@Component({
selector: 'msfa-layout',
@@ -17,15 +19,18 @@ import { filter, switchMap } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LayoutComponent extends UnsubscribeDirective {
private startBreadcrumb: NavigationBreadcrumbsItem = {
@Input() showBreadCrumbs = true;
private readonly _startBreadcrumb: NavigationBreadcrumbsItem = {
text: 'Start',
routerLink: '/',
};
private _breadcrumbsItems$ = new BehaviorSubject<NavigationBreadcrumbsItem[]>([this.startBreadcrumb]);
private _breadcrumbsItems$ = new BehaviorSubject<NavigationBreadcrumbsItem[]>([this._startBreadcrumb]);
isLoggedIn$: Observable<boolean> = this.authenticationService.isLoggedIn$;
user$: Observable<User> = this.isLoggedIn$.pipe(
filter(loggedIn => !!loggedIn),
switchMap(() => this.userService.user$)
selectedOrganization$: Observable<Organization> = this.userService.selectedOrganization$;
user$: Observable<User> = this.userService.user$;
roles$: Observable<Role[]> = this.user$.pipe(
filter(user => !!user),
map(user => user.roles)
);
get breadcrumbsItems(): NavigationBreadcrumbsItem[] {
@@ -62,7 +67,7 @@ export class LayoutComponent extends UnsubscribeDirective {
.toString()
.split('/')
.filter(path => !!path);
this._breadcrumbsItems$.next(mapPathsToBreadcrumbs(paths, this.startBreadcrumb));
this._breadcrumbsItems$.next(mapPathsToBreadcrumbs(paths, this._startBreadcrumb));
})
);
}
@@ -0,0 +1,79 @@
<button
class="roles-dialog-button"
type="button"
(click)="openRolesDialog()"
aria-controls="roles-dialog"
aria-label="Öppnar en dialog med information om behörigheter"
>
{{buttonText}}
</button>
<digi-ng-dialog
id="roles-dialog"
class="roles-dialog"
[afActive]="displayRolesDialog"
(afOnInactive)="closeRolesDialog()"
(afOnPrimaryClick)="closeRolesDialog()"
afHeading="Om rollerna i systemet"
afHeadingLevel="h2"
afPrimaryButtonText="Stäng"
afSecondaryButtonText=""
>
<p>
Läs beskrivningarna nedan för att lära dig mer om de olika rollerna. Personalen kan tilldelas en roll, eller flera
roller, beroende på vad de arbetar med. Tänk på att rollen endast gäller inom de utförande verksamheter och adresser
som personalen fått behörighet till.
</p>
<p>
All personal kommer att kunna se sitt eget personalkonto, där de kan se vilka roller och utförande verksamheter och
adresser som tilldelats dem i systemet. De kommer även att se startsidan.
</p>
<h3>Administrera behörigheter</h3>
<p>
Rollen passar personal som ska administrera behörigheter i systemet. Rollen bör begränsas till ett fåtal personer
och kan användas av exempelvis firmatecknare, behörighetsadministratör, eller annan person som ska kunna
administrera personalens behörigheter. Rollen gäller endast inom de utförande verksamheter och adresser som getts
behörighet till.
</p>
<p>Rollen ger tillgång till följande funktioner:</p>
<ul>
<li>Skapa nya personalkonton</li>
<li>Se personallista</li>
<li>Se och ändra personalkonto och dess behörigheter</li>
<li>Ta bort personalkonton</li>
</ul>
<h3>Ta emot nya deltagare</h3>
<p>
Rollen passar personal som ska se nya deltagare som inkommit i systemet och som ska tilldela handledare till nya
deltagare. Rollen kan exempelvis användas av samordnande roller, handledare, administratörer, eller annan personal
som ska kunna utföra dessa arbetsuppgifter. Rollen gäller endast inom de utförande verksamheter och adresser som
getts behörighet till.
</p>
<p>Rollen ger tillgång till följande funktioner:</p>
<ul>
<li>Se lista över nya deltagare som inkommit</li>
<li>Tilldela handledare till nya deltagare</li>
<li>Ta bort nya deltagare där beslut om avbrott skett innan tjänsten startat</li>
</ul>
<h3>Rapportering, planering och information om deltagare</h3>
<p>
Rollen passar personal som arbetar nära deltagare. Rollen kan användas av exempelvis handledare, coacher, studie-
och yrkesvägledare, lärare eller andra roller som behöver se information om deltagare, planera aktiviteter med
deltagare eller hantera deltagares rapporter. Rollen gäller endast inom de utförande verksamheter och adresser som
getts behörighet till.
</p>
<p>Rollen ger tillgång till följande funktioner:</p>
<ul>
<li>Se lista över deltagare</li>
<li>Se information om deltagare</li>
<li>Planera aktiviteter i en gemensam planering</li>
<li>Skicka och se avvikelserapporter</li>
<li>Skicka och se resultatrapporter</li>
<li>Skicka och se slutredovisningar</li>
<li>Skicka och se informativa rapporter</li>
</ul>
</digi-ng-dialog>
@@ -0,0 +1,9 @@
@import 'mixins/link';
.roles-dialog {
position: absolute;
}
.roles-dialog-button {
@include msfa__button-as-link(true);
}
@@ -0,0 +1,26 @@
/* tslint:disable:no-unused-variable */
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RolesDialogComponent } from './roles-dialog.component';
describe('RolesDialogComponent', () => {
let component: RolesDialogComponent;
let fixture: ComponentFixture<RolesDialogComponent>;
beforeEach(async(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [RolesDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RolesDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'msfa-roles-dialog',
templateUrl: './roles-dialog.component.html',
styleUrls: ['./roles-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RolesDialogComponent {
@Input() buttonText = 'Läs mer om roller här';
displayRolesDialog = false;
openRolesDialog(): void {
this.displayRolesDialog = true;
}
closeRolesDialog(): void {
this.displayRolesDialog = false;
}
}
@@ -0,0 +1,12 @@
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RolesDialogComponent } from './roles-dialog.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [RolesDialogComponent],
imports: [CommonModule, DigiNgDialogModule],
exports: [RolesDialogComponent],
})
export class RolesDialogModule {}
@@ -7,4 +7,5 @@ export const NAVIGATION = {
'planera-aktiviteter': 'Planera aktiviteter',
'mitt-konto': 'Mitt konto',
'skapa-personalkonto': 'Skapa personalkonto',
obehorig: 'Saknar behörigheter',
};
@@ -3,6 +3,8 @@ export enum IconType {
SETTINGS = 'settings', // Custom
PLUS = 'plus', // Custom
CLIPBOARD = 'clipboard', // Custom
BUILDING = 'building', // Custom
LOGOUT = 'logout', // Custom
USER = 'user',
USERS = 'users',
BELL = 'bell',
@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { environment } from '@msfa-environment';
import { AuthenticationService } from '@msfa-services/api/authentication.service';
import { UserService } from '@msfa-services/api/user.service';
import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
@@ -10,24 +9,13 @@ import { map, switchMap } from 'rxjs/operators';
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(
private authenticationService: AuthenticationService,
private router: Router,
private userService: UserService
) {}
constructor(private authenticationService: AuthenticationService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.authenticationService.isLoggedIn$.pipe(
switchMap(loggedIn => {
if (loggedIn) {
return this.userService.selectedOrganization$.pipe(
map(organization => {
if (!organization) {
void this.router.navigateByUrl(`/organization-picker`);
}
return true;
})
);
return of(true);
} else if (route.queryParams.code) {
return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result));
}
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class OrganizationGuard implements CanActivate {
constructor(private router: Router, private userService: UserService) {}
canActivate(): Observable<boolean> {
return this.userService.selectedOrganization$.pipe(
map(organization => {
if (!organization) {
void this.router.navigateByUrl(`/organization-picker`);
}
return true;
})
);
}
}
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { RoleEnum } from '@msfa-enums/role.enum';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
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;
return this.userService.user$.pipe(
filter(user => !!user),
map(({ roles }) => {
const userHasRole = roles.some(role => role.type === expectedRole);
if (userHasRole) {
return true;
}
void this.router.navigateByUrl('/obehorig');
})
);
}
}
@@ -0,0 +1,4 @@
export interface EmployeeAdressResponse {
id: number;
namn: string;
}
@@ -5,6 +5,5 @@ export interface EmployeeEditRequest {
roles: RoleEnum[];
tjanstIds: number[];
allaUtforandeVerksamheter: boolean;
utforandeVerksamhetIds?: number[];
adressIds: number[];
}
@@ -0,0 +1,4 @@
export interface EmployeeTjanstResponse {
namn: string;
tjansteKod: string;
}
@@ -0,0 +1,8 @@
import { EmployeeAdressResponse } from './employee-adress.response.model';
export interface EmployeeUtforandeVerksamhetResponse {
id: number;
namn: string;
allaAdresser: boolean;
adresser: EmployeeAdressResponse[];
}
@@ -1,18 +1,7 @@
import { RoleEnum } from '@msfa-enums/role.enum';
import { PaginationMeta } from '@msfa-models/pagination-meta.model';
import { TjanstResponse } from './tjanst.response.model';
interface UtforandeVerksamhetResponse {
id: number;
name: string;
allaAdresser: boolean;
adresser?: AdressResponse[];
}
interface AdressResponse {
id: number;
name: string;
}
import { EmployeeTjanstResponse } from './employee-tjanst.response.model';
import { EmployeeUtforandeVerksamhetResponse } from './employee-utforande-verksamhet.response.model';
export interface EmployeeCompactResponse {
ciamUserId: string;
@@ -29,14 +18,9 @@ export interface EmployeeResponse {
email: string;
ssn: string;
roles: RoleEnum[];
tjanster: TjanstResponse[];
tjanster: EmployeeTjanstResponse[];
allaUtforandeVerksamheter: boolean;
utforandeVerksamheter: UtforandeVerksamhetResponse[];
// Will be removed
tjansteKoder: string[];
utforandeVerksamhetIds: number[];
adressIds: number[];
utforandeVerksamheter: EmployeeUtforandeVerksamhetResponse[];
}
export interface EmployeesDataResponse {
@@ -1,6 +1,8 @@
import { RoleEnum } from '@msfa-enums/role.enum';
export interface UserInfoResponse {
id: string;
firstName: string;
lastName: string;
roles: string[];
roles: RoleEnum[];
}
@@ -0,0 +1,4 @@
export interface UtforandeAdressResponse {
id: number;
namn: string;
}
@@ -0,0 +1,7 @@
import { UtforandeAdressResponse } from './utforande-adress.response.model';
export interface UtforandeVerksamhetResponse {
id: number;
name: string;
adresser: UtforandeAdressResponse[];
}
@@ -0,0 +1,15 @@
import { EmployeeAdressResponse } from './api/employee-adress.response.model';
export interface EmployeeAdress {
id: number;
name: string;
}
export function mapResponseToEmployeeAdress(data: EmployeeAdressResponse): EmployeeAdress {
const { id, namn } = data;
return {
id,
name: namn,
};
}
@@ -0,0 +1,15 @@
import { EmployeeTjanstResponse } from './api/employee-tjanst.response.model';
export interface EmployeeTjanst {
name: string;
tjansteKod: string;
}
export function mapResponseToEmployeeTjanst(data: EmployeeTjanstResponse): EmployeeTjanst {
const { namn, tjansteKod } = data;
return {
name: namn,
tjansteKod,
};
}
@@ -0,0 +1,22 @@
import { EmployeeUtforandeVerksamhetResponse } from './api/employee-utforande-verksamhet.response.model';
import { EmployeeAdress, mapResponseToEmployeeAdress } from './employee-adress.model';
export interface EmployeeUtforandeVerksamhet {
id: number;
name: string;
allaAdresser: boolean;
adresser: EmployeeAdress[];
}
export function mapResponseToEmployeeUtforandeVerksamhet(
data: EmployeeUtforandeVerksamhetResponse
): EmployeeUtforandeVerksamhet {
const { id, namn, allaAdresser, adresser } = data;
return {
id,
name: namn,
allaAdresser,
adresser: adresser.map(adress => mapResponseToEmployeeAdress(adress)),
};
}
@@ -1,22 +1,14 @@
import { RoleEnum } from '@msfa-enums/role.enum';
import { EmployeeCompactResponse, EmployeeResponse } from './api/employee.response.model';
import { EmployeeTjanst, mapResponseToEmployeeTjanst } from './employee-tjanst.model';
import {
EmployeeUtforandeVerksamhet,
mapResponseToEmployeeUtforandeVerksamhet,
} from './employee-utforande-verksamhet.model';
import { PaginationMeta } from './pagination-meta.model';
import { mapResponseToTjanst, Tjanst } from './tjanst.model';
const CURRENT_YEAR = new Date().getFullYear().toString().slice(2, 4);
interface UtforandeVerksamhet {
id: number;
name: string;
allaAdresser: boolean;
adresser?: Adress[];
}
interface Adress {
id: number;
name: string;
}
export interface EmployeeCompact {
id: string;
fullName: string;
@@ -33,13 +25,9 @@ export interface Employee {
email: string;
ssn: string;
roles: RoleEnum[];
tjanster: Tjanst[];
tjanster: EmployeeTjanst[];
allaUtforandeVerksamheter: boolean;
utforandeVerksamheter: UtforandeVerksamhet[];
tjanstCodes: string[];
utforandeVerksamhetIds: number[];
utforandeAdressIds: number[];
utforandeVerksamheter: EmployeeUtforandeVerksamhet[];
}
export interface EmployeesData {
@@ -81,9 +69,6 @@ export function mapResponseToEmployee(data: EmployeeResponse): Employee {
tjanster,
allaUtforandeVerksamheter,
utforandeVerksamheter,
tjansteKoder,
utforandeVerksamhetIds,
adressIds,
} = data;
return {
id: ciamUserId,
@@ -93,11 +78,10 @@ export function mapResponseToEmployee(data: EmployeeResponse): Employee {
email,
ssn: ssn ? mapResponseToSsn(ssn) : null,
roles: roles || [],
tjanster: tjanster?.map(tjanst => mapResponseToTjanst(tjanst)),
tjanster: tjanster?.map(tjanst => mapResponseToEmployeeTjanst(tjanst)),
allaUtforandeVerksamheter,
utforandeVerksamheter,
tjanstCodes: tjansteKoder || [],
utforandeVerksamhetIds: utforandeVerksamhetIds || [],
utforandeAdressIds: adressIds || [],
utforandeVerksamheter: utforandeVerksamheter.map(utforandeVerksamhet =>
mapResponseToEmployeeUtforandeVerksamhet(utforandeVerksamhet)
),
};
}
@@ -1,12 +1,11 @@
import { Tjanst } from "./tjanst.model";
import { Tjanst } from './tjanst.model';
export interface FormTagData {
tjanstekod: string,
name: string;
tjanstekod: string;
name: string;
}
export function mapTjanstToFormTag(tjanstData: Tjanst): FormTagData {
const { name, code } = tjanstData;
return { tjanstekod: code, name}
}
const { name, code } = tjanstData;
return { tjanstekod: code, name };
}
@@ -1,11 +1,12 @@
import { UserInfoResponse } from './api/user-info.response.model';
import { mapResponseToRoles, Role } from './role.model';
export interface UserInfo {
id: string;
firstName: string;
lastName: string;
fullName: string;
roles: string[];
roles: Role[];
}
export function mapResponseToUserInfo(data: UserInfoResponse): UserInfo {
@@ -16,6 +17,6 @@ export function mapResponseToUserInfo(data: UserInfoResponse): UserInfo {
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
roles,
roles: mapResponseToRoles(roles),
};
}
@@ -1,6 +1,7 @@
import { OrganizationResponse } from './api/organization.response.model';
import { UserInfoResponse } from './api/user-info.response.model';
import { mapResponseToOrganization, Organization } from './organization.model';
import { mapResponseToRoles } from './role.model';
import { UserInfo } from './user-info.model';
export interface User extends UserInfo {
@@ -15,7 +16,7 @@ export function mapUserApiResponseToUser(userInfo: UserInfoResponse, organizatio
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
roles,
roles: mapResponseToRoles(roles),
organizations: organizations ? organizations.map(organization => mapResponseToOrganization(organization)) : [],
};
}
@@ -0,0 +1,15 @@
import { UtforandeAdressResponse } from './api/utforande-adress.response.model';
export interface UtforandeAdress {
id: number;
name: string;
}
export function mapResponseToUtforandeAdress(data: UtforandeAdressResponse): UtforandeAdress {
const { id, namn } = data;
return {
id,
name: namn,
};
}
@@ -0,0 +1,18 @@
import { UtforandeVerksamhetResponse } from './api/utforande-verksamhet.response.model';
import { mapResponseToUtforandeAdress, UtforandeAdress } from './utforande-adress.model';
export interface UtforandeVerksamhet {
id: number;
name: string;
adresser: UtforandeAdress[];
}
export function mapResponseToUtforandeVerksamhet(data: UtforandeVerksamhetResponse): UtforandeVerksamhet {
const { id, name, adresser } = data;
return {
id,
name,
adresser: adresser.map(adress => mapResponseToUtforandeAdress(adress)),
};
}
@@ -24,7 +24,6 @@ import { Sort } from '@msfa-models/sort.model';
import { ErrorService } from '@msfa-services/error.service';
import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs';
import { catchError, distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
import { TjanstService } from './tjanst.service';
@Injectable({
providedIn: 'root',
@@ -48,11 +47,7 @@ export class EmployeeService extends UnsubscribeDirective {
private _employeeToDelete$ = new BehaviorSubject<Employee>(null);
public employeeToDelete$: Observable<Employee> = this._employeeToDelete$.asObservable();
constructor(
private httpClient: HttpClient,
private errorService: ErrorService,
private tjanstService: TjanstService
) {
constructor(private httpClient: HttpClient, private errorService: ErrorService) {
super();
super.unsubscribeOnDestroy(
combineLatest([this._currentEmployeeId$, this._lastUpdatedEmployeeId$])
@@ -63,20 +58,7 @@ export class EmployeeService extends UnsubscribeDirective {
!currLastUpdatedEmployeeId && prevEmployeeId === currEmployeeId
),
switchMap(([currentEmployeeId]) =>
combineLatest([this._fetchEmployee$(currentEmployeeId), this.tjanstService.tjanster$]).pipe(
filter(([employee, allTjanster]) => !!(employee && allTjanster?.length)),
map(([employee, allTjanster]) => {
const tjanster = [];
employee.tjanstCodes?.forEach(code => {
const currentTjanst = allTjanster.find(tjanst => tjanst.code === code);
if (currentTjanst) {
tjanster.push(currentTjanst);
}
});
return { ...employee, tjanster };
})
)
this._fetchEmployee$(currentEmployeeId).pipe(filter(employee => !!employee))
)
)
.subscribe(employee => {
@@ -17,23 +17,34 @@ import { AuthenticationService } from './authentication.service';
})
export class UserService extends UnsubscribeDirective {
private _apiBaseUrl = `${environment.api.url}/auth`;
private _isLoggedIn$: Observable<boolean> = this.authenticationService.isLoggedIn$;
private _organizations$ = new BehaviorSubject<Organization[]>(null);
public organizations$: Observable<Organization[]> = this._organizations$.asObservable();
private _user$ = new BehaviorSubject<User>(null);
public user$: Observable<User> = this._user$.asObservable();
private _selectedOrganizationNumber$ = new BehaviorSubject<string>(null);
constructor(private httpClient: HttpClient, private authenticationService: AuthenticationService) {
super();
this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);
super.unsubscribeOnDestroy(
this.authenticationService.isLoggedIn$
this._isLoggedIn$
.pipe(
filter(loggedIn => !!loggedIn),
switchMap(() => combineLatest([this._fetchUserInfo$(), this._fetchOrganizations$()]))
switchMap(() => this._fetchOrganizations$())
)
.subscribe(([userInfo, organizations]) => {
this._user$.next({ ...userInfo, organizations });
.subscribe(organizations => {
this._organizations$.next(organizations);
}),
combineLatest([this._isLoggedIn$, this.selectedOrganization$])
.pipe(
filter(([loggedIn, selectedOrganization]) => !!(loggedIn && selectedOrganization)),
switchMap(() => this._fetchUserInfo$())
)
.subscribe(userInfo => {
this._user$.next({ ...userInfo, organizations: this._organizations$.value });
})
);
this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);
}
private _fetchOrganizations$(): Observable<Organization[]> {
@@ -55,11 +66,11 @@ export class UserService extends UnsubscribeDirective {
}
public get selectedOrganization$(): Observable<Organization | null> {
return combineLatest([this._selectedOrganizationNumber$, this._user$]).pipe(
filter(([, user]) => !!user),
map(([organizationNumber, user]) => {
return combineLatest([this._selectedOrganizationNumber$, this._organizations$]).pipe(
filter(([, organizations]) => !!organizations?.length),
map(([organizationNumber, organizations]) => {
return organizationNumber
? user.organizations.find(organization => organization.organizationNumber === organizationNumber)
? organizations.find(organization => organization.organizationNumber === organizationNumber)
: null;
})
);
@@ -2,39 +2,34 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { Params } from '@msfa-models/api/params.model';
import { UtforandeVerksamhetResponse } from '@msfa-models/api/utforande-verksamhet.response.model';
import { UtforandeAdress } from '@msfa-models/utforande-adress.model';
import { mapResponseToUtforandeVerksamhet, UtforandeVerksamhet } from '@msfa-models/utforande-verksamhet.model';
import {
TreeNode,
TreeNodesSelectorService,
} from '@msfa-shared/components/tree-nodes-selector/services/tree-nodes-selector.service';
import { Observable, of } from 'rxjs';
export interface UtforandeVerksamhetAdress {
id: number;
namn: string;
}
export interface UtforandeVerksamhet {
id: number;
name: string;
adresser: Array<UtforandeVerksamhetAdress>;
}
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class UtforandeVerksamheterService {
private readonly apiBaseUrl = `${environment.api.url}/utforandeverksamheter`;
private readonly _apiBaseUrl = `${environment.api.url}/utforandeverksamheter`;
constructor(private treeNodesSelectorService: TreeNodesSelectorService, private httpClient: HttpClient) {}
getUtforandeVerksamheter(tjanstIds: Array<number>): Observable<Array<UtforandeVerksamhet>> {
fetchUtforandeVerksamheter$(tjanstIds: number[]): Observable<UtforandeVerksamhet[]> {
if (!tjanstIds.length) {
return of<Array<UtforandeVerksamhet>>([]);
return of<UtforandeVerksamhet[]>([]);
}
const params: Params = { tjansteIds: tjanstIds.map(tjanstId => tjanstId.toString()) };
return this.httpClient.get<Array<UtforandeVerksamhet>>(`${this.apiBaseUrl}`, { params });
return this.httpClient
.get<{ data: UtforandeVerksamhetResponse[] }>(`${this._apiBaseUrl}`, { params })
.pipe(map(({ data }) => data.map(uv => mapResponseToUtforandeVerksamhet(uv))));
}
getSelectedAdressIdsFromTreeNode(treeNode: TreeNode): number[] {
@@ -43,7 +38,7 @@ export class UtforandeVerksamheterService {
return selectedUtforandeVerksamheter.map(uv => uv.adresser.map(adress => adress.id)).flat();
}
getTreeNodeDataFromUtforandeVerksamheter(utforandeVerksamhetList: Array<UtforandeVerksamhet>): TreeNode | null {
getTreeNodeDataFromUtforandeVerksamheter(utforandeVerksamhetList: UtforandeVerksamhet[]): TreeNode | null {
let treeNode: TreeNode | null = null;
if (!utforandeVerksamhetList || utforandeVerksamhetList.length === 0 || !Array.isArray(utforandeVerksamhetList)) {
@@ -67,7 +62,7 @@ export class UtforandeVerksamheterService {
childItemType: 'Adresser',
children: utforandeVerksamhet.adresser
? utforandeVerksamhet.adresser.map(adress => {
return { label: adress.namn, isSelected: false, value: adress };
return { label: adress.name, isSelected: false, value: adress };
})
: [],
};
@@ -80,8 +75,8 @@ export class UtforandeVerksamheterService {
return treeNode;
}
getSelectedUtforandeVerksamheterFromTreeNode(treeNode: TreeNode): Array<UtforandeVerksamhet> {
let utforandeVerksamhetList: Array<UtforandeVerksamhet> = [];
getSelectedUtforandeVerksamheterFromTreeNode(treeNode: TreeNode): UtforandeVerksamhet[] {
let utforandeVerksamhetList: UtforandeVerksamhet[] = [];
if (!treeNode || !treeNode.children) {
return utforandeVerksamhetList;
@@ -95,7 +90,7 @@ export class UtforandeVerksamheterService {
name: originalUtforandeVerksamhet?.name,
id: originalUtforandeVerksamhet?.id,
adresser: utforandeVerksamhetNode.children.map(adressNode => {
return adressNode.value as UtforandeVerksamhetAdress;
return adressNode.value as UtforandeAdress;
}),
};
return utforandeVerksamhet;
@@ -4,14 +4,15 @@ import { TreeNode } from '@msfa-shared/components/tree-nodes-selector/services/t
export class TreeNodeValidator {
static IsValidTreeNode(
validationFn: (treeNode: TreeNode | null | undefined) => boolean,
nameOfError: string
nameOfError: string,
allUtforandeVerksamheterFormControl: AbstractControl
): ValidatorFn {
return (control: AbstractControl): { [key: string]: unknown } => {
const isValid = validationFn(control.value);
const validationObj = {};
if (isValid) {
if (isValid || allUtforandeVerksamheterFormControl?.value) {
return null;
}
@@ -1,31 +1,46 @@
@import './variables/colors';
@import '~@digi/core/dist/collection/components/_button/button/button.css';
//Button properties
$msfa-button--padding: var(--digi-button--padding);
$msfa-button--margin: 0.5rem;
$msfa-button--border-radius: var(--digi-button--border-radius);
$msfa-button--transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
$msfa-button--border: var(--digi-button--border);
$msfa-button--text-decoration: none;
$msfa-button--font-weight: var(--digi-button--font-weight);
$msfa-button--font-font-size: var(--digi-button--font-size);
//A basic link button
@mixin msfa-button-template($backgroundcolor, $textcolor, $hovercolor) {
background: $backgroundcolor;
padding: $msfa-button--padding;
margin: $msfa-button--margin;
border-radius: $msfa-button--border-radius;
transition: $msfa-button--transition;
border: $msfa-button--border;
@mixin msfa__button($type: 'primary') {
padding: var(--digi-button--padding);
border-radius: var(--digi-button--border-radius);
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
text-decoration: none;
font-weight: $msfa-button--font-weight;
font-size: $msfa-button--font-font-size;
color: $textcolor;
font-weight: var(--digi-button--font-weight);
font-size: var(--digi-button--font-size);
width: var(--digi-button--width);
display: flex;
align-items: center;
gap: 0.3rem;
text-align: var(--digi-button--text-align);
border: var(--digi-button--border);
outline: var(--digi-button--outline);
border-color: var(--digi-button--border-color);
&:hover {
background: $hovercolor;
@if $type == 'secondary' {
background-color: var(--digi-button--background--secondary);
color: var(--digi-button--color--secondary);
} @else if $type == 'tertiary' {
background-color: transparent;
color: var(--digi-button--color--tertiary);
border-width: 0;
} @else {
background-color: var(--digi-button--background);
color: var(--digi-button--color);
}
&:hover,
&:focus {
outline: var(--digi-button--outline--focus);
border-color: var(--digi-button--border-color--hover);
@if $type == 'secondary' {
background-color: var(--digi-button--background--secondary--hover);
color: var(--digi-button--color--secondary--hover);
} @else if $type == 'tertiary' {
color: var(--digi-button--color--tertiary--hover);
} @else {
background-color: var(--digi-button--background--hover);
color: var(--digi-button--color--hover);
}
}
}
@@ -0,0 +1,30 @@
@mixin msfa__link($ignore-visited: false) {
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--digi--typography--color--link);
font-size: var(--digi--typography--font-size);
font-weight: var(--digi--typography--font-weight--semibold);
gap: var(--digi--layout--gutter--xs);
&:hover {
text-decoration: underline;
}
@if $ignore-visited {
&:visited {
color: var(--digi--typography--color--link);
&:hover {
color: var(--digi--typography--color--link--active);
}
}
}
}
@mixin msfa__button-as-link($ignore-visited: false) {
@include msfa__link($ignore-visited);
background-color: transparent;
border-width: 0;
padding: 0;
}
+3 -17
View File
@@ -1,6 +1,7 @@
@import '@digi/core/dist/digi/digi.css';
@import 'mixins/a11y';
@import 'mixins/icon';
@import 'mixins/link';
@keyframes spinning {
from {
@@ -82,25 +83,10 @@ dl {
}
&__link {
display: inline-flex;
align-items: center;
text-decoration: none;
font-weight: var(--digi--typography--font-weight--semibold);
&:hover {
text-decoration: underline;
}
&--with-icon {
gap: var(--digi--layout--gutter--xs);
}
@include msfa__link(false);
&--ignore-visited:visited {
color: var(--digi--typography--color--link);
&:hover {
color: var(--digi--typography--color--link--active);
}
@include msfa__link(true);
}
}
}
@@ -1,15 +1,7 @@
@import '~@digi/styles/src/ui/variables/ui__variables';
@import '~@digi/core/dist/collection/components/_button/button/button.css';
// AF DIGI Variables
$digi--ui--color--primary-light: lighten($digi--ui--color--primary, 10%);
$digi--ui--color--primary: $digi--ui--color--stratos;
// Local variables
$msfa-button--background--primary: var(--digi-button--background);
$msfa-button--text--primary: var(--digi--typography--color--text--light);
$msfa-button--hover--primary: var(--digi-button--background--hover);
$msfa-button--background--secondary: var(--digi-button--background--secondary);
$msfa-button--text--secondary: var(--digi--ui--color--primary);
$msfa-button--hover--secondary: var(--digi-button--background--secondary--hover);