refactor(project): Renamed all instances of dafa to msfa or mina-sidor-fa. (TV-379)

Squashed commit of the following:

commit d3f52ff6876f6e246c7d3c188e56cc2370289341
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Tue Aug 17 14:10:38 2021 +0200

    Renamed all dafa instances to msfa
This commit is contained in:
Erik Tiekstra
2021-08-18 07:10:28 +02:00
parent 6d29baa533
commit 03a2c7a42f
349 changed files with 720 additions and 734 deletions

View File

@@ -0,0 +1,85 @@
import { NgModule } from '@angular/core';
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
import { environment } from '@msfa-environment';
import { AuthGuard } from '@msfa-guards/auth.guard';
const routes: Routes = [
{
path: '',
data: { title: '' },
loadChildren: () => import('./pages/start/start.module').then(m => m.StartModule),
canActivate: [AuthGuard],
},
{
path: 'administration',
data: { title: 'Administration' },
loadChildren: () => import('./pages/administration/administration.module').then(m => m.AdministrationModule),
canActivate: [AuthGuard],
},
{
path: 'deltagare',
data: { title: 'Deltagare' },
loadChildren: () => import('./pages/deltagare/deltagare.module').then(m => m.DeltagareModule),
canActivate: [AuthGuard],
},
{
path: 'avrop',
data: { title: 'Avrop' },
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: 'Releases' },
loadChildren: () => import('./pages/releases/releases.module').then(m => m.ReleasesModule),
canActivate: [AuthGuard],
},
{
path: 'logout',
data: { title: 'Logga ut' },
loadChildren: () => import('./pages/logout/logout.module').then(m => m.LogoutModule),
},
];
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: '**',
data: { title: 'Sidan hittas inte' },
loadChildren: () => import('./pages/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule),
canActivate: [AuthGuard],
});
const options: ExtraOptions = {
useHash: false,
};
@NgModule({
imports: [RouterModule.forRoot(routes, options)],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,3 @@
<router-outlet></router-outlet>
<msfa-toast-list></msfa-toast-list>

View File

@@ -0,0 +1,21 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [AppComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@@ -0,0 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {}

View File

@@ -0,0 +1,25 @@
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ErrorHandler, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AuthGuard } from '@msfa-guards/auth.guard';
import { CustomErrorHandler } from '@msfa-interceptors/custom-error-handler.module';
import { AuthInterceptor } from '@msfa-services/api/auth.interceptor';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ToastListModule } from './components/toast-list/toast-list.module';
import { AvropModule } from './pages/avrop/avrop.module';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, AppRoutingModule, ToastListModule, AvropModule],
providers: [
{
provide: ErrorHandler,
useClass: CustomErrorHandler,
},
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard,
],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -0,0 +1,15 @@
<section [ngClass]="className" *ngIf="errors$ | async as errors">
<ul class="toast-list__list" *ngIf="errors.length">
<li class="toast-list__item" *ngFor="let error of errors">
<msfa-toast [error]="error" (closeToast)="removeError($event)"></msfa-toast>
</li>
</ul>
<div class="msfa__a11y-sr-only" [attr.aria-live]="ariaLivePoliteness" [attr.aria-atomic]="ariaAtomic">
<ng-container *ngIf="errors.length">
<h3>{{ errors.length }} fel har uppstått!</h3>
<ul>
<li *ngFor="let error of errors">{{ error.name }}: {{ error.message }}</li>
</ul>
</ng-container>
</div>
</section>

View File

@@ -0,0 +1,55 @@
@import 'mixins/list';
.toast-list {
position: fixed;
pointer-events: none;
z-index: 9999;
margin: var(--digi--layout--gutter);
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
&--top-right {
justify-content: flex-end;
}
&--top-left {
justify-content: flex-start;
}
&--top-center {
justify-content: center;
}
&--center-right {
justify-content: flex-end;
align-items: center;
}
&--center-left {
justify-content: flex-start;
align-items: center;
}
&--center-center {
justify-content: center;
align-items: center;
}
&--bottom-right {
justify-content: flex-end;
align-items: flex-end;
}
&--bottom-left {
justify-content: flex-start;
align-items: flex-end;
}
&--bottom-center {
justify-content: center;
align-items: flex-end;
}
&__list {
@include msfa__reset-list;
}
&__item:not(:first-child) {
margin-top: var(--digi--layout--gutter);
}
}

View File

@@ -0,0 +1,26 @@
/* tslint:disable:no-unused-variable */
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ToastListComponent } from './toast-list.component';
describe('ToastListComponent', () => {
let component: ToastListComponent;
let fixture: ComponentFixture<ToastListComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [ToastListComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(ToastListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,30 @@
import { AriaLivePoliteness } from '@angular/cdk/a11y';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ToastPosition } from '@msfa-enums/toast-position.enum';
import { CustomError } from '@msfa-models/error/custom-error';
import { ErrorService } from '@msfa-services/error.service';
import { Observable } from 'rxjs';
@Component({
selector: 'msfa-toast-list',
templateUrl: './toast-list.component.html',
styleUrls: ['./toast-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ToastListComponent {
@Input() ariaLivePoliteness: AriaLivePoliteness = 'assertive';
@Input() ariaAtomic = true;
@Input() position: ToastPosition = ToastPosition.TOP_RIGHT;
errors$: Observable<CustomError[]> = this.errorService.errors$;
constructor(private errorService: ErrorService) {}
get className(): string {
return `toast-list toast-list--${this.position}`;
}
removeError(error: CustomError): void {
this.errorService.remove(error);
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ToastListComponent } from './toast-list.component';
import { ToastModule } from './toast/toast.module';
@NgModule({
declarations: [ToastListComponent],
imports: [CommonModule, ToastModule],
exports: [ToastListComponent],
})
export class ToastListModule {}

View File

@@ -0,0 +1,14 @@
<div [ngClass]="className">
<div class="toast__icon-wrapper">
<msfa-icon [icon]="iconType.INFO" size="l" *ngIf="error.severity === errorSeverity.HIGH"></msfa-icon>
<msfa-icon [icon]="iconType.WARNING" size="l" *ngIf="error.severity === errorSeverity.MEDIUM"></msfa-icon>
<msfa-icon [icon]="iconType.APPROVED" size="l" *ngIf="error.severity === errorSeverity.LOW"></msfa-icon>
</div>
<div class="toast__content">
<button class="toast__close-button" aria-label="Stäng meddelandet" (click)="emitCloseEvent()">
<msfa-icon [icon]="iconType.X" size="l"></msfa-icon>
</button>
<h3 class="toast__heading">{{ error.name }}</h3>
<p class="toast__message">{{ error.message }}</p>
</div>
</div>

View File

@@ -0,0 +1,64 @@
@import 'variables/shadows';
.toast {
position: relative;
display: flex;
align-items: stretch;
background-color: var(--digi--ui--color--informative);
border: 2px solid var(--digi--ui--color--informative);
box-shadow: $msfa__shadow;
pointer-events: auto;
color: var(--digi--typography--color--text);
font-size: 1rem;
&--high {
background-color: var(--digi--ui--color--danger);
border-color: var(--digi--ui--color--danger);
}
&--medium {
background-color: var(--digi--ui--color--warning);
border-color: var(--digi--ui--color--warning);
}
&__icon-wrapper {
display: flex;
align-items: center;
padding: var(--digi--layout--gutter);
color: var(--digi--typography--color--text--light);
font-size: 2rem;
.toast--medium & {
color: var(--digi--typography--color--text);
}
}
&__content {
display: flex;
width: 100%;
background-color: var(--digi--ui--color--background);
flex-direction: column;
justify-content: center;
padding: var(--digi--layout--gutter--s);
}
&__heading {
margin: 0;
}
&__message {
max-width: 400px !important;
margin: 0;
overflow-wrap: break-word;
}
&__close-button {
background-color: transparent;
border-width: 0;
position: absolute;
top: 0;
right: 0;
padding: var(--digi--layout--gutter--s);
color: var(--digi--typography--color--text);
}
}

View File

@@ -0,0 +1,30 @@
/* tslint:disable:no-unused-variable */
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CustomError } from '@msfa-models/error/custom-error';
import { ToastComponent } from './toast.component';
describe('ToastComponent', () => {
let component: ToastComponent;
let fixture: ComponentFixture<ToastComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ToastComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(ToastComponent);
component = fixture.componentInstance;
component.error = new CustomError({ error: { name: 'Test', message: 'TestError' } });
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,34 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ErrorSeverity } from '@msfa-enums/error-severity.enum';
import { IconType } from '@msfa-enums/icon-type.enum';
import { CustomError } from '@msfa-models/error/custom-error';
@Component({
selector: 'msfa-toast',
templateUrl: './toast.component.html',
styleUrls: ['./toast.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ToastComponent implements AfterViewInit {
@Input() error: CustomError;
@Output() closeToast = new EventEmitter<CustomError>();
iconType = IconType;
errorSeverity = ErrorSeverity;
ngAfterViewInit(): void {
if (this.error.removeAfter) {
setTimeout(() => {
this.closeToast.emit(this.error);
}, this.error.removeAfter);
}
}
get className(): string {
return `toast toast--${this.error.severity.toLowerCase()}`;
}
emitCloseEvent(): void {
this.closeToast.emit(this.error);
}
}

View File

@@ -0,0 +1,23 @@
import { DigiNgIconExclamationCircleModule } from '@af/digi-ng/_icon/icon-exclamation-circle';
import { DigiNgIconExclamationTriangleModule } from '@af/digi-ng/_icon/icon-exclamation-triangle';
import { DigiNgIconInfoCircleRegModule } from '@af/digi-ng/_icon/icon-info-circle-reg';
import { DigiNgIconXModule } from '@af/digi-ng/_icon/icon-x';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { IconModule } from '@msfa-shared/components/icon/icon.module';
import { ToastComponent } from './toast.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ToastComponent],
imports: [
CommonModule,
DigiNgIconXModule,
DigiNgIconExclamationCircleModule,
DigiNgIconExclamationTriangleModule,
DigiNgIconInfoCircleRegModule,
IconModule,
],
exports: [ToastComponent],
})
export class ToastModule {}

View File

@@ -0,0 +1,36 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: 'personal',
pathMatch: 'full',
},
{
path: 'personal',
loadChildren: () => import('./pages/employees/employees.module').then(m => m.EmployeesModule),
},
{
path: 'personal/:employeeId',
loadChildren: () => import('./pages/employee-card/employee-card.module').then(m => m.EmployeeCardModule),
},
{
path: 'skapa-konto',
loadChildren: () => import('./pages/employee-form/employee-form.module').then(m => m.EmployeeFormModule),
},
{
path: 'bjuda-in',
loadChildren: () => import('./pages/employee-invite/employee-invite.module').then(m => m.EmployeeInviteModule),
},
{
path: 'redigera-konto/:employeeId',
loadChildren: () => import('./pages/employee-form/employee-form.module').then(m => m.EmployeeFormModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AdministrationRoutingModule {}

View File

@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AdministrationComponent } from './administration.component';
describe('AdministrationComponent', () => {
let component: AdministrationComponent;
let fixture: ComponentFixture<AdministrationComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [AdministrationComponent],
imports: [RouterTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(AdministrationComponent);
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-administration',
templateUrl: './administration.component.html',
styleUrls: ['./administration.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdministrationComponent {}

View File

@@ -0,0 +1,10 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AdministrationRoutingModule } from './administration-routing.module';
import { AdministrationComponent } from './administration.component';
@NgModule({
declarations: [AdministrationComponent],
imports: [CommonModule, AdministrationRoutingModule],
})
export class AdministrationModule {}

View File

@@ -0,0 +1,93 @@
<msfa-layout>
<section class="employee-card">
<digi-typography *ngIf="detailedEmployeeData$ | async as detailedEmployeeData; else loadingRef">
<div class="employee-card__editcontainer">
<h1>{{ detailedEmployeeData.fullName }}</h1>
<span class="employee-card__editbutton">
<a href="./administration/skapa-konto">Redigera</a>
</span>
</div>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus accusantium sit, reprehenderit, esse suscipit
quis similique harum est eum eveniet aspernatur delectus magni asperiores porro aliquam voluptate! Architecto,
perferendis commodi.
</p>
<div class="employee-card__contents">
<div class="employee-card__column">
<h2>Kontaktuppgifter</h2>
<dl>
<dt>Namn</dt>
<dd *ngIf="detailedEmployeeData.fullName; else emptyDD">{{ detailedEmployeeData.fullName }}</dd>
<dt>Personnummer</dt>
<dd *ngIf="detailedEmployeeData.ssn; else emptyDD">
<msfa-hide-text
symbols="********-****"
[changingText]="detailedEmployeeData.ssn"
ariaLabelType="personnummer"
></msfa-hide-text>
</dd>
</dl>
</div>
<div class="employee-card__column">
<h2>Tjänst</h2>
<ul class="employee-card__list">
<ng-container *ngIf="detailedEmployeeData.services.length; else emptyDD">
<li class="employee-card__column--listitem" *ngFor="let service of detailedEmployeeData.services">
{{ service.name }}
</li>
</ng-container>
</ul>
</div>
<div class="employee-card__organizations">
<h2>Utförande verksamheter och utförande adresser</h2>
<ul class="employee-card__list" *ngIf="detailedEmployeeData.organizations?.length">
<li class="employee-card__list" *ngFor="let organization of detailedEmployeeData.organizations">
{{ organization.name }}
<ul>
<li class="employee-card__listitem--indent">
{{ organization.address.street }} {{ organization.address.postalCode }} {{
organization.address.houseNumber }} {{ organization.address.city }}
</li>
</ul>
</li>
</ul>
</div>
<div class="employee-card__column">
<h2>Behörigheter</h2>
<ul class="employee-card__list">
<ng-container *ngIf="detailedEmployeeData.authorizations.length; else emptyDD">
<li *ngFor="let authorization of detailedEmployeeData.authorizations">{{ authorization.name }}</li>
</ng-container>
</ul>
</div>
</div>
<p></p>
</digi-typography>
<div class="employee-card__footer">
<span class="employee-card__secondarybutton">
<a href="./administration/personal">Tillbaka till personallistan</a>
</span>
<span class="employee-card__primarybutton">
<a href="./administration/skapa-konto">Skapa nytt konto</a>
</span>
</div>
</section>
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar personalkortet"></digi-ng-skeleton-base>
</ng-template>
<ng-template #emptyDD class="employee-card__list">
<dd>
<span aria-hidden="true">-</span>
<span class="msfa__a11y-sr-only">Info saknas</span>
</dd>
</ng-template>
</msfa-layout>

View File

@@ -0,0 +1,92 @@
@import 'variables/gutters';
@import 'variables/colors';
@import 'mixins/buttons';
@import 'mixins/list';
.employee-card {
&__contents {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--xl $digi--layout--gutter--l;
}
&__editcontainer {
display: flex;
justify-content: space-between;
align-items: center;
}
&__h2 {
margin-top: 0;
}
&__column {
width: 100%;
max-width: var(--digi--typography--text--max-width);
}
&__organizations {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
&__footer {
margin-top: 5rem;
}
//LISTS
&__list {
@include msfa__reset-list;
}
&__listitem--indent {
@include msfa__reset-list;
margin-left: 1rem;
}
&__description {
margin-left: 0.1rem;
grid-column: 1;
}
&__term {
margin: 0;
grid-column: 1;
font-weight: var(--digi--typography--font-weight--semibold);
}
//BUTTONS
&__primarybutton {
a {
@include msfa_buttontemplate(
$msfa-button--background--primary,
$msfa-button--text--primary,
$msfa-button--hover--primary
);
}
}
&__secondarybutton {
a {
@include msfa_buttontemplate(
$msfa-button--background--secondary,
$msfa-button--text--secondary,
$msfa-button--hover--secondary
);
}
}
&__editbutton {
a {
@include msfa_buttontemplate(
$msfa-button--background--secondary,
$msfa-button--text--secondary,
$msfa-button--hover--secondary
);
width: var(--digi-button--width);
}
}
}

View File

@@ -0,0 +1,30 @@
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 { EmployeeCardComponent } from './employee-card.component';
describe('EmployeeCardComponent', () => {
let component: EmployeeCardComponent;
let fixture: ComponentFixture<EmployeeCardComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeeCardComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(EmployeeCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,48 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Employee } from '@msfa-models/employee.model';
import { Participant } from '@msfa-models/participant.model';
import { EmployeeService } from '@msfa-services/api/employee.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
@Component({
selector: 'msfa-employee-card',
templateUrl: './employee-card.component.html',
styleUrls: ['./employee-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmployeeCardComponent {
private _pendingSelectedParticipants$ = new BehaviorSubject<string[]>([]);
private _employeeId$: Observable<string> = this.activatedRoute.params.pipe(
map(({ employeeId }) => employeeId as string)
);
detailedEmployeeData$: Observable<Employee> = this._employeeId$.pipe(
switchMap(employeeId => this.employeeService.fetchDetailedEmployeeData$(employeeId))
);
constructor(private activatedRoute: ActivatedRoute, private employeeService: EmployeeService) {}
get pendingSelectedParticipants(): string[] {
return this._pendingSelectedParticipants$.getValue();
}
handleChangeEmployee(): void {
console.log('change employee: ', this.pendingSelectedParticipants);
}
handleChangeParticipant(id: string, checked: boolean): void {
const currentPendingSelectedParticipants = this.pendingSelectedParticipants;
if (checked) {
this._pendingSelectedParticipants$.next([...this.pendingSelectedParticipants, id]);
} else {
this._pendingSelectedParticipants$.next(currentPendingSelectedParticipants.filter(currentId => currentId !== id));
}
}
handleChangeAllParticipants(participants: Participant[], checked: boolean): void {
this._pendingSelectedParticipants$.next(checked ? participants.map(participant => participant.id) : []);
}
}

View File

@@ -0,0 +1,24 @@
import { DigiNgLayoutExpansionPanelModule } from '@af/digi-ng/_layout/layout-expansion-panel';
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 { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { LocalDatePipeModule } from '@msfa-shared/pipes/local-date/local-date.module';
import { EmployeeCardComponent } from './employee-card.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeeCardComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: EmployeeCardComponent }]),
LayoutModule,
DigiNgSkeletonBaseModule,
DigiNgLayoutExpansionPanelModule,
LocalDatePipeModule,
HideTextModule,
],
})
export class EmployeeCardModule {}

View File

@@ -0,0 +1,164 @@
<msfa-layout>
<section class="employee-form">
<digi-typography>
<h1>Skapa nytt konto</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam magna neque, interdum vel massa eget, condimentum
rutrum velit. Sed vitae ullamcorper sem. Aliquam malesuada nunc sed purus mollis scelerisque. Curabitur bibendum
leo quis ante porttitor tincidunt. Nam tincidunt imperdiet tortor eu suscipit. Maecenas ut dui est.
</p>
</digi-typography>
<form [formGroup]="formGroup" (ngSubmit)="submitForm()">
<digi-form-error-list
class="employee-form__error-list"
*ngIf="formGroup.invalid && submitted && formErrors.length"
af-heading="Felmeddelanden"
>
<a *ngFor="let error of formErrors" [routerLink]="" [fragment]="'employee-form-' + error.id"
>{{ error.message }}</a
>
</digi-form-error-list>
<div class="employee-form__block">
<digi-typography>
<h2>Personuppgifter</h2>
</digi-typography>
<digi-ng-form-input
afId="employee-form-firstName"
class="employee-form__input"
formControlName="firstName"
afLabel="Förnamn"
afInvalidMessage="Förnamn är obligatoriskt"
[afDisableValidStyle]="true"
[afInvalid]="firstNameControl.invalid && firstNameControl.dirty"
></digi-ng-form-input>
<digi-ng-form-input
afId="employee-form-lastName"
class="employee-form__input"
formControlName="lastName"
afLabel="Efternamn"
afInvalidMessage="Efternamn är obligatoriskt"
[afDisableValidStyle]="true"
[afInvalid]="lastNameControl.invalid && lastNameControl.dirty"
></digi-ng-form-input>
<digi-ng-form-input
afId="employee-form-ssn"
class="employee-form__input"
formControlName="ssn"
afLabel="Personnummer"
[afInvalidMessage]="ssnControl.errors?.message || ''"
[afDisableValidStyle]="true"
[afInvalid]="ssnControl.invalid && ssnControl.dirty"
></digi-ng-form-input>
</div>
<div class="employee-form__block" *ngIf="services$ | async as services">
<fieldset class="employee-form__fieldset">
<digi-typography>
<legend>Tjänster</legend>
</digi-typography>
<ul class="employee-form__services">
<li *ngFor="let service of services; let first = first" class="employee-form__service-item">
<digi-form-checkbox
[afId]="(first && 'employee-form-services') || undefined"
af-variation="primary"
[afValidation]="servicesControl.invalid && servicesControl.dirty && 'error'"
[afLabel]="service.name"
[afValue]="service.id"
[afChecked]="servicesControl.value.includes(service)"
(afOnChange)="toggleService(service, $event.detail.target.checked)"
></digi-form-checkbox>
</li>
</ul>
<digi-form-validation-message
class="employee-form__validation-message"
*ngIf="servicesControl.invalid && servicesControl.dirty"
af-variation="error"
>
{{ servicesControl.errors.message }}
</digi-form-validation-message>
</fieldset>
</div>
<div class="employee-form__block" *ngIf="authorizations$ | async as authorizations">
<fieldset class="employee-form__fieldset">
<digi-typography>
<legend>Tilldela behörigheter</legend>
</digi-typography>
<ul class="employee-form__authorizations">
<li
*ngFor="let authorization of authorizations; let first = first"
class="employee-form__authorization-item"
>
<digi-form-checkbox
class="employee-form__digi-checkbox"
[afId]="(first && 'employee-form-authorizations') || undefined"
af-variation="primary"
[afValidation]="authorizationsControl.invalid && authorizationsControl.dirty && 'error'"
[afLabel]="authorization.name"
[afValue]="authorization.id"
[afChecked]="authorizationsControl.value.includes(authorization)"
(afOnChange)="toggleAuthorization(authorization, $event.detail.target.checked)"
></digi-form-checkbox>
<digi-button
af-variation="secondary"
[afAriaLabel]="'Läs mer om ' + authorization.name"
af-size="s"
class="employee-form__read-more"
(afOnClick)="openDialog(true, authorization.name)"
>
Läs mer
</digi-button>
</li>
</ul>
<digi-form-validation-message
class="employee-form__validation-message"
*ngIf="authorizationsControl.invalid && authorizationsControl.dirty"
af-variation="error"
>
{{ authorizationsControl.errors.message }}
</digi-form-validation-message>
</fieldset>
</div>
<div class="employee-form__footer">
<digi-button af-type="reset" af-variation="secondary" (afOnClick)="resetForm($event.detail)"
>Avbryt</digi-button
>
<digi-button af-type="submit">Registrera konto</digi-button>
</div>
<!-- Modal/ Dialog window -->
<digi-ng-dialog
[afActive]="toggleDialog"
(afOnInactive)="openDialog(false)"
(afOnPrimaryClick)="openDialog(false)"
[afHeading]="modalAuthInfo.name"
afHeadingLevel="h3"
afPrimaryButtonText="Stäng"
>
<p>
Behörigheten passar personer som arbetar nära deltagare. Behörigheten kan användas av exempelvis handledare,
coacher, studie- och yrkesvägledare, lärare eller annan roll som behöver kunna se information om deltager,
kontakta deltagare, planera aktiviteter med deltagre och hantera rapporter för deltagre.
</p>
<p>Behörigheten ger tillgång till och utföra aktiviteter i följande funktioner i systemet:</p>
<p>
- Deltagarlista <br />
- Information om deltagare <br />
- Resultatrapporter <br />
- Slutredovisning <br />
- Informativ rapport <br />
- Skicka välkomstbrev * <br />
- Planera deltagares aktiviteter <br />
- Deltagares schema <br />
- Avvikelserapporter <br />
- Närvaro- och frånvarorapporter <br /><br />
</p>
</digi-ng-dialog>
</form>
</section>
</msfa-layout>

View File

@@ -0,0 +1,77 @@
@import 'mixins/list';
@import 'variables/gutters';
.employee-form {
&__block {
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
&__input {
display: block;
width: 100%;
margin-bottom: var(--digi--layout--gutter);
}
&__fieldset {
padding: 0;
border: 0;
legend {
width: 100%;
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);
}
}
&__validation-message {
display: block;
margin-top: var(--digi--layout--gutter--s);
}
&__services,
&__authorizations {
@include msfa__reset-list;
margin-bottom: var(--digi--layout--gutter);
}
&__authorization-item {
display: grid;
grid-template-columns: 1fr;
grid-template-areas: 'auth-checkbox read-more';
}
&__service-item {
display: flex;
align-items: center;
&:not(:first-child) {
margin-top: var(--digi--layout--gutter);
}
}
&__error-list {
display: block;
margin-top: $digi--layout--gutter--l;
}
&__footer {
margin-top: $digi--layout--gutter--xl;
display: flex;
gap: var(--digi--layout--gutter);
}
&__digi-checkbox {
grid-area: auth-checkbox;
align-self: center;
padding: 0.5rem 0;
}
&__read-more {
grid-area: read-more;
align-self: center;
}
}

View File

@@ -0,0 +1,47 @@
import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgPopoverModule } from '@af/digi-ng/_popover/popover';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { EmployeeFormComponent } from './employee-form.component';
describe('EmployeeFormComponent', () => {
let component: EmployeeFormComponent;
let fixture: ComponentFixture<EmployeeFormComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeeFormComponent],
imports: [
RouterTestingModule,
HttpClientTestingModule,
ReactiveFormsModule,
DigiNgFormInputModule,
DigiNgFormRadiobuttonGroupModule,
DigiNgFormDatepickerModule,
DigiNgFormSelectModule,
DigiNgPopoverModule,
DigiNgFormCheckboxModule,
],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(EmployeeFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,149 @@
import { FormSelectBaseItem } from '@af/digi-ng/_form/form-select-base';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { Authorization } from '@msfa-models/authorization.model';
import { Service } from '@msfa-models/service.model';
import { AuthorizationService } from '@msfa-services/api/authorizations.service';
import { EmployeeService } from '@msfa-services/api/employee.service';
import { ServiceService } from '@msfa-services/api/service.service';
import { SocialSecurityNumberValidator } from '@msfa-utils/validators/social-security-number.validator';
import { RequiredValidator } from '@msfa-validators/required.validator';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'msfa-employee-form',
templateUrl: './employee-form.component.html',
styleUrls: ['./employee-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmployeeFormComponent {
services$: Observable<Service[]> = this.serviceService.services$;
authorizations$: Observable<Authorization[]> = this.authorizationService.authorizations$;
servicesSelectItems$: Observable<FormSelectBaseItem[]> = this.services$.pipe(
map(services => services.map(({ name, id }) => ({ name, value: id })))
);
toggleDialog = false;
modalAuthInfo: { name: string } = { name: 'Test Behörighetsnamn' };
formGroup: FormGroup = this.formBuilder.group({
firstName: this.formBuilder.control('', [RequiredValidator('Förnamn')]),
lastName: this.formBuilder.control('', [RequiredValidator('Efternamn')]),
ssn: this.formBuilder.control('', [RequiredValidator('Personnummer'), SocialSecurityNumberValidator()]),
services: this.formBuilder.control([], [RequiredValidator('en tjänst')]),
authorizations: this.formBuilder.control([], [RequiredValidator('en behörighet')]),
});
todaysDate = new Date();
submitted = false;
constructor(
private formBuilder: FormBuilder,
private employeeService: EmployeeService,
private serviceService: ServiceService,
private authorizationService: AuthorizationService,
private router: Router
) {}
get firstNameControl(): AbstractControl {
return this.formGroup.get('firstName');
}
get lastNameControl(): AbstractControl {
return this.formGroup.get('lastName');
}
get ssnControl(): AbstractControl {
return this.formGroup.get('ssn');
}
get servicesControl(): AbstractControl {
return this.formGroup.get('services');
}
get authorizationsControl(): AbstractControl {
return this.formGroup.get('authorizations');
}
get formErrors(): { id: string; message: string }[] {
const controlsWithErrors = Object.keys(this.formGroup.controls).filter(
key => !!this.formGroup.controls[key].errors
);
return controlsWithErrors.map(key => ({
id: key,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: this.formGroup.controls[key].errors.message,
}));
}
private _markFormAsDirty(): void {
Object.keys(this.formGroup.controls).forEach(control => {
this.formGroup.get(control).markAsDirty();
this.formGroup.get(control).markAsTouched();
});
}
toggleAuthorization(authorization: Authorization, checked: boolean): void {
const currentAuthorizations = this.authorizationsControl.value as { id: unknown }[];
if (checked) {
this.authorizationsControl.patchValue([...currentAuthorizations, authorization]);
} else {
this.authorizationsControl.patchValue(
currentAuthorizations.filter(currentAuthorization => currentAuthorization.id !== authorization.id)
);
}
}
toggleService(service: Service, checked: boolean): void {
const currentServices = this.servicesControl.value as { id: unknown }[];
if (checked) {
this.servicesControl.patchValue([...currentServices, service]);
} else {
this.servicesControl.patchValue(currentServices.filter(currentService => currentService.id !== service.id));
}
}
openDialog(val: boolean, authName?: string): void {
if (authName) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.modalAuthInfo.name = authName;
}
this.toggleDialog = val;
}
setFocusOnInvalidInput(event: CustomEvent): void {
console.log(event.target);
}
resetForm(event: Event): void {
event.preventDefault();
this.formGroup.reset({
firstName: '',
lastName: '',
ssn: '',
services: [],
authorizations: [],
});
// Object.keys(this.formGroup.controls).forEach(controlKey => this.formGroup.controls[controlKey].markAsPristine());
}
submitForm(): void {
this.submitted = true;
if (this.formGroup.valid) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const submittableValues = {
...this.formGroup.value,
};
const post = this.employeeService.postNewEmployee(submittableValues).subscribe({
next: id => {
void this.router.navigate(['/administration', 'personal', id]);
},
complete: () => {
post.unsubscribe();
},
});
} else {
console.error('Form is invalid, do something...');
this._markFormAsDirty();
}
}
}

View File

@@ -0,0 +1,34 @@
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgPopoverModule } from '@af/digi-ng/_popover/popover';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { LocalDatePipeModule } from '@msfa-shared/pipes/local-date/local-date.module';
import { EmployeeFormComponent } from './employee-form.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeeFormComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: EmployeeFormComponent }]),
LayoutModule,
ReactiveFormsModule,
LocalDatePipeModule,
DigiNgFormInputModule,
DigiNgFormRadiobuttonGroupModule,
DigiNgFormDatepickerModule,
DigiNgFormSelectModule,
DigiNgPopoverModule,
DigiNgFormCheckboxModule,
DigiNgDialogModule,
],
})
export class EmployeeFormModule {}

View File

@@ -0,0 +1,60 @@
<msfa-layout>
<section class="employee-invite">
<digi-typography>
<h1>Skapa nytt personalkonto</h1>
<p>Såhär skapar du ett nytt personalkonto:</p>
<ol>
<li>Skicka en inbjudningslänk till personalens e-postadress.</li>
<li>Personalen öppnar inbjudningslänken via sin e-post och skapar ett personalkonto med sitt Bank-ID.</li>
<li>
När kontot är skapat ser du det i personallistan. Det nya personalkontot saknar fortfarande behörigheter.
</li>
<li>
Ge personalkontot behörigheter genom att klicka på namnet i personallistan och ange vilka behörigheter
personalen ska ha. Nu kan personalen logga in och arbeta.
</li>
</ol>
</digi-typography>
<form [formGroup]="form" (ngSubmit)="submitForm()">
<div class="employee-invite__block">
<digi-typography>
<h2>Skicka en inbjudningslänk</h2>
<p>
Skicka en inbjudningslänk till personalen du vill lägga till som systemanvändare. Ange personalens
e-postadress nedan och tryck på skicka inbjudningslänk.
</p>
</digi-typography>
<div class="employee-invite__input-section">
<digi-ng-form-input
afId="employee-invite-email"
class="employee-invite__input"
formControlName="email"
afLabel="E-postadress"
afType="email"
[afRequired]="true"
[afInvalidMessage]="email.errors?.message || 'Ogiltig e-postadress'"
[afDisableValidStyle]="true"
[afInvalid]="email.invalid && email.dirty"
></digi-ng-form-input>
<digi-button af-size="m" af-type="submit" class="employee-invite__invitation-btn"
>Skicka inbjudningslänk</digi-button
>
</div>
</div>
<digi-notification-alert
*ngIf="(latestSubmittedInvite$ | async) as latestSubmittedInvite"
af-variation="success"
af-heading="Allt gick bra"
af-heading-level="h3"
af-closeable="true"
(click)="onCloseAlert()"
>
<p>Inbjudan har skickats till {{latestSubmittedInvite.email}}</p>
</digi-notification-alert>
<footer class="employee-invite__footer">
<msfa-back-link [route]="['/administration/personal']">Tillbaka till personallistan</msfa-back-link>
</footer>
</form>
</section>
</msfa-layout>

View File

@@ -0,0 +1,31 @@
@import 'variables/gutters';
.employee-invite {
&__block {
max-width: var(--digi--typography--text--max-width);
margin-top: $digi--layout--gutter--xl;
margin-bottom: $digi--layout--gutter--xl;
}
&__input-section {
display: flex;
margin-top: $digi--layout--gutter--xl;
}
&__input {
display: block;
min-width: 240px;
margin-bottom: var(--digi--layout--gutter);
}
&__invitation-btn {
margin-top: 31px;
margin-left: 16px;
}
&__footer {
margin-top: $digi--layout--gutter--xl;
display: flex;
gap: var(--digi--layout--gutter);
}
}

View File

@@ -0,0 +1,33 @@
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { EmployeeInviteComponent } from './employee-invite.component';
describe('EmployeeInviteComponent', () => {
let component: EmployeeInviteComponent;
let fixture: ComponentFixture<EmployeeInviteComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ EmployeeInviteComponent ],
imports: [
ReactiveFormsModule,
DigiNgFormInputModule
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(EmployeeInviteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,54 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { EmployeeService } from '@msfa-services/api/employee.service';
import { RequiredValidator } from '@msfa-utils/validators/required.validator';
import { BehaviorSubject, Observable } from 'rxjs';
@Component({
selector: 'msfa-employee-invite',
templateUrl: './employee-invite.component.html',
styleUrls: ['./employee-invite.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmployeeInviteComponent implements OnInit {
form: FormGroup;
private latestSubmittedInvite_ = new BehaviorSubject<string>('');
latestSubmittedInvite$: Observable<string> = this.latestSubmittedInvite_.asObservable();
constructor(private employeeService: EmployeeService) {}
ngOnInit(): void {
this.form = new FormGroup({
email: new FormControl('', [
RequiredValidator('E-postadress'),
Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'),
]),
});
}
get email() {
return this.form.get('email');
}
submitForm(): void {
if (this.form.invalid) {
this.email.markAsDirty();
this.email.markAsTouched();
return;
}
const post = this.employeeService.postEmployeeInvitation(this.form.value).subscribe({
next: () => {
this.latestSubmittedInvite_.next(this.form.value);
this.form.reset();
},
complete: () => {
post.unsubscribe();
},
});
}
onCloseAlert(): void {
this.latestSubmittedInvite_.next('');
}
}

View File

@@ -0,0 +1,22 @@
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { EmployeeInviteComponent } from './employee-invite.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeeInviteComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: EmployeeInviteComponent }]),
LayoutModule,
BackLinkModule,
ReactiveFormsModule,
DigiNgFormInputModule,
],
})
export class EmployeeInviteModule {}

View File

@@ -0,0 +1,64 @@
<div class="employees-list">
<digi-table af-variation="secondary">
<table>
<thead>
<tr>
<th scope="col" class="employees-list__column-head" *ngFor="let column of columnHeaders">
<button
class="employees-list__sort-button"
[attr.id]="'sort-button-' + column.key"
(click)="handleSort(column.key)"
>
{{column.label}}
<ng-container *ngIf="sort.key === column.key">
<digi-icon-caret-up
class="employees-list__sort-icon"
*ngIf="sort.order === orderType.ASC"
></digi-icon-caret-up>
<digi-icon-caret-down
class="employees-list__sort-icon"
*ngIf="sort.order === orderType.DESC"
></digi-icon-caret-down>
</ng-container>
</button>
</th>
<th scope="col" class="employees-list__column-head">Redigera</th>
</tr>
</thead>
<tbody>
<tr class="employees-list__row" *ngFor="let employee of employees">
<th scope="row">
<a [routerLink]="employee.id" class="employees-list__link">{{ employee.fullName }}</a>
</th>
<td>
<ng-container *ngFor="let service of employee.services; let last = last">
{{ service.name }}<ng-container *ngIf="!last">, </ng-container>
</ng-container>
</td>
<td>
<ng-container *ngFor="let organization of employee.organizations; let last = last">
{{ organization.address.city }}<ng-container *ngIf="!last">, </ng-container>
</ng-container>
</td>
<td>
<digi-button af-variation="tertiary">
<digi-icon-edit style="--digi--ui--width--icon: 1.25rem" slot="icon"></digi-icon-edit>
</digi-button>
</td>
</tr>
</tbody>
</table>
</digi-table>
<digi-navigation-pagination
*ngIf="totalPages > 1"
class="employees-list__pagination"
[afTotalPages]="totalPages"
[afCurrentResultStart]="currentResultStart"
[afCurrentResultEnd]="currentResultEnd"
[afTotalResults]="count"
(afOnPageChange)="setNewPage($event.detail)"
af-result-name="medarbetare"
>
</digi-navigation-pagination>
</div>

View File

@@ -0,0 +1,35 @@
@import 'variables/gutters';
.employees-list {
&__column-head {
padding: 0;
}
&__sort-button {
position: relative;
background-color: transparent;
border-width: 0;
width: 100%;
text-align: left;
padding: var(--digi--layout--gutter--s) $digi--layout--gutter--l var(--digi--layout--gutter--s)
var(--digi--layout--gutter);
margin: 0;
font-size: inherit;
font-weight: inherit;
display: flex;
align-items: center;
gap: var(--digi--layout--gutter);
cursor: pointer;
}
&__sort-icon {
position: absolute;
display: inline-flex;
right: 0.5rem;
}
&__pagination {
display: block;
margin-top: var(--digi--layout--gutter);
}
}

View File

@@ -0,0 +1,54 @@
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 { Employee } from '@msfa-models/employee.model';
import { EmployeesListComponent } from './employees-list.component';
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({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeesListComponent],
imports: [RouterTestingModule],
}).compileComponents();
fixture = TestBed.createComponent(EmployeesListComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('20 employees sorted by Full name Ascending', () => {
beforeEach(() => {
component.employees = employeesMock;
component.paginationMeta = { count: employeesMock.length, limit: 50, page: 1, totalPages: 3 };
component.sort = { key: <keyof Employee>'fullName', order: SortOrder.ASC };
fixture.detectChanges();
});
it('should display the rows from employees object 20 rows regardless of pagination', () => {
expect(getEmployeeRows().length).toBe(20);
});
it('should display the up caret next to Full name to indicate that it´s sorted by full name Ascending', () => {
const fullNameUpCaret = fixture.debugElement.query(By.css('#sort-button-fullName > digi-icon-caret-up'));
expect(fullNameUpCaret).toBeTruthy();
});
it('should only display one caret', () => {
const upCarets = fixture.debugElement.queryAll(By.css('digi-icon-caret-up'));
const downCarets = fixture.debugElement.queryAll(By.css('digi-icon-caret-down'));
expect(upCarets.length + downCarets.length).toBe(1);
});
});
});

View File

@@ -0,0 +1,62 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { SortOrder } from '@msfa-enums/sort-order.enum';
import { Employee } from '@msfa-models/employee.model';
import { PaginationMeta } from '@msfa-models/pagination-meta.model';
import { Sort } from '@msfa-models/sort.model';
@Component({
selector: 'msfa-employees-list',
templateUrl: './employees-list.component.html',
styleUrls: ['./employees-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmployeesListComponent {
@Input() employees: Employee[];
@Input() paginationMeta: PaginationMeta;
@Input() sort: Sort<keyof Employee>;
@Output() sorted = new EventEmitter<keyof Employee>();
@Output() paginated = new EventEmitter<number>();
columnHeaders: { label: string; key: keyof Employee }[] = [
{ label: 'Namn', key: 'fullName' },
{
label: 'Tjänst',
key: 'services',
},
{
label: 'Utförandeverksamheter',
key: 'organizations',
},
];
orderType = SortOrder;
get currentPage(): number {
return this.paginationMeta.page;
}
get totalPages(): number {
return this.paginationMeta?.totalPages;
}
get count(): number {
return this.paginationMeta.count;
}
get currentResultStart(): number {
return (this.currentPage - 1) * this.paginationMeta.limit + 1;
}
get currentResultEnd(): number {
const end = this.currentResultStart + this.paginationMeta.limit - 1;
return end < this.count ? end : this.count;
}
handleSort(key: keyof Employee): void {
this.sorted.emit(key);
}
setNewPage(page: number): void {
this.paginated.emit(page);
}
}

View File

@@ -0,0 +1,565 @@
import { Service } from '@msfa-enums/service.enum';
import { Employee } from '@msfa-models/employee.model';
export const employeesMock: Employee[] = [
{
id: 'b136f30a-3997-4fdd-8c02-2415ee9c6d83',
firstName: 'Jayson',
lastName: 'Karlsson',
ssn: '19951019-7751',
organizations: [
{
id: 'd5b9d727-4473-47be-bdc0-cc3d6ed85934',
name: 'Svensson, Olsson and Nilsson',
kaNumber: 999419,
address: {
street: 'Eriksson gatan',
houseNumber: 85,
postalCode: '13202',
city: 'Columbia',
kommun: 'Halmstads kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628956,
},
{
id: 'c3359c5d-e0ff-4792-a3f3-7142fef932e5',
firstName: 'Elbert',
lastName: 'Andersson',
ssn: '19701221-4753',
organizations: [
{
id: 'fc42fe9c-ad06-46df-9c33-9e61b5c3f881',
name: 'Nilsson, Svensson and Johansson',
kaNumber: 578637,
address: {
street: 'Olsson gatan',
houseNumber: 33,
postalCode: '98821',
city: 'Hacienda Heights',
kommun: 'Motala kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628956,
},
{
id: '28cc9679-bf5e-4066-900f-0866710ebdbc',
firstName: 'Tyreek',
lastName: 'Larsson',
ssn: '19530826-5774',
organizations: [
{
id: '11da9de5-2ce2-4364-a1b2-08a263bfc248',
name: 'Eriksson Group',
kaNumber: 975639,
address: {
street: 'Vito allén',
houseNumber: 82,
postalCode: '61048',
city: 'Helsing Consuelo',
kommun: 'Finspångs kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628956,
},
{
id: '6fae0a53-fd04-4ca0-b099-933161c920b8',
firstName: 'Jarret',
lastName: 'Eriksson',
ssn: '19731207-7794',
organizations: [
{
id: 'b8011410-d7a8-4163-98c8-3262cf0681b9',
name: 'Svensson - Svensson',
kaNumber: 815388,
address: {
street: 'Delphine allén',
houseNumber: 26,
postalCode: '26994',
city: 'En Akeem',
kommun: 'Smedjebackens kommun',
},
},
],
services: [
{
id: '20e09e98-c744-45b3-95ef-54ef51af32c0',
name: 'KVL' as Service,
},
],
authorizations: [],
createdAt: 1623655628957,
},
{
id: '6fd136ad-f51a-4a30-b6e9-dd1116cf90d6',
firstName: 'Bradley',
lastName: 'Svensson',
ssn: '19831128-5775',
organizations: [
{
id: '53d64944-040c-44ee-9505-879ae05f660e',
name: 'Persson, Andersson and Karlsson',
kaNumber: 234733,
address: {
street: 'Althea allén',
houseNumber: 42,
postalCode: '76986',
city: 'Myrtisby',
kommun: 'Olofströms kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628957,
},
{
id: '0f8445b0-9eb6-432d-9967-0541ea74d9c6',
firstName: 'Heath',
lastName: 'Karlsson',
ssn: '19821114-5302',
organizations: [
{
id: '6bd3806d-b49d-412d-96b7-76ff4ec27a44',
name: 'Olsson, Andersson and Andersson',
kaNumber: 902976,
address: {
street: 'Johansson gatan',
houseNumber: 92,
postalCode: '65702',
city: 'Lessieland',
kommun: 'Motala kommun',
},
},
],
services: [
{
id: '20e09e98-c744-45b3-95ef-54ef51af32c0',
name: 'KVL' as Service,
},
],
authorizations: [],
createdAt: 1623655628957,
},
{
id: '29cfccc4-bf1d-4eaa-88d9-b86e22203bc7',
firstName: 'Mitchel',
lastName: 'Andersson',
ssn: '19680607-4896',
organizations: [
{
id: '11da9de5-2ce2-4364-a1b2-08a263bfc248',
name: 'Eriksson Group',
kaNumber: 975639,
address: {
street: 'Vito allén',
houseNumber: 82,
postalCode: '61048',
city: 'Helsing Consuelo',
kommun: 'Finspångs kommun',
},
},
],
services: [
{
id: '20e09e98-c744-45b3-95ef-54ef51af32c0',
name: 'KVL' as Service,
},
],
authorizations: [],
createdAt: 1623655628957,
},
{
id: '412a7141-82fa-4b98-812f-e092910663af',
firstName: 'Raheem',
lastName: 'Andersson',
ssn: '19820609-8453',
organizations: [
{
id: 'e2d4f74f-c1da-478d-a116-d6dfa1b0183c',
name: 'Larsson - Gustafsson',
kaNumber: 852472,
address: {
street: 'Gust gatan',
houseNumber: 77,
postalCode: '52349',
city: 'Katelynnmora',
kommun: 'Hofors kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628957,
},
{
id: 'a4df2f97-fdaf-4b41-8793-dd84ea631502',
firstName: 'Ricky',
lastName: 'Johansson',
ssn: '19980903-7392',
organizations: [
{
id: 'd5b9d727-4473-47be-bdc0-cc3d6ed85934',
name: 'Svensson, Olsson and Nilsson',
kaNumber: 999419,
address: {
street: 'Eriksson gatan',
houseNumber: 85,
postalCode: '13202',
city: 'Columbia',
kommun: 'Halmstads kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628957,
},
{
id: '1a4cbf66-14f7-45c1-ad64-0df6ec3bdc2c',
firstName: 'Billie',
lastName: 'Andersson',
ssn: '19710304-8866',
organizations: [
{
id: 'fc42fe9c-ad06-46df-9c33-9e61b5c3f881',
name: 'Nilsson, Svensson and Johansson',
kaNumber: 578637,
address: {
street: 'Olsson gatan',
houseNumber: 33,
postalCode: '98821',
city: 'Hacienda Heights',
kommun: 'Motala kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628958,
},
{
id: 'b3703a76-474d-4cb3-b74f-fa41ffe158b5',
firstName: 'Lizzie',
lastName: 'Karlsson',
ssn: '19890729-2332',
organizations: [
{
id: 'd5b9d727-4473-47be-bdc0-cc3d6ed85934',
name: 'Svensson, Olsson and Nilsson',
kaNumber: 999419,
address: {
street: 'Eriksson gatan',
houseNumber: 85,
postalCode: '13202',
city: 'Columbia',
kommun: 'Halmstads kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628958,
},
{
id: 'd1523e59-b400-4b6d-8a4b-f393d70bf2d5',
firstName: 'Cruz',
lastName: 'Gustafsson',
ssn: '19861226-1321',
organizations: [
{
id: '11da9de5-2ce2-4364-a1b2-08a263bfc248',
name: 'Eriksson Group',
kaNumber: 975639,
address: {
street: 'Vito allén',
houseNumber: 82,
postalCode: '61048',
city: 'Helsing Consuelo',
kommun: 'Finspångs kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628958,
},
{
id: '1340f322-7e80-4f59-926a-bde9fc6621fc',
firstName: 'Jeremie',
lastName: 'Svensson',
ssn: '19681107-4830',
organizations: [
{
id: 'fc42fe9c-ad06-46df-9c33-9e61b5c3f881',
name: 'Nilsson, Svensson and Johansson',
kaNumber: 578637,
address: {
street: 'Olsson gatan',
houseNumber: 33,
postalCode: '98821',
city: 'Hacienda Heights',
kommun: 'Motala kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628958,
},
{
id: 'bd207a4b-ab8a-41e9-8492-ddc580dc2cfd',
firstName: 'Mae',
lastName: 'Olsson',
ssn: '19980630-7229',
organizations: [
{
id: '6bd3806d-b49d-412d-96b7-76ff4ec27a44',
name: 'Olsson, Andersson and Andersson',
kaNumber: 902976,
address: {
street: 'Johansson gatan',
houseNumber: 92,
postalCode: '65702',
city: 'Lessieland',
kommun: 'Motala kommun',
},
},
],
services: [
{
id: '20e09e98-c744-45b3-95ef-54ef51af32c0',
name: 'KVL' as Service,
},
],
authorizations: [],
createdAt: 1623655628958,
},
{
id: '5ab9ad0f-fabc-4916-9d9c-7559100866cd',
firstName: 'Mable',
lastName: 'Gustafsson',
ssn: '19821217-3880',
organizations: [
{
id: 'e2d4f74f-c1da-478d-a116-d6dfa1b0183c',
name: 'Larsson - Gustafsson',
kaNumber: 852472,
address: {
street: 'Gust gatan',
houseNumber: 77,
postalCode: '52349',
city: 'Katelynnmora',
kommun: 'Hofors kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628958,
},
{
id: '996c421e-4e93-4d04-961e-5d1268840a2e',
firstName: 'Lonie',
lastName: 'Nilsson',
ssn: '19920429-1095',
organizations: [
{
id: 'b8011410-d7a8-4163-98c8-3262cf0681b9',
name: 'Svensson - Svensson',
kaNumber: 815388,
address: {
street: 'Delphine allén',
houseNumber: 26,
postalCode: '26994',
city: 'En Akeem',
kommun: 'Smedjebackens kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628959,
},
{
id: '24e28f54-8bdd-4ee5-b072-37e25feba220',
firstName: 'Albertha',
lastName: 'Olsson',
ssn: '19900128-8896',
organizations: [
{
id: 'd75cad98-75b5-40e6-8674-765121938928',
name: 'Karlsson, Gustafsson and Svensson',
kaNumber: 619459,
address: {
street: 'Svensson gärdet',
houseNumber: 41,
postalCode: '16444',
city: 'Gustafssonberg',
kommun: 'Hallsbergs kommun',
},
},
],
services: [
{
id: '20e09e98-c744-45b3-95ef-54ef51af32c0',
name: 'KVL' as Service,
},
],
authorizations: [],
createdAt: 1623655628959,
},
{
id: '4de5abb8-bda6-40e2-9b26-6a834e61b543',
firstName: 'Giovanny',
lastName: 'Nilsson',
ssn: '19620130-3009',
organizations: [
{
id: '53d64944-040c-44ee-9505-879ae05f660e',
name: 'Persson, Andersson and Karlsson',
kaNumber: 234733,
address: {
street: 'Althea allén',
houseNumber: 42,
postalCode: '76986',
city: 'Myrtisby',
kommun: 'Olofströms kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628959,
},
{
id: '61ff95f7-175c-48df-94d2-e5e368ba116c',
firstName: 'Meta',
lastName: 'Olsson',
ssn: '19790727-4413',
organizations: [
{
id: '6bd3806d-b49d-412d-96b7-76ff4ec27a44',
name: 'Olsson, Andersson and Andersson',
kaNumber: 902976,
address: {
street: 'Johansson gatan',
houseNumber: 92,
postalCode: '65702',
city: 'Lessieland',
kommun: 'Motala kommun',
},
},
],
services: [
{
id: '20e09e98-c744-45b3-95ef-54ef51af32c0',
name: 'KVL' as Service,
},
],
authorizations: [],
createdAt: 1623655628959,
},
{
id: '3909e35e-22be-4d95-b4f4-a6f34309c7b8',
firstName: 'Candelario',
lastName: 'Svensson',
ssn: '19741125-2817',
organizations: [
{
id: 'd7ba7bb8-2946-4444-b60e-edf4e0cf27dd',
name: 'Eriksson - Gustafsson',
kaNumber: 393573,
address: {
street: 'Juvenal vägen',
houseNumber: 92,
postalCode: '53784',
city: 'Alenaland',
kommun: 'Bromölla kommun',
},
},
],
services: [
{
id: 'a33515e7-045a-4da5-8646-9eed160b18d1',
name: 'KROM' as Service,
},
],
authorizations: [],
createdAt: 1623655628959,
},
];

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EmployeesListComponent } from './employees-list.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeesListComponent],
imports: [CommonModule, RouterModule],
exports: [EmployeesListComponent],
})
export class EmployeesListModule {}

View File

@@ -0,0 +1,45 @@
<msfa-layout>
<section class="employees">
<digi-typography>
<h1>Personal</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam magna neque, interdum vel massa eget, condimentum
rutrum velit. Sed vitae ullamcorper sem. Aliquam malesuada nunc sed purus mollis scelerisque. Curabitur bibendum
leo quis ante porttitor tincidunt. Nam tincidunt imperdiet tortor eu suscipit. Maecenas ut dui est.
</p>
<div class="employees__cta-wrapper">
<digi-ng-link-button afText="Skapa nytt konto" afRoute="/administration/bjuda-in"></digi-ng-link-button>
</div>
<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"
(afOnInput)="setSearchValue($event)"
></digi-form-input-search>
<digi-form-checkbox
class="employees__only-employees-without-authorization"
af-label="Visa endast personer utan behörigheter"
(afOnChange)="setOnlyEmployeesWithoutAuthorization($event)"
></digi-form-checkbox>
</form>
<msfa-employees-list
*ngIf="employeesData$ | async as employeesData; else loadingRef"
[employees]="employeesData.data"
[paginationMeta]="employeesData.meta"
[sort]="sort$ | async"
[order]="order$ | async"
(sorted)="handleEmployeesSort($event)"
(paginated)="setNewPage($event)"
></msfa-employees-list>
</digi-typography>
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar personal"></digi-ng-skeleton-base>
</ng-template>
</section>
</msfa-layout>

View File

@@ -0,0 +1,19 @@
@import 'variables/gutters';
.employees {
&__cta-wrapper {
margin-top: var(--digi--layout--gutter);
}
&__search-wrapper {
display: flex;
flex-direction: column;
max-width: var(--digi--typography--text--max-width);
margin-top: $digi--layout--gutter--l;
margin-bottom: $digi--layout--gutter--xl;
}
&__only-employees-without-authorization {
margin-top: $digi--layout--gutter--l;
}
}

View File

@@ -0,0 +1,30 @@
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 { EmployeesComponent } from './employees.component';
describe('EmployeesComponent', () => {
let component: EmployeesComponent;
let fixture: ComponentFixture<EmployeesComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeesComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(EmployeesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
void expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,51 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum';
import { Employee, EmployeesData } from '@msfa-models/employee.model';
import { Sort } from '@msfa-models/sort.model';
import { EmployeeService } from '@msfa-services/api/employee.service';
import { BehaviorSubject, Observable } from 'rxjs';
@Component({
selector: 'msfa-employees',
templateUrl: './employees.component.html',
styleUrls: ['./employees.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmployeesComponent {
private _searchValue$ = new BehaviorSubject<string>('');
private _onlyEmployeesWithoutAuthorization$ = new BehaviorSubject<boolean>(false);
employeesData$: Observable<EmployeesData> = this.employeeService.employeesData$;
sort$: Observable<Sort<keyof Employee>> = this.employeeService.sort$;
iconType = IconType;
constructor(private employeeService: EmployeeService) {}
get searchValue(): string {
return this._searchValue$.getValue();
}
setSearchFilter(): void {
this.employeeService.setSearchFilter(this.searchValue);
}
setSearchValue($event: CustomEvent<{ target: { value: string } }>): void {
this._searchValue$.next($event.detail.target.value);
}
handleEmployeesSort(key: keyof Employee): void {
this.employeeService.setSort(key);
}
setNewPage(page: number): void {
this.employeeService.setPage(page);
}
get onlyEmployeesWithoutAuthorization(): boolean {
return this._onlyEmployeesWithoutAuthorization$.getValue();
}
setOnlyEmployeesWithoutAuthorization(event: CustomEvent<{ target: { checked: boolean } }>): void {
this._onlyEmployeesWithoutAuthorization$.next(event.detail.target.checked);
this.employeeService.setOnlyEmployeesWithoutAuthorization(this.onlyEmployeesWithoutAuthorization);
}
}

View File

@@ -0,0 +1,26 @@
import { DigiNgLinkButtonModule } from '@af/digi-ng/_link/link-button';
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 { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { EmployeesListModule } from './components/employees-list/employees-list.module';
import { EmployeesComponent } from './employees.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [EmployeesComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: EmployeesComponent }]),
LayoutModule,
DigiNgLinkInternalModule,
DigiNgSkeletonBaseModule,
EmployeesListModule,
DigiNgLinkButtonModule,
FormsModule,
],
})
export class EmployeesModule {}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AvropService } from './avrop.service';
describe('AvropServiceService', () => {
let service: AvropService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AvropService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,98 @@
<msfa-layout>
<section class="call-off" *ngIf="currentStep$ | async; let currentStep; else: loadingRef">
<digi-typography>
<h2>Välj deltagare att tilldela</h2>
<p>Steg {{ currentStep }} av {{ steps }}:</p>
</digi-typography>
<digi-ng-progress-progressbar [afSteps]="steps" afAriaLabel="An aria label" [afActiveStep]="currentStep">
</digi-ng-progress-progressbar>
<div>
<ng-container *ngIf="currentStep == 4">
<h2>Avropet är sparat</h2>
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="goToStep1()">
Tillbaka till nya deltagare
</digi-button>
</ng-container>
<ng-container *ngIf="currentStep == 1">
<msfa-avrop-filters></msfa-avrop-filters>
</ng-container>
<ng-container *ngIf="currentStep == 3">
<h2>Vänligen bekräfta</h2>
</ng-container>
<ng-container *ngIf="currentStep < 4">
<msfa-avrop-table
[selectableDeltagareList]="selectableDeltagareList$ | async"
[selectedDeltagareListInput]="selectedDeltagareList$ | async"
[isLocked]="deltagareListIsLocked$ | async"
(changedSelectedDeltagareList)="updateSelectedDeltagareList($event)"
[handledare]="selectedHandledare$ | async"
[handledareConfirmed]="handledareConfirmed$ | async"
></msfa-avrop-table>
</ng-container>
<ng-container *ngIf="currentStep == 1">
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="lockSelectedDeltagare()">
Lås deltagare
</digi-button>
</ng-container>
<ng-container *ngIf="currentStep == 2">
<h2>Välj handledare</h2>
<ng-container *ngIf="selectableHandledareList$ | async; let selectableHandledareList; else loadingRefSmall">
<select
[value]="(selectedHandledare$ | async)?.id ? (selectedHandledare$ | async)?.id : ''"
(change)="changeHandledare($event)"
>
<option disabled value="">Välj handledare</option>
<option *ngFor="let selectableHandledare of selectableHandledareList" [value]="selectableHandledare?.id">
{{ selectableHandledare?.fullName }}
</option>
</select>
<span *ngIf="selectableHandledareList.length === 0"
>Inga handledare har behörighet till alla markerade deltagare</span
>
</ng-container>
<br /><br />
<digi-button
af-variation="secondary"
af-size="m"
class="employee-form__read-more"
(afOnClick)="unlockSelectedDeltagare()"
>
Tillbaka
</digi-button>
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="confirmHandledare()">
Tilldela
</digi-button>
</ng-container>
<div *ngIf="currentStep == 3">
<br /><br />
<digi-button
af-variation="secondary"
af-size="m"
class="employee-form__read-more"
(afOnClick)="unconfirmHandledare()"
>
Tillbaka
</digi-button>
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="save()"> Spara avrop </digi-button>
</div>
</div>
</section>
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar personal"></digi-ng-skeleton-base>
</ng-template>
<ng-template #loadingRefSmall>
<digi-icon-spinner af-title="Laddar innehåll"></digi-icon-spinner>
</ng-template>
<hr />
</msfa-layout>

View File

@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AvropComponent } from './avrop.component';
describe('CallOffComponent', () => {
let component: AvropComponent;
let fixture: ComponentFixture<AvropComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [AvropComponent],
imports: [RouterTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(AvropComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,63 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
import { Observable } from 'rxjs';
import { AvropService } from './avrop.service';
import { HandledareAvrop } from './models/handledare-avrop';
@Component({
selector: 'msfa-avrop',
templateUrl: './avrop.component.html',
styleUrls: ['./avrop.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AvropComponent {
steps = 3;
currentStep$ = this.avropService.currentStep$;
selectedUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = this.avropService
.selectedUtforandeVerksamheter$;
selectableDeltagareList$: Observable<Avrop[]> = this.avropService.selectableDeltagareList$;
selectedDeltagareList$: Observable<Avrop[]> = this.avropService.selectedDeltagareList$;
selectableHandledareList$: Observable<HandledareAvrop[]> = this.avropService.selectableHandledareList$;
selectedHandledare$: Observable<HandledareAvrop> = this.avropService.selectedHandledare$;
deltagareListIsLocked$: Observable<boolean> = this.avropService.deltagareListIsLocked$;
handledareConfirmed$: Observable<boolean> = this.avropService.handledareIsConfirmed$;
constructor(private avropService: AvropService) {}
updateSelectedDeltagareList(deltagareList: Avrop[]): void {
this.avropService.setSelectedDeltagare(deltagareList);
}
lockSelectedDeltagare(): void {
this.avropService.lockSelectedDeltagare();
}
unlockSelectedDeltagare(): void {
this.avropService.unlockSelectedDeltagare();
}
confirmHandledare(): void {
this.avropService.confirmHandledare();
}
unconfirmHandledare(): void {
this.avropService.unconfirmHandledare();
}
async save(): Promise<void> {
return this.avropService.save();
}
changeHandledare(newHandledare: { target: HTMLInputElement }): void {
const handledareId = newHandledare.target.value;
this.avropService.setHandledareState(handledareId);
}
goToStep1(): void {
this.avropService.goToStep1();
}
}

View File

@@ -0,0 +1,30 @@
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
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 { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { AvropComponent } from './avrop.component';
import { AvropFiltersComponent } from './components/avrop-filters/avrop-filters.component';
import { TemporaryFilterComponent } from './components/avrop-filters/temporary-filter/temporary-filter.component';
import { AvropTableRowComponent } from './components/avrop-table/avrop-table-row/avrop-table-row.component';
import { AvropTableComponent } from './components/avrop-table/avrop-table.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [
AvropComponent,
AvropFiltersComponent,
AvropTableComponent,
AvropTableRowComponent,
TemporaryFilterComponent,
],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: AvropComponent }]),
LayoutModule,
DigiNgProgressProgressbarModule,
DigiNgSkeletonBaseModule,
],
})
export class AvropModule {}

View File

@@ -0,0 +1,194 @@
import { Injectable } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
import { AvropApiService } from '@msfa-services/api/avrop-api.service';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { first, map, switchMap } from 'rxjs/operators';
import { HandledareAvrop } from './models/handledare-avrop';
type Step = 1 | 2 | 3 | 4;
@Injectable({
providedIn: 'root',
})
export class AvropService {
private _selectedTjanster$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
private _selectedUtforandeVerksamheter$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
private _selectedKommuner$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
private _selectedDeltagareList$ = new BehaviorSubject<Avrop[]>([]);
private _deltagareListIsLocked$ = new BehaviorSubject<boolean>(null);
private _selectedHandledare$ = new BehaviorSubject<HandledareAvrop>(null);
private _handledareIsConfirmed$ = new BehaviorSubject<boolean>(false);
private _avropIsSaved$ = new BehaviorSubject<boolean>(false);
selectedTjanster$: Observable<MultiselectFilterOption[]> = this._selectedTjanster$.asObservable();
selectedUtforandeVerksamheter$: Observable<
MultiselectFilterOption[]
> = this._selectedUtforandeVerksamheter$.asObservable();
selectedKommuner$: Observable<MultiselectFilterOption[]> = this._selectedKommuner$.asObservable();
selectableDeltagareList$: Observable<Avrop[]> = combineLatest([
this.selectedTjanster$,
this.selectedUtforandeVerksamheter$,
this.selectedKommuner$,
]).pipe(
switchMap(([selectedTjanster, selectedUtforandeVerksamheter, selectedKommuner]) =>
this.avropApiService.getNyaAvrop$(selectedTjanster, selectedKommuner, selectedUtforandeVerksamheter)
)
);
selectableTjanster$: Observable<MultiselectFilterOption[]> = combineLatest([
this.selectedUtforandeVerksamheter$,
this.selectedKommuner$,
]).pipe(
switchMap(([selectedUtforandeVerksamheter, selectedKommuner]) =>
this.avropApiService.getSelectableTjanster$(selectedKommuner, selectedUtforandeVerksamheter)
)
);
selectableUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = combineLatest([
this.selectedTjanster$,
this.selectedKommuner$,
]).pipe(
switchMap(([selectedTjanster, selectedKommuner]) =>
this.avropApiService.getSelectableUtforandeVerksamheter$(selectedTjanster, selectedKommuner)
)
);
selectableKommuner$: Observable<MultiselectFilterOption[]> = combineLatest([
this.selectedTjanster$,
this.selectedUtforandeVerksamheter$,
]).pipe(
switchMap(([selectedTjanster, selectedUtforandeVerksamheter]) =>
this.avropApiService.getSelectableKommuner$(selectedTjanster, selectedUtforandeVerksamheter)
)
);
selectedDeltagareList$: Observable<Avrop[]> = this._selectedDeltagareList$.asObservable();
deltagareListIsLocked$: Observable<boolean> = this._deltagareListIsLocked$.asObservable();
lockedDeltagareList$: Observable<Avrop[]> = combineLatest([
this.selectedDeltagareList$,
this.deltagareListIsLocked$,
]).pipe(map(([selectedDeltagareList, isLocked]) => (isLocked ? selectedDeltagareList : null)));
selectableHandledareList$: Observable<HandledareAvrop[]> = this.lockedDeltagareList$.pipe(
switchMap(lockedDeltagare => this.avropApiService.getSelectableHandledare$(lockedDeltagare))
);
selectedHandledare$: Observable<HandledareAvrop> = this._selectedHandledare$.asObservable();
handledareIsConfirmed$: Observable<boolean> = this._handledareIsConfirmed$.asObservable();
avropIsSaved$: Observable<boolean> = this._handledareIsConfirmed$.asObservable();
currentStep$: Observable<Step> = combineLatest([
this.handledareIsConfirmed$,
this._deltagareListIsLocked$,
this.avropIsSaved$,
]).pipe(
map(([confirmedHandledare, lockedDeltagareList, avropIsSaved]) =>
AvropService.calculateStep(confirmedHandledare, lockedDeltagareList, avropIsSaved)
)
);
private static calculateStep(
confirmedHandledare: boolean,
deltagareListIsLocked: boolean,
avropIsSaved: boolean
): Step {
if (avropIsSaved && confirmedHandledare && deltagareListIsLocked) {
return 4;
}
if (confirmedHandledare && deltagareListIsLocked) {
return 3;
}
if (deltagareListIsLocked) {
return 2;
}
return 1;
}
setSelectedDeltagare(deltagare: Avrop[]): void {
this._selectedDeltagareList$.next(deltagare);
}
constructor(private avropApiService: AvropApiService) {}
lockSelectedDeltagare(): void {
if ((this._selectedDeltagareList$?.value?.length ?? -1) <= 0) {
throw new Error('För att låsa deltagare behöver några ha markerats först.');
}
this._deltagareListIsLocked$.next(true);
}
unlockSelectedDeltagare(): void {
this._deltagareListIsLocked$.next(false);
}
confirmHandledare(): void {
if (!this._selectedHandledare$?.value) {
throw new Error('För att kunna tilldela behövs en handledare väljas först.');
}
this._handledareIsConfirmed$.next(true);
}
unconfirmHandledare(): void {
this._handledareIsConfirmed$.next(false);
}
async save(): Promise<void> {
if (!this._handledareIsConfirmed$) {
throw new Error('Handledaren måste bekräftas innan avropet kan sparas');
}
if (!this._deltagareListIsLocked$) {
throw new Error('Deltagarlistan måste låsas innan avropet kan sparas');
}
await this.avropApiService.tilldelaHandledare(this._selectedDeltagareList$.value, this._selectedHandledare$.value);
this._avropIsSaved$.next(true);
return;
}
setHandledareState(handledareId: string): void {
this.selectableHandledareList$.pipe(first()).subscribe(handledareList => {
this._selectedHandledare$.next(handledareList.find(handledare => handledare.id === handledareId));
});
}
setSelectedTjanster(selectedFilterOptions: MultiselectFilterOption[]): void {
this._selectedTjanster$.next(selectedFilterOptions);
}
setSelectedUtforandeVerksamheter(selectedFilterOptions: MultiselectFilterOption[]): void {
this._selectedUtforandeVerksamheter$.next(selectedFilterOptions);
}
setSelectedKommuner(selectedFilterOptions: MultiselectFilterOption[]): void {
this._selectedKommuner$.next(selectedFilterOptions);
}
goToStep1(): void {
this._selectedHandledare$.next(null);
this._selectedDeltagareList$.next([]);
this._deltagareListIsLocked$.next(false);
this._handledareIsConfirmed$.next(false);
}
removeKommun(kommunToRemove: MultiselectFilterOption) {
this.setSelectedKommuner(this._selectedKommuner$.value.filter(selectedKommun => selectedKommun !== kommunToRemove));
}
removeUtforandeVerksamhet(utforandeVerksamhetToRemove: MultiselectFilterOption) {
this.setSelectedUtforandeVerksamheter(
this._selectedUtforandeVerksamheter$.value.filter(
selectedUtforandeVerksamhet => selectedUtforandeVerksamhet !== utforandeVerksamhetToRemove
)
);
}
removeTjanst(tjanstToRemove: MultiselectFilterOption) {
this.setSelectedTjanster(this._selectedTjanster$.value.filter(selectedTjanst => selectedTjanst !== tjanstToRemove));
}
}

View File

@@ -0,0 +1,49 @@
<div style="display: flex">
<ng-container *ngIf="selectableTjanster$ | async; let selectableTjanster; else loadingRef">
<msfa-temporary-filter
[filterLabel]="'Tjänster'"
[filterOptions]="selectableTjanster"
[selectedOptions]="selectedTjanster$ | async"
(selectedOptionsChange)="updateSelectedTjanster($event)"
></msfa-temporary-filter>
</ng-container>
<ng-container *ngIf="selectableUtforandeVerksamheter$ | async; let selectableUtforandeVerksamheter; else loadingRef">
<msfa-temporary-filter
[filterLabel]="'Utförande verksamheter'"
[filterOptions]="selectableUtforandeVerksamheter"
[selectedOptions]="selectedUtforandeVerksamheter$ | async"
(selectedOptionsChange)="updateSelectedUtforandeVerksamheter($event)"
></msfa-temporary-filter>
</ng-container>
<ng-container *ngIf="selectableKommuner$ | async; let selectableKommuner; else loadingRef">
<msfa-temporary-filter
[filterLabel]="'Kommuner'"
[filterOptions]="selectableKommuner"
[selectedOptions]="selectedKommuner$ | async"
(selectedOptionsChange)="updateSelectedKommuner($event)"
></msfa-temporary-filter>
</ng-container>
</div>
<br /><br />
<div class="avrop-filters__tags">
<div class="avrop-filters__tag" *ngFor="let kommun of selectedKommuner$ | async">
<digi-tag [afText]="kommun.label" (click)="removeKommun(kommun)" af-no-icon="false" af-size="s"></digi-tag>
</div>
<div class="avrop-filters__tag" *ngFor="let kommun of selectedUtforandeVerksamheter$ | async">
<digi-tag
[afText]="kommun.label"
(click)="removeUtforandeVerksamhet(kommun)"
af-no-icon="false"
af-size="s"
></digi-tag>
</div>
<div class="avrop-filters__tag" *ngFor="let kommun of selectedTjanster$ | async">
<digi-tag [afText]="kommun.label" (click)="removeTjanst(kommun)" af-no-icon="false" af-size="s"></digi-tag>
</div>
</div>
<ng-template #loadingRef>
<div class="avrop-filters__loading-spinner">
<digi-icon-spinner af-title="Laddar innehåll"></digi-icon-spinner>
</div>
</ng-template>

View File

@@ -0,0 +1,17 @@
@import 'variables/gutters';
.avrop-filters {
&__tags {
display: flex;
flex-wrap: wrap;
width: 100%;
}
&__tag {
display: inline-block;
margin-right: $digi--layout--gutter--s;
}
&__loading-spinner {
margin-right: $digi--layout--gutter;
}
}

View File

@@ -0,0 +1,55 @@
import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { AvropFiltersComponent } from './avrop-filters.component';
import { TemporaryFilterComponent } from './temporary-filter/temporary-filter.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AvropService } from '../../avrop.service';
import { of } from 'rxjs';
import { By } from '@angular/platform-browser';
describe('AvropFiltersComponent', () => {
let component: AvropFiltersComponent;
let fixture: ComponentFixture<AvropFiltersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [AvropFiltersComponent, TemporaryFilterComponent],
providers: [AvropService],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AvropFiltersComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show 1 tag if selectedKommuner$ is an observable with one value', () => {
component.selectedKommuner$ = of([{ id: '1', label: 'Stockholm', count: 1 }]);
fixture.detectChanges();
const tags = fixture.debugElement.queryAll(By.css('.avrop-filters--tag'));
expect(tags.length).toBe(1);
});
it('clicking a kommun-tag should trigger removeKommun()', fakeAsync(() => {
jest.spyOn(component, 'removeKommun').mockReturnThis();
component.selectedKommuner$ = of([{ id: '1', label: 'Stockholm', count: 1 }]);
fixture.detectChanges();
const tags = fixture.debugElement.query(By.css('digi-tag'));
tags.nativeElement.click();
tick();
fixture.detectChanges();
expect(component.removeKommun).toHaveBeenCalled();
discardPeriodicTasks();
}));
it('should show loading spinners when filters are loading', () => {
fixture.detectChanges();
const tags = fixture.debugElement.queryAll(By.css('digi-icon-spinner'));
expect(tags.length).toBe(3);
});
});

View File

@@ -0,0 +1,47 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
import { Observable } from 'rxjs';
import { AvropService } from '../../avrop.service';
@Component({
selector: 'msfa-avrop-filters',
templateUrl: './avrop-filters.component.html',
styleUrls: ['./avrop-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AvropFiltersComponent {
selectableTjanster$: Observable<MultiselectFilterOption[]> = this.avropService.selectableTjanster$;
selectableUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = this.avropService
.selectableUtforandeVerksamheter$;
selectableKommuner$: Observable<MultiselectFilterOption[]> = this.avropService.selectableKommuner$;
selectedTjanster$: Observable<MultiselectFilterOption[]> = this.avropService.selectedTjanster$;
selectedUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = this.avropService
.selectedUtforandeVerksamheter$;
selectedKommuner$: Observable<MultiselectFilterOption[]> = this.avropService.selectedKommuner$;
constructor(private avropService: AvropService) {}
updateSelectedTjanster(filterOptions: MultiselectFilterOption[]): void {
this.avropService.setSelectedTjanster(filterOptions);
}
updateSelectedUtforandeVerksamheter(filterOptions: MultiselectFilterOption[]): void {
this.avropService.setSelectedUtforandeVerksamheter(filterOptions);
}
updateSelectedKommuner(filterOptions: MultiselectFilterOption[]): void {
this.avropService.setSelectedKommuner(filterOptions);
}
removeKommun(kommunToRemove: MultiselectFilterOption) {
this.avropService.removeKommun(kommunToRemove);
}
removeUtforandeVerksamhet(utforandeVerksamhetToRemove: MultiselectFilterOption) {
this.avropService.removeUtforandeVerksamhet(utforandeVerksamhetToRemove);
}
removeTjanst(tjanstToRemove: MultiselectFilterOption) {
this.avropService.removeTjanst(tjanstToRemove);
}
}

View File

@@ -0,0 +1,12 @@
<div style="border: 2px solid #CCC; background: #EFEFEF; margin-right: 2rem; padding: 1rem;">
<strong>{{filterLabel}}</strong>
<digi-form-checkbox
*ngFor="let filterOption of filterOptions"
[afLabel]="filterOption.label + ' (' + (filterOption.count || 0) + ')'"
(change)="setOptionState(filterOption, $event.target.checked)"
[afChecked]="isSelected(filterOption)"
>
</digi-form-checkbox>
<digi-button (click)="emitSelectedOptions()" af-size="s">Spara</digi-button>
</div>

View File

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

View File

@@ -0,0 +1,57 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
} from '@angular/core';
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
import { BehaviorSubject, Observable } from 'rxjs';
@Component({
selector: 'msfa-temporary-filter',
templateUrl: './temporary-filter.component.html',
styleUrls: ['./temporary-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TemporaryFilterComponent implements OnInit, OnChanges {
private _selectedAvropFilterOption$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
selectedAvropFilterOptionState$: Observable<MultiselectFilterOption[]>;
@Input() filterLabel: string;
@Input() filterOptions: MultiselectFilterOption[];
@Input() selectedOptions: MultiselectFilterOption[];
@Output() selectedOptionsChange = new EventEmitter<MultiselectFilterOption[]>();
// THIS SHOULD BE REPLACED BY DIGI COMPONENT
ngOnInit(): void {
this._selectedAvropFilterOption$ = new BehaviorSubject<MultiselectFilterOption[]>(this.selectedOptions);
this.selectedAvropFilterOptionState$ = this._selectedAvropFilterOption$.asObservable();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.selectedOptions?.currentValue) {
this._selectedAvropFilterOption$.next(changes.selectedOptions.currentValue);
}
}
isSelected(filterOption: MultiselectFilterOption): boolean {
return this.selectedOptions?.includes(filterOption) ?? false;
}
setOptionState(filterOption: MultiselectFilterOption, isSelected: boolean): void {
if (isSelected) {
return this._selectedAvropFilterOption$.next([...(this._selectedAvropFilterOption$.value ?? []), filterOption]);
}
return this._selectedAvropFilterOption$.next(
this._selectedAvropFilterOption$.value?.filter(item => item != filterOption) ?? []
);
}
emitSelectedOptions(): void {
this.selectedOptionsChange.emit(this._selectedAvropFilterOption$.value);
}
}

View File

@@ -0,0 +1,81 @@
<div class="avrop-table-row">
<div class="avrop-table-row__data-row">
<div class="avrop-table-row__data-column">
<digi-form-checkbox
*ngIf="!isLocked"
class="avrop-table-row__checkbox"
[afLabel]="'Välj sökande'"
[afChecked]="isSelected"
(change)="emitSelectionChange($event.target.checked)"
>
</digi-form-checkbox>
</div>
<div class="avrop-table-row__data-column">
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Namn:</strong>
<span>{{deltagare?.fullName}}</span>
</digi-typography>
</div>
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Tjänst:</strong>
<span>{{deltagare?.tjanst}}</span>
</digi-typography>
</div>
</div>
<div class="avrop-table-row__data-column">
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Startdatum:</strong>
<digi-typography-time *ngIf="deltagare?.startDate" [afDateTime]="deltagare?.startDate"></digi-typography-time>
</digi-typography>
</div>
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Slutdatum:</strong>
<digi-typography-time *ngIf="deltagare?.endDate" [afDateTime]="deltagare?.endDate"></digi-typography-time>
</digi-typography>
</div>
</div>
<div class="avrop-table-row__data-column">
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Språkstöd/Tolk:</strong>
<span>{{deltagare?.sprakstod + '/' + deltagare?.tolkbehov}}</span>
</digi-typography>
</div>
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Utförande adress:</strong>
<span>{{deltagare?.utforandeAdress}}</span>
</digi-typography>
</div>
</div>
<div class="avrop-table-row__data-column avrop-table-row__data-column--bottom-align">
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Spår/nivå:</strong>
<span>{{deltagare?.trackCode}}</span>
</digi-typography>
</div>
</div>
<div *ngIf="handledare" class="avrop-table-row__data-column avrop-table-row__data-column--bottom-align">
<div class="avrop-table__cell">
<digi-typography>
<strong class="avrop-table__label">Vald handledare:</strong>
<span>{{handledare?.fullName}}</span>
</digi-typography>
</div>
</div>
</div>
<digi-button
class="avrop-table-row__close-btn"
*ngIf="handledareConfirmed"
[attr.af-variation]="ButtonVariation.TERTIARY"
(afOnClick)="emitDeltagareDeleted()"
>
Ta bort
<digi-icon-x slot="icon"></digi-icon-x>
</digi-button>
</div>

View File

@@ -0,0 +1,47 @@
@import 'variables/gutters';
.avrop-table-row {
position: relative;
display: flex;
flex-direction: row;
padding: var(--digi--layout--gutter) var(--digi--layout--gutter);
background-color: var(--digi--ui--color--background--secondary);
}
.avrop-table-row__close-btn {
position: absolute;
top: 0;
right: 0;
::ng-deep {
button {
padding: 0.5rem 0.75rem !important;
}
}
}
.avrop-table-row__checkbox {
margin-top: 1.5rem;
}
.avrop-table-row__data-row {
display: flex;
flex-direction: row;
gap: $digi--layout--gutter--xl $digi--layout--gutter--l;
flex-grow: 1;
}
.avrop-table-row__data-column {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l 0;
flex-grow: 1;
}
.avrop-table-row__data-column--bottom-align {
justify-content: flex-end;
}
.avrop-table__label {
display: block;
}

View File

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

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { ButtonVariation } from '../../../enums/button-vatiation.enum';
import { HandledareAvrop } from '../../../models/handledare-avrop';
@Component({
selector: 'msfa-avrop-table-row',
templateUrl: './avrop-table-row.component.html',
styleUrls: ['./avrop-table-row.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AvropTableRowComponent {
@Input() deltagare: Avrop;
@Input() isSelected: boolean;
@Input() isLocked: boolean;
@Input() handledare: HandledareAvrop;
@Input() handledareConfirmed: boolean;
@Output() isSelectedChange = new EventEmitter<boolean>();
@Output() deleteDeltagareClicked = new EventEmitter<void>();
ButtonVariation = ButtonVariation;
emitSelectionChange(isSelected: boolean): void {
this.isSelectedChange.emit(isSelected);
}
emitDeltagareDeleted(): void {
this.deleteDeltagareClicked.emit();
}
}

View File

@@ -0,0 +1,11 @@
<div class="avrop-table">
<msfa-avrop-table-row
*ngFor="let deltagare of deltagareRows"
[deltagare]="deltagare"
[isSelected]="isSelected(deltagare)"
[isLocked]="isLocked"
(isSelectedChange)="isSelectedChange(deltagare, $event)"
[handledare]="handledare"
[handledareConfirmed]="handledareConfirmed"
></msfa-avrop-table-row>
</div>

View File

@@ -0,0 +1,5 @@
.avrop-table {
display: flex;
flex-direction: column;
gap: 1rem;
}

View File

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

View File

@@ -0,0 +1,50 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
import { HandledareAvrop } from '../../models/handledare-avrop';
@Component({
selector: 'msfa-avrop-table',
templateUrl: './avrop-table.component.html',
styleUrls: ['./avrop-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AvropTableComponent implements OnInit {
private _selectedDeltagare$ = new BehaviorSubject<Avrop[]>(null);
selectedDeltagareState$: Observable<Avrop[]> = this._selectedDeltagare$.asObservable();
@Input() selectableDeltagareList: Avrop[];
@Input() selectedDeltagareListInput: Avrop[];
@Input() handledare: HandledareAvrop;
@Input() isLocked: boolean;
@Input() handledareConfirmed: boolean;
@Output() changedSelectedDeltagareList = new EventEmitter<Avrop[]>();
get deltagareRows(): Avrop[] {
return this.isLocked ? this.selectedDeltagareListInput : this.selectableDeltagareList;
}
ngOnInit(): void {
this._selectedDeltagare$
.pipe(filter(x => !!x))
.subscribe(selectedDeltagare => this.changedSelectedDeltagareList.emit(selectedDeltagare));
// TODO lägg till unusubscribeOnDestroy
}
isSelected(deltagare: Avrop): boolean {
return this.selectedDeltagareListInput?.includes(deltagare) ?? false;
}
isSelectedChange(deltagare: Avrop, isSelected: boolean): void {
if (isSelected) {
return this._selectedDeltagare$.next([
...(this._selectedDeltagare$.value?.filter(deltagareInList => deltagareInList != deltagare) ?? []),
deltagare,
]);
}
return this._selectedDeltagare$.next(
this._selectedDeltagare$.value?.filter(deltagareInList => deltagareInList != deltagare) ?? []
);
}
}

View File

@@ -0,0 +1,5 @@
export enum ButtonVariation {
PRIMARY = 'primary',
SECONDARY = 'secondary',
TERTIARY = 'tertiary',
}

View File

@@ -0,0 +1,4 @@
export interface HandledareAvrop {
fullName: string;
id: string;
}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DeltagareComponent } from './deltagare.component';
const routes: Routes = [
{
path: '',
component: DeltagareComponent,
},
{
path: ':deltagareId',
loadChildren: () => import('./pages/deltagare-card/deltagare-card.module').then(m => m.DeltagareCardModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DeltagareRoutingModule {}

View File

@@ -0,0 +1,22 @@
<msfa-layout>
<digi-typography>
<section class="deltagare">
<h1>Deltagarlista</h1>
<p>
Här ser du en lista på de deltagare du är tilldelad. Klicka på deltagarens namn för att öppna och se mer
information om deltagarna.
</p>
<ul>
<li><a routerLink="1">Klicka för att gå till sokandeId 1</a></li>
<li><a routerLink="2">Klicka för att gå till sokandeId 2</a></li>
<li><a routerLink="3">Klicka för att gå till sokandeId 3</a></li>
<li><a routerLink="1000">Klicka för att gå till sokandeId 1000</a></li>
</ul>
<ul *ngIf="allDeltagare$ | async as allDeltagare">
<li *ngFor="let deltagare of allDeltagare"><a [routerLink]="deltagare.id">{{deltagare.fullName}}</a></li>
</ul>
</section>
</digi-typography>
</msfa-layout>

View File

@@ -0,0 +1,2 @@
.deltagare {
}

View File

@@ -0,0 +1,28 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { DeltagareComponent } from './deltagare.component';
describe('DeltagareComponent', () => {
let component: DeltagareComponent;
let fixture: ComponentFixture<DeltagareComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [DeltagareComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DeltagareCompact } from '@msfa-models/deltagare.model';
import { DeltagareService } from '@msfa-services/api/deltagare.service';
import { Observable } from 'rxjs';
@Component({
selector: 'msfa-deltagare',
templateUrl: './deltagare.component.html',
styleUrls: ['./deltagare.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareComponent {
allDeltagare$: Observable<DeltagareCompact[]> = this.deltagareService.allDeltagare$;
constructor(private deltagareService: DeltagareService) {}
}

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { DeltagareRoutingModule } from './deltagare-routing.module';
import { DeltagareComponent } from './deltagare.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareComponent],
imports: [CommonModule, DeltagareRoutingModule, LayoutModule],
})
export class DeltagareModule {}

View File

@@ -0,0 +1,211 @@
<msfa-layout>
<section class="deltagare-card">
<div *ngIf="deltagare$ | async as deltagare; else loadingRef">
<digi-typography>
<header class="deltagare-card__header">
<msfa-back-link [route]="['/deltagare']">Tillbaka till deltagarlistan</msfa-back-link>
<h1>Deltagarinformation</h1>
</header>
<digi-navigation-tabs af-aria-label="Deltagarinformation">
<digi-navigation-tab af-aria-label="Om deltagaren" af-id="deltagare-card-personuppgifter">
<div class="deltagare-card__tab-contents">
<div class="deltagare-card__tab-column">
<h2>Personuppgifter</h2>
<dl>
<dt>Namn:</dt>
<dd *ngIf="deltagare.fullName; else emptyDD">{{ deltagare.fullName }}</dd>
<dt>Personnummer:</dt>
<dd *ngIf="deltagare.ssn; else emptyDD">
<msfa-hide-text
symbols="********-****"
[changingText]="deltagare.ssn"
ariaLabelType="personnummer"
></msfa-hide-text>
</dd>
<ng-container *ngFor="let address of deltagare.addresses">
<dt>{{address.type}}:</dt>
<dd>
<address>
{{ address.street }}<br />
{{ address.postalCode }} {{ address.city }}
</address>
</dd>
</ng-container>
<dt>Telefon:</dt>
<ng-container *ngIf="deltagare.phoneNumbers?.length; else emptyDD">
<ng-container *ngFor="let phoneNumber of deltagare.phoneNumbers">
<dd>{{ phoneNumber.type }}: {{phoneNumber.number}}</dd>
</ng-container>
</ng-container>
<dt>E-postadress:</dt>
<dd *ngIf="deltagare.email; else emptyDD">
<a href="mailto:{{deltagare.email}}">{{ deltagare.email }}</a>
</dd>
</dl>
</div>
<div class="deltagare-card__tab-column">
<h2>Behov och språk</h2>
<dl>
<dt>Funktionsnedsättningar:</dt>
<ng-container *ngIf="deltagare.disabilities.length; else emptyDD">
<dd *ngFor="let disability of deltagare.disabilities">
<span>{{ disability.title }}</span>
<digi-ng-popover
*ngIf="disability.description"
class="deltagare-card__popover"
[afRelativeIconSize]="true"
>{{ disability.description }}</digi-ng-popover
>
</dd>
</ng-container>
</dl>
<dl>
<dt>Tolkbehov:</dt>
<dd>
{{deltagare.avropInformation.tolkbehov ? 'Ja (' + deltagare.avropInformation.tolkbehov + ')' :
'Nej'}}
</dd>
<dt>Språkstöd:</dt>
<dd>
{{deltagare.avropInformation.sprakstod ? 'Ja (' + deltagare.avropInformation.sprakstod + ')' :
'Nej'}}
</dd>
<dt>Språk jag kan använda på jobbet:</dt>
<dd *ngIf="deltagare.workLanguages.length else emptyDD">{{ deltagare.workLanguages.join(', ')}}</dd>
</dl>
</div>
<div class="deltagare-card__tab-column">
<h2>Om tjänsten</h2>
<dl>
<dt>Tillhörande tjänst:</dt>
<dd *ngIf="deltagare.avropInformation.tjanst; else emptyDD">
{{ deltagare.avropInformation.tjanst }}
</dd>
<dt>Datum för tjänstens början:</dt>
<dd *ngIf="deltagare.avropInformation.startDate; else emptyDD">
<digi-typography-time [afDateTime]="deltagare.avropInformation.startDate"></digi-typography-time>
</dd>
<dt>Datum för tjänstens slut:</dt>
<dd *ngIf="deltagare.avropInformation.endDate; else emptyDD">
<digi-typography-time [afDateTime]="deltagare.avropInformation.endDate"></digi-typography-time>
</dd>
<dt>Deltagandefrekvens:</dt>
<dd *ngIf="deltagare.avropInformation.participationFrequency; else emptyDD">
{{ deltagare.avropInformation.participationFrequency }}
</dd>
<dt>Nivå:</dt>
<dd *ngIf="deltagare.avropInformation.participationFrequency; else emptyDD">
{{ deltagare.avropInformation.trackName }}
</dd>
<dt>Utförande verksamhet:</dt>
<dd *ngIf="deltagare.avropInformation.utforandeVerksamhet; else emptyDD">
{{ deltagare.avropInformation.utforandeVerksamhet }}
</dd>
<dt>Utförande adress:</dt>
<dd *ngIf="deltagare.avropInformation.utforandeAdress; else emptyDD">
{{ deltagare.avropInformation.utforandeAdress }}
</dd>
<dt>Genomförandereferens:</dt>
<dd *ngIf="deltagare.avropInformation.genomforandeReferens; else emptyDD">
{{ deltagare.avropInformation.genomforandeReferens }}
</dd>
</dl>
</div>
</div>
</digi-navigation-tab>
<digi-navigation-tab af-aria-label="Erfarenheter" af-id="deltagare-card-matchningsuppgifter">
<div class="deltagare-card__tab-contents">
<div class="deltagare-card__tab-column">
<h2>Arbetslivserfarenhet</h2>
<ng-container *ngIf="firstVisibleWorkExperiences$ | async as firstVisibleWorkExperiences;">
<ul
class="deltagare-card__experience-list"
*ngIf="firstVisibleWorkExperiences.length; else emptyText;"
>
<li *ngFor="let workExperience of firstVisibleWorkExperiences">
<h3 class="deltagare-card__subheading">{{ workExperience.employer }}</h3>
<digi-typography-time [afDateTime]="workExperience.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="workExperience.dateTo"></digi-typography-time><br />
{{ workExperience.profession }}
<p>{{ workExperience.description }}</p>
</li>
</ul>
</ng-container>
<ng-container *ngIf="hiddenWorkExperiences$ | async as hiddenWorkExperiences">
<digi-ng-layout-expansion-panel
class="deltagare-card__accordion"
[afExpanded]="accordionExpanded"
(click)="toggleAccordionExpanded()"
*ngIf="hiddenWorkExperiences.length"
>
<span class="deltagare-card__accordion-trigger" data-slot-trigger
>{{ accordionExpanded ? 'Dölj' : 'Visa' }} fler arbetsgivare</span
>
<ul class="deltagare-card__experience-list">
<li *ngFor="let workExperience of hiddenWorkExperiences">
<h3 class="deltagare-card__subheading">{{ workExperience.employer }}</h3>
<digi-typography-time [afDateTime]="workExperience.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="workExperience.dateTo"></digi-typography-time><br />
{{ workExperience.profession }}
<p>{{ workExperience.description }}</p>
</li>
</ul>
</digi-ng-layout-expansion-panel>
</ng-container>
</div>
<div class="deltagare-card__tab-column">
<h2>Utbildning</h2>
<dl>
<dt>Högsta utbildningsnivå:</dt>
<dd *ngIf="deltagare.highestEducation.level; else emptyDD">
{{ deltagare.highestEducation.level.description }}: {{ deltagare.highestEducation.sunKod.description
}}
</dd>
<h3 class="deltagare-card__subheading deltagare-card__subheading--with-margin">Utbildningar:</h3>
<ul class="deltagare-card__experience-list" *ngIf="deltagare.educations.length; else emptyText">
<li *ngFor="let education of deltagare.educations">
<h4 class="deltagare-card__subheading">{{ education.organizer }}</h4>
<digi-typography-time [afDateTime]="education.dateFrom"></digi-typography-time> -
<digi-typography-time [afDateTime]="education.dateFrom"></digi-typography-time><br />
{{ education.education}}
<p>{{ education.description }}</p>
</li>
</ul>
</dl>
</div>
<div class="deltagare-card__tab-column">
<h2>Körkortsinformation</h2>
<dl>
<dt>Har körkort</dt>
<dd>{{deltagare.driversLicense.licenses.length ? 'Ja' : 'Nej'}}</dd>
<ng-container *ngIf="deltagare.driversLicense.licenses.length">
<dt>Körkortsklasser</dt>
<dd>{{deltagare.driversLicense.licenses.join(', ')}}</dd>
<dt>Tillgång till bil</dt>
<dd>{{deltagare.driversLicense.accessToCar ? 'Ja' : 'Nej'}}</dd>
</ng-container>
</dl>
</div>
</div>
</digi-navigation-tab>
</digi-navigation-tabs>
</digi-typography>
</div>
</section>
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar deltagarinformation"></digi-ng-skeleton-base>
</ng-template>
<ng-template #emptyDD>
<dd>
<span aria-hidden="true">-</span>
<span class="msfa__a11y-sr-only">Info saknas</span>
</dd>
</ng-template>
<ng-template #emptyText>
<p>
<span aria-hidden="true">-</span>
<span class="msfa__a11y-sr-only">Info saknas</span>
</p>
</ng-template>
</msfa-layout>

View File

@@ -0,0 +1,62 @@
@import 'variables/gutters';
@import 'mixins/list';
.deltagare-card {
&__tab-contents {
display: flex;
gap: $digi--layout--gutter--l;
margin: 0 $digi--layout--gutter--l;
}
&__tab-column {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
}
dd {
margin: 0 0 1rem;
}
dt,
&__subheading {
font-size: var(--digi--typography--font-size--desktop);
font-weight: var(--digi--typography--font-weight--semibold);
margin: var(--digi--layout--gutter--s) 0 0;
&--with-margin {
font-size: var(--digi--typography--font-size--h3);
margin-bottom: var(--digi--layout--gutter--s);
}
}
&__experience-list {
@include msfa__reset-list;
}
&__accordion {
display: block;
margin-top: var(--digi--layout--gutter);
}
&__accordion-trigger {
font-weight: var(--digi--typography--font-weight--semibold);
}
&__popover {
display: inline-block;
margin-left: var(--digi--layout--gutter--s);
}
&__header,
&__footer {
display: flex;
flex-direction: row-reverse;
align-items: center;
justify-content: space-between;
}
&__footer {
margin-top: $digi--layout--gutter--l;
}
}

View File

@@ -0,0 +1,28 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { DeltagareCardComponent } from './deltagare-card.component';
describe('DeltagareCardComponent', () => {
let component: DeltagareCardComponent;
let fixture: ComponentFixture<DeltagareCardComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [DeltagareCardComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IconType } from '@msfa-enums/icon-type.enum';
import { Deltagare } from '@msfa-models/deltagare.model';
import { WorkExperience } from '@msfa-models/work-experience.model';
import { DeltagareService } from '@msfa-services/api/deltagare.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'msfa-deltagare-card',
templateUrl: './deltagare-card.component.html',
styleUrls: ['./deltagare-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareCardComponent {
deltagare$: Observable<Deltagare> = this.deltagareService.deltagare$;
firstVisibleWorkExperiences$: Observable<WorkExperience[]> = this.deltagare$.pipe(
map(deltagare => deltagare.workExperiences.slice(0, 2))
);
hiddenWorkExperiences$: Observable<WorkExperience[]> = this.deltagare$.pipe(
map(deltagare => deltagare.workExperiences.slice(2))
);
iconType = IconType;
accordionExpanded = false;
constructor(private activatedRoute: ActivatedRoute, private deltagareService: DeltagareService) {
this.deltagareService.setCurrentDeltagareId(this.activatedRoute.snapshot.params.deltagareId);
}
toggleAccordionExpanded(): void {
this.accordionExpanded = !this.accordionExpanded;
}
}

View File

@@ -0,0 +1,31 @@
import { DigiNgLayoutExpansionPanelModule } from '@af/digi-ng/_layout/layout-expansion-panel';
import { DigiNgLinkInternalModule } from '@af/digi-ng/_link/link-internal';
import { DigiNgPopoverModule } from '@af/digi-ng/_popover/popover';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
import { IconModule } from '@msfa-shared/components/icon/icon.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { DeltagareCardComponent } from './deltagare-card.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareCardComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: DeltagareCardComponent }]),
LayoutModule,
DigiNgLinkInternalModule,
IconModule,
BackLinkModule,
DigiNgLayoutExpansionPanelModule,
HideTextModule,
DigiNgSkeletonBaseModule,
DigiNgPopoverModule,
],
exports: [DeltagareCardComponent],
})
export class DeltagareCardModule {}

View File

@@ -0,0 +1,8 @@
<digi-typography>
<section class="logout">
<h1>Du har nu loggats ut</h1>
<p>
<a [routerLink]="loginUrl">Logga in</a>
</p>
</section>
</digi-typography>

View File

@@ -0,0 +1,3 @@
.logout {
margin: 3rem;
}

View File

@@ -0,0 +1,28 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LogoutComponent } from './logout.component';
describe('LogoutComponent', () => {
let component: LogoutComponent;
let fixture: ComponentFixture<LogoutComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [LogoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(LogoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { environment } from '@msfa-environment';
import { AuthenticationService } from '@msfa-services/api/authentication.service';
@Component({
selector: 'msfa-logout',
templateUrl: './logout.component.html',
styleUrls: ['./logout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LogoutComponent implements OnInit {
loginUrl = environment.loginUrl;
constructor(private authenticationService: AuthenticationService) {}
ngOnInit(): void {
this.authenticationService.logout();
}
}

View File

@@ -0,0 +1,12 @@
import { DigiNgButtonModule } from '@af/digi-ng/_button/button';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LogoutComponent } from './logout.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [LogoutComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: LogoutComponent }]), DigiNgButtonModule],
})
export class LogoutModule {}

View File

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

View File

@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { MessagesComponent } from './messages.component';
describe('MessagesComponent', () => {
let component: MessagesComponent;
let fixture: ComponentFixture<MessagesComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [MessagesComponent],
imports: [RouterTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(MessagesComponent);
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-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessagesComponent {}

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,13 @@
<digi-typography>
<section class="mock-login">
<h1>Mock login</h1>
<p>
Simulera att man loggar in och blir redirectad till startsidan med en Authorization-code som sedan används för att
hämta authentication-token:
</p>
<digi-ng-button routerLink="/" [queryParams]="{ code: 'auth_code_from_CIAM_with_all_permissions'}">
Logga in med fullständiga rättigheter
</digi-ng-button>
</section>
</digi-typography>

View File

@@ -0,0 +1,3 @@
.mock-login {
margin: 5rem;
}

View File

@@ -0,0 +1,27 @@
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MockLoginComponent } from './mock-login.component';
describe('ReleasesComponent', () => {
let component: MockLoginComponent;
let fixture: ComponentFixture<MockLoginComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
declarations: [MockLoginComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(MockLoginComponent);
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-mock-login',
templateUrl: './mock-login.component.html',
styleUrls: ['./mock-login.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MockLoginComponent {}

View File

@@ -0,0 +1,21 @@
import { DigiNgButtonModule } from '@af/digi-ng/_button/button';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { MockLoginComponent } from './mock-login.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [MockLoginComponent],
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: MockLoginComponent,
},
]),
DigiNgButtonModule,
],
})
export class MockLoginModule {}

View File

@@ -0,0 +1,12 @@
<msfa-layout>
<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>
</section>
</digi-typography>
</msfa-layout>

Some files were not shown because too many files have changed in this diff Show More