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

View File

@@ -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({
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({

View File

@@ -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"
>
<li *ngFor="let utforandeVerksamhet of employee.utforandeVerksamheter">
<digi-info-card
*ngFor="let utforandeverksamhet of employee.utforandeVerksamhet"
[afHeading]="utforandeverksamhet.namn"
af-heading-level="h2"
[afHeading]="utforandeVerksamhet.name"
af-heading-level="h4"
af-type="info"
class="employee-card__utforandeverksamheter-card"
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)"
>
<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'}}
{{ isAccordionExpanded(utforandeVerksamhet.id) ? '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>
<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>
</div>
</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

View File

@@ -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 {

View File

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

View File

@@ -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 {}

View File

@@ -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,9 +15,16 @@
[afInvalid]="emailFormControl.invalid && emailFormControl.touched"
></digi-ng-form-input>
<fieldset>
<legend>Tjänster</legend>
<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>
<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"
@@ -34,23 +40,21 @@
>
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>
<fieldset>
<legend>Utförande verksamheter och adresser</legend>
<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>
<div class="edit-employee-form__choose_all-utforande-verksamh">
<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>
</div>
></digi-ng-form-checkbox>
<msfa-tree-nodes-selector
*ngIf="!toggleAllUtforandeVerksamhetFormControl.value"
[headingText]="'Välj utförande verksamheter och adresser'"
@@ -59,32 +63,22 @@
[showValidation]="utforandeVerksamhetFormControl?.touched"
[validationMessages]="utforandeVerksamhetFormControl.errors?.required ? ['Välj minst en utförande verksamhet'] : []"
(selectedTreeNodesChanged)="updateToggleAllUtforandeVerksamheter()"
>
</msfa-tree-nodes-selector>
</fieldset>
></msfa-tree-nodes-selector>
</div>
<fieldset *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
<legend>Behörigheter</legend>
<div class="edit-employee-form__block" *ngIf="rolesFormGroup && availableRoles" [formGroup]="rolesFormGroup">
<h3>Roller</h3>
<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>
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__roles-item" *ngFor="let role of availableRoles">
<li class="edit-employee-form__role" *ngFor="let role of availableRoles">
<digi-ng-form-checkbox
[afLabel]="role.name"
[formControlName]="getFormControlName(role)"
@@ -92,12 +86,13 @@
</li>
</ul>
</fieldset>
</div>
<digi-notification-alert
*ngIf="errorWhileUpdating"
af-variation="danger"
af-heading="Kunde inte spara"
af-heading-level="h2"
af-heading-level="h4"
[afCloseable]="true"
(afOnClose)="emitCloseError()"
>
@@ -105,6 +100,18 @@
<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
class="edit-employee-form__link-btn edit-employee-form__link-btn--secondary"
@@ -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>

View File

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

View File

@@ -49,10 +49,6 @@ describe('EditEmployeeFormComponent', () => {
tjanster: [],
allaUtforandeVerksamheter: false,
utforandeVerksamheter: [],
tjanstCodes: [],
utforandeVerksamhetIds: [],
utforandeAdressIds: [],
};
fixture.detectChanges();
});

View File

@@ -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: [],
});
}

View File

@@ -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"

View File

@@ -10,7 +10,6 @@
&__block {
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
&__input {

View File

@@ -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;

View File

@@ -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 {}

View File

@@ -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({

View File

@@ -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"

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
<msfa-layout>
<section class="messages">Meddelanden funkar!</section>
</msfa-layout>

View File

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

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -1,9 +1,29 @@
<msfa-layout>
<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>
</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>

View File

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

View File

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

View File

@@ -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) {}
}

View File

@@ -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 {}

View File

@@ -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(

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -1,3 +0,0 @@
<msfa-layout>
<section class="settings">Inställningar funkar!</section>
</msfa-layout>

View File

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

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -1,3 +0,0 @@
<msfa-layout>
<section class="statistics">Statistik funkar!</section>
</msfa-layout>

View File

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

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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>

View File

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

View File

@@ -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 {}

View File

@@ -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 {}

View File

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

View File

@@ -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"

View File

@@ -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',

View File

@@ -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>

View File

@@ -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;
::ng-deep .digi-logo {
--digi-logo--padding: 0;
}
&__logo {
height: 2rem;
vertical-align: middle;
}
}

View File

@@ -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],

View File

@@ -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>

View File

@@ -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;

View File

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

View File

@@ -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

View File

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

View File

@@ -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>

View File

@@ -43,6 +43,5 @@
&__footer {
grid-area: footer;
background-color: var(--digi--ui--color--primary);
min-height: 10rem;
}
}

View File

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

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
@import 'mixins/link';
.roles-dialog {
position: absolute;
}
.roles-dialog-button {
@include msfa__button-as-link(true);
}

View File

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

View File

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

View File

@@ -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 {}

View File

@@ -7,4 +7,5 @@ export const NAVIGATION = {
'planera-aktiviteter': 'Planera aktiviteter',
'mitt-konto': 'Mitt konto',
'skapa-personalkonto': 'Skapa personalkonto',
obehorig: 'Saknar behörigheter',
};

View File

@@ -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',

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
export interface EmployeeAdressResponse {
id: number;
namn: string;
}

View File

@@ -5,6 +5,5 @@ export interface EmployeeEditRequest {
roles: RoleEnum[];
tjanstIds: number[];
allaUtforandeVerksamheter: boolean;
utforandeVerksamhetIds?: number[];
adressIds: number[];
}

View File

@@ -0,0 +1,4 @@
export interface EmployeeTjanstResponse {
namn: string;
tjansteKod: string;
}

View File

@@ -0,0 +1,8 @@
import { EmployeeAdressResponse } from './employee-adress.response.model';
export interface EmployeeUtforandeVerksamhetResponse {
id: number;
namn: string;
allaAdresser: boolean;
adresser: EmployeeAdressResponse[];
}

View File

@@ -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 {

View File

@@ -1,6 +1,8 @@
import { RoleEnum } from '@msfa-enums/role.enum';
export interface UserInfoResponse {
id: string;
firstName: string;
lastName: string;
roles: string[];
roles: RoleEnum[];
}

View File

@@ -0,0 +1,4 @@
export interface UtforandeAdressResponse {
id: number;
namn: string;
}

View File

@@ -0,0 +1,7 @@
import { UtforandeAdressResponse } from './utforande-adress.response.model';
export interface UtforandeVerksamhetResponse {
id: number;
name: string;
adresser: UtforandeAdressResponse[];
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

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

View File

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

View File

@@ -1,12 +1,11 @@
import { Tjanst } from "./tjanst.model";
import { Tjanst } from './tjanst.model';
export interface FormTagData {
tjanstekod: string,
tjanstekod: string;
name: string;
}
export function mapTjanstToFormTag(tjanstData: Tjanst): FormTagData {
const { name, code } = tjanstData;
return { tjanstekod: code, name}
return { tjanstekod: code, name };
}

View File

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

View File

@@ -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)) : [],
};
}

View File

@@ -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,
};
}

View File

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

View File

@@ -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 => {

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

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

View File

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