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
@@ -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 {}
@@ -0,0 +1,3 @@
<router-outlet></router-outlet>
<msfa-toast-list></msfa-toast-list>
@@ -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();
});
});
@@ -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 {}
+25
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 {}
@@ -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>
@@ -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);
}
}
@@ -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();
});
});
@@ -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);
}
}
@@ -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 {}
@@ -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>
@@ -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);
}
}
@@ -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();
});
});
@@ -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);
}
}
@@ -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 {}
@@ -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 {}
@@ -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();
});
});
@@ -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 {}
@@ -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 {}
@@ -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>
@@ -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);
}
}
}
@@ -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();
});
});
@@ -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) : []);
}
}
@@ -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 {}
@@ -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>
@@ -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;
}
}
@@ -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();
});
});
@@ -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();
}
}
}
@@ -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 {}
@@ -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>
@@ -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);
}
}
@@ -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();
});
});
@@ -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('');
}
}
@@ -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 {}
@@ -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>
@@ -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);
}
}
@@ -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);
});
});
});
@@ -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);
}
}
@@ -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,
},
];
@@ -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 {}
@@ -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>
@@ -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;
}
}
@@ -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();
});
});
@@ -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);
}
}
@@ -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 {}
@@ -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();
});
});
@@ -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>
@@ -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();
});
});
@@ -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();
}
}
@@ -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 {}
@@ -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));
}
}
@@ -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>
@@ -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;
}
}
@@ -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);
});
});
@@ -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);
}
}
@@ -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>
@@ -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();
});
});
@@ -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);
}
}
@@ -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>
@@ -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;
}
@@ -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();
});
});
@@ -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();
}
}
@@ -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>
@@ -0,0 +1,5 @@
.avrop-table {
display: flex;
flex-direction: column;
gap: 1rem;
}
@@ -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();
});
});
@@ -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) ?? []
);
}
}
@@ -0,0 +1,5 @@
export enum ButtonVariation {
PRIMARY = 'primary',
SECONDARY = 'secondary',
TERTIARY = 'tertiary',
}
@@ -0,0 +1,4 @@
export interface HandledareAvrop {
fullName: string;
id: string;
}
@@ -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 {}
@@ -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>
@@ -0,0 +1,2 @@
.deltagare {
}
@@ -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();
});
});
@@ -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) {}
}
@@ -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 {}
@@ -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>
@@ -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;
}
}
@@ -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();
});
});
@@ -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;
}
}
@@ -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 {}
@@ -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>
@@ -0,0 +1,3 @@
.logout {
margin: 3rem;
}
@@ -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();
});
});
@@ -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();
}
}
@@ -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 {}
@@ -0,0 +1,3 @@
<msfa-layout>
<section class="messages">Meddelanden funkar!</section>
</msfa-layout>
@@ -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();
});
});
@@ -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 {}
@@ -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 {}
@@ -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>
@@ -0,0 +1,3 @@
.mock-login {
margin: 5rem;
}
@@ -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();
});
});
@@ -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 {}
@@ -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 {}
@@ -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