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:
17
apps/mina-sidor-fa/.browserslistrc
Normal file
17
apps/mina-sidor-fa/.browserslistrc
Normal file
@@ -0,0 +1,17 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||
41
apps/mina-sidor-fa/.eslintrc.json
Normal file
41
apps/mina-sidor-fa/.eslintrc.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"extends": ["../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"excludedFiles": ["*.spec.ts"],
|
||||
"extends": [
|
||||
"plugin:@nrwl/nx/angular",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"parserOptions": { "project": ["apps/mina-sidor-fa/tsconfig.*?.json"] },
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "msfa",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "msfa",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nrwl/nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
23
apps/mina-sidor-fa/jest.config.js
Normal file
23
apps/mina-sidor-fa/jest.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
displayName: 'mina-sidor-fa',
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsConfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
astTransformers: {
|
||||
before: [
|
||||
'jest-preset-angular/build/InlineFilesTransformer',
|
||||
'jest-preset-angular/build/StripStylesTransformer',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
coverageDirectory: '../../coverage/apps/mina-sidor-fa',
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
|
||||
'jest-preset-angular/build/AngularSnapshotSerializer.js',
|
||||
'jest-preset-angular/build/HTMLCommentSerializer.js',
|
||||
],
|
||||
};
|
||||
85
apps/mina-sidor-fa/src/app/app-routing.module.ts
Normal file
85
apps/mina-sidor-fa/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
|
||||
import { environment } from '@msfa-environment';
|
||||
import { AuthGuard } from '@msfa-guards/auth.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
data: { title: '' },
|
||||
loadChildren: () => import('./pages/start/start.module').then(m => m.StartModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'administration',
|
||||
data: { title: 'Administration' },
|
||||
loadChildren: () => import('./pages/administration/administration.module').then(m => m.AdministrationModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'deltagare',
|
||||
data: { title: 'Deltagare' },
|
||||
loadChildren: () => import('./pages/deltagare/deltagare.module').then(m => m.DeltagareModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'avrop',
|
||||
data: { title: 'Avrop' },
|
||||
loadChildren: () => import('./pages/avrop/avrop.module').then(m => m.AvropModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'meddelanden',
|
||||
data: { title: 'Meddelanden' },
|
||||
loadChildren: () => import('./pages/messages/messages.module').then(m => m.MessagesModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'statistik',
|
||||
data: { title: 'Statistik' },
|
||||
loadChildren: () => import('./pages/statistics/statistics.module').then(m => m.StatisticsModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'installningar',
|
||||
data: { title: 'Inställningar' },
|
||||
loadChildren: () => import('./pages/settings/settings.module').then(m => m.SettingsModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'releases',
|
||||
data: { title: 'Releases' },
|
||||
loadChildren: () => import('./pages/releases/releases.module').then(m => m.ReleasesModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'logout',
|
||||
data: { title: 'Logga ut' },
|
||||
loadChildren: () => import('./pages/logout/logout.module').then(m => m.LogoutModule),
|
||||
},
|
||||
];
|
||||
|
||||
if (!environment.production) {
|
||||
routes.push({
|
||||
path: 'mock-login',
|
||||
data: { title: 'Mock login' },
|
||||
loadChildren: () => import('./pages/mock-login/mock-login.module').then(m => m.MockLoginModule),
|
||||
});
|
||||
}
|
||||
|
||||
routes.push({
|
||||
path: '**',
|
||||
data: { title: 'Sidan hittas inte' },
|
||||
loadChildren: () => import('./pages/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule),
|
||||
canActivate: [AuthGuard],
|
||||
});
|
||||
|
||||
const options: ExtraOptions = {
|
||||
useHash: false,
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, options)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
3
apps/mina-sidor-fa/src/app/app.component.html
Normal file
3
apps/mina-sidor-fa/src/app/app.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<msfa-toast-list></msfa-toast-list>
|
||||
0
apps/mina-sidor-fa/src/app/app.component.scss
Normal file
0
apps/mina-sidor-fa/src/app/app.component.scss
Normal file
21
apps/mina-sidor-fa/src/app/app.component.spec.ts
Normal file
21
apps/mina-sidor-fa/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [AppComponent],
|
||||
imports: [RouterTestingModule, HttpClientTestingModule],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
});
|
||||
9
apps/mina-sidor-fa/src/app/app.component.ts
Normal file
9
apps/mina-sidor-fa/src/app/app.component.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppComponent {}
|
||||
25
apps/mina-sidor-fa/src/app/app.module.ts
Normal file
25
apps/mina-sidor-fa/src/app/app.module.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
98
apps/mina-sidor-fa/src/app/pages/avrop/avrop.component.html
Normal file
98
apps/mina-sidor-fa/src/app/pages/avrop/avrop.component.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<msfa-layout>
|
||||
<section class="call-off" *ngIf="currentStep$ | async; let currentStep; else: loadingRef">
|
||||
<digi-typography>
|
||||
<h2>Välj deltagare att tilldela</h2>
|
||||
<p>Steg {{ currentStep }} av {{ steps }}:</p>
|
||||
</digi-typography>
|
||||
<digi-ng-progress-progressbar [afSteps]="steps" afAriaLabel="An aria label" [afActiveStep]="currentStep">
|
||||
</digi-ng-progress-progressbar>
|
||||
|
||||
<div>
|
||||
<ng-container *ngIf="currentStep == 4">
|
||||
<h2>Avropet är sparat</h2>
|
||||
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="goToStep1()">
|
||||
Tillbaka till nya deltagare
|
||||
</digi-button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStep == 1">
|
||||
<msfa-avrop-filters></msfa-avrop-filters>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentStep == 3">
|
||||
<h2>Vänligen bekräfta</h2>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentStep < 4">
|
||||
<msfa-avrop-table
|
||||
[selectableDeltagareList]="selectableDeltagareList$ | async"
|
||||
[selectedDeltagareListInput]="selectedDeltagareList$ | async"
|
||||
[isLocked]="deltagareListIsLocked$ | async"
|
||||
(changedSelectedDeltagareList)="updateSelectedDeltagareList($event)"
|
||||
[handledare]="selectedHandledare$ | async"
|
||||
[handledareConfirmed]="handledareConfirmed$ | async"
|
||||
></msfa-avrop-table>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStep == 1">
|
||||
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="lockSelectedDeltagare()">
|
||||
Lås deltagare
|
||||
</digi-button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentStep == 2">
|
||||
<h2>Välj handledare</h2>
|
||||
<ng-container *ngIf="selectableHandledareList$ | async; let selectableHandledareList; else loadingRefSmall">
|
||||
<select
|
||||
[value]="(selectedHandledare$ | async)?.id ? (selectedHandledare$ | async)?.id : ''"
|
||||
(change)="changeHandledare($event)"
|
||||
>
|
||||
<option disabled value="">Välj handledare</option>
|
||||
|
||||
<option *ngFor="let selectableHandledare of selectableHandledareList" [value]="selectableHandledare?.id">
|
||||
{{ selectableHandledare?.fullName }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<span *ngIf="selectableHandledareList.length === 0"
|
||||
>Inga handledare har behörighet till alla markerade deltagare</span
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<br /><br />
|
||||
<digi-button
|
||||
af-variation="secondary"
|
||||
af-size="m"
|
||||
class="employee-form__read-more"
|
||||
(afOnClick)="unlockSelectedDeltagare()"
|
||||
>
|
||||
Tillbaka
|
||||
</digi-button>
|
||||
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="confirmHandledare()">
|
||||
Tilldela
|
||||
</digi-button>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="currentStep == 3">
|
||||
<br /><br />
|
||||
<digi-button
|
||||
af-variation="secondary"
|
||||
af-size="m"
|
||||
class="employee-form__read-more"
|
||||
(afOnClick)="unconfirmHandledare()"
|
||||
>
|
||||
Tillbaka
|
||||
</digi-button>
|
||||
<digi-button af-size="m" class="employee-form__read-more" (afOnClick)="save()"> Spara avrop </digi-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ng-template #loadingRef>
|
||||
<digi-ng-skeleton-base [afCount]="3" afText="Laddar personal"></digi-ng-skeleton-base>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingRefSmall>
|
||||
<digi-icon-spinner af-title="Laddar innehåll"></digi-icon-spinner>
|
||||
</ng-template>
|
||||
|
||||
<hr />
|
||||
</msfa-layout>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
63
apps/mina-sidor-fa/src/app/pages/avrop/avrop.component.ts
Normal file
63
apps/mina-sidor-fa/src/app/pages/avrop/avrop.component.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { Avrop } from '@msfa-models/avrop.model';
|
||||
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AvropService } from './avrop.service';
|
||||
import { HandledareAvrop } from './models/handledare-avrop';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-avrop',
|
||||
templateUrl: './avrop.component.html',
|
||||
styleUrls: ['./avrop.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AvropComponent {
|
||||
steps = 3;
|
||||
|
||||
currentStep$ = this.avropService.currentStep$;
|
||||
|
||||
selectedUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = this.avropService
|
||||
.selectedUtforandeVerksamheter$;
|
||||
selectableDeltagareList$: Observable<Avrop[]> = this.avropService.selectableDeltagareList$;
|
||||
selectedDeltagareList$: Observable<Avrop[]> = this.avropService.selectedDeltagareList$;
|
||||
selectableHandledareList$: Observable<HandledareAvrop[]> = this.avropService.selectableHandledareList$;
|
||||
selectedHandledare$: Observable<HandledareAvrop> = this.avropService.selectedHandledare$;
|
||||
deltagareListIsLocked$: Observable<boolean> = this.avropService.deltagareListIsLocked$;
|
||||
handledareConfirmed$: Observable<boolean> = this.avropService.handledareIsConfirmed$;
|
||||
|
||||
constructor(private avropService: AvropService) {}
|
||||
|
||||
updateSelectedDeltagareList(deltagareList: Avrop[]): void {
|
||||
this.avropService.setSelectedDeltagare(deltagareList);
|
||||
}
|
||||
|
||||
lockSelectedDeltagare(): void {
|
||||
this.avropService.lockSelectedDeltagare();
|
||||
}
|
||||
|
||||
unlockSelectedDeltagare(): void {
|
||||
this.avropService.unlockSelectedDeltagare();
|
||||
}
|
||||
|
||||
confirmHandledare(): void {
|
||||
this.avropService.confirmHandledare();
|
||||
}
|
||||
|
||||
unconfirmHandledare(): void {
|
||||
this.avropService.unconfirmHandledare();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
return this.avropService.save();
|
||||
}
|
||||
|
||||
changeHandledare(newHandledare: { target: HTMLInputElement }): void {
|
||||
const handledareId = newHandledare.target.value;
|
||||
|
||||
this.avropService.setHandledareState(handledareId);
|
||||
}
|
||||
|
||||
goToStep1(): void {
|
||||
this.avropService.goToStep1();
|
||||
}
|
||||
}
|
||||
30
apps/mina-sidor-fa/src/app/pages/avrop/avrop.module.ts
Normal file
30
apps/mina-sidor-fa/src/app/pages/avrop/avrop.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
|
||||
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
|
||||
import { AvropComponent } from './avrop.component';
|
||||
import { AvropFiltersComponent } from './components/avrop-filters/avrop-filters.component';
|
||||
import { TemporaryFilterComponent } from './components/avrop-filters/temporary-filter/temporary-filter.component';
|
||||
import { AvropTableRowComponent } from './components/avrop-table/avrop-table-row/avrop-table-row.component';
|
||||
import { AvropTableComponent } from './components/avrop-table/avrop-table.component';
|
||||
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [
|
||||
AvropComponent,
|
||||
AvropFiltersComponent,
|
||||
AvropTableComponent,
|
||||
AvropTableRowComponent,
|
||||
TemporaryFilterComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild([{ path: '', component: AvropComponent }]),
|
||||
LayoutModule,
|
||||
DigiNgProgressProgressbarModule,
|
||||
DigiNgSkeletonBaseModule,
|
||||
],
|
||||
})
|
||||
export class AvropModule {}
|
||||
194
apps/mina-sidor-fa/src/app/pages/avrop/avrop.service.ts
Normal file
194
apps/mina-sidor-fa/src/app/pages/avrop/avrop.service.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Avrop } from '@msfa-models/avrop.model';
|
||||
import { MultiselectFilterOption } from '@msfa-models/multiselect-filter-option';
|
||||
import { AvropApiService } from '@msfa-services/api/avrop-api.service';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { HandledareAvrop } from './models/handledare-avrop';
|
||||
|
||||
type Step = 1 | 2 | 3 | 4;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AvropService {
|
||||
private _selectedTjanster$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
|
||||
private _selectedUtforandeVerksamheter$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
|
||||
private _selectedKommuner$ = new BehaviorSubject<MultiselectFilterOption[]>(null);
|
||||
private _selectedDeltagareList$ = new BehaviorSubject<Avrop[]>([]);
|
||||
private _deltagareListIsLocked$ = new BehaviorSubject<boolean>(null);
|
||||
private _selectedHandledare$ = new BehaviorSubject<HandledareAvrop>(null);
|
||||
private _handledareIsConfirmed$ = new BehaviorSubject<boolean>(false);
|
||||
private _avropIsSaved$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
selectedTjanster$: Observable<MultiselectFilterOption[]> = this._selectedTjanster$.asObservable();
|
||||
selectedUtforandeVerksamheter$: Observable<
|
||||
MultiselectFilterOption[]
|
||||
> = this._selectedUtforandeVerksamheter$.asObservable();
|
||||
selectedKommuner$: Observable<MultiselectFilterOption[]> = this._selectedKommuner$.asObservable();
|
||||
|
||||
selectableDeltagareList$: Observable<Avrop[]> = combineLatest([
|
||||
this.selectedTjanster$,
|
||||
this.selectedUtforandeVerksamheter$,
|
||||
this.selectedKommuner$,
|
||||
]).pipe(
|
||||
switchMap(([selectedTjanster, selectedUtforandeVerksamheter, selectedKommuner]) =>
|
||||
this.avropApiService.getNyaAvrop$(selectedTjanster, selectedKommuner, selectedUtforandeVerksamheter)
|
||||
)
|
||||
);
|
||||
|
||||
selectableTjanster$: Observable<MultiselectFilterOption[]> = combineLatest([
|
||||
this.selectedUtforandeVerksamheter$,
|
||||
this.selectedKommuner$,
|
||||
]).pipe(
|
||||
switchMap(([selectedUtforandeVerksamheter, selectedKommuner]) =>
|
||||
this.avropApiService.getSelectableTjanster$(selectedKommuner, selectedUtforandeVerksamheter)
|
||||
)
|
||||
);
|
||||
selectableUtforandeVerksamheter$: Observable<MultiselectFilterOption[]> = combineLatest([
|
||||
this.selectedTjanster$,
|
||||
this.selectedKommuner$,
|
||||
]).pipe(
|
||||
switchMap(([selectedTjanster, selectedKommuner]) =>
|
||||
this.avropApiService.getSelectableUtforandeVerksamheter$(selectedTjanster, selectedKommuner)
|
||||
)
|
||||
);
|
||||
selectableKommuner$: Observable<MultiselectFilterOption[]> = combineLatest([
|
||||
this.selectedTjanster$,
|
||||
this.selectedUtforandeVerksamheter$,
|
||||
]).pipe(
|
||||
switchMap(([selectedTjanster, selectedUtforandeVerksamheter]) =>
|
||||
this.avropApiService.getSelectableKommuner$(selectedTjanster, selectedUtforandeVerksamheter)
|
||||
)
|
||||
);
|
||||
|
||||
selectedDeltagareList$: Observable<Avrop[]> = this._selectedDeltagareList$.asObservable();
|
||||
|
||||
deltagareListIsLocked$: Observable<boolean> = this._deltagareListIsLocked$.asObservable();
|
||||
lockedDeltagareList$: Observable<Avrop[]> = combineLatest([
|
||||
this.selectedDeltagareList$,
|
||||
this.deltagareListIsLocked$,
|
||||
]).pipe(map(([selectedDeltagareList, isLocked]) => (isLocked ? selectedDeltagareList : null)));
|
||||
|
||||
selectableHandledareList$: Observable<HandledareAvrop[]> = this.lockedDeltagareList$.pipe(
|
||||
switchMap(lockedDeltagare => this.avropApiService.getSelectableHandledare$(lockedDeltagare))
|
||||
);
|
||||
|
||||
selectedHandledare$: Observable<HandledareAvrop> = this._selectedHandledare$.asObservable();
|
||||
|
||||
handledareIsConfirmed$: Observable<boolean> = this._handledareIsConfirmed$.asObservable();
|
||||
avropIsSaved$: Observable<boolean> = this._handledareIsConfirmed$.asObservable();
|
||||
|
||||
currentStep$: Observable<Step> = combineLatest([
|
||||
this.handledareIsConfirmed$,
|
||||
this._deltagareListIsLocked$,
|
||||
this.avropIsSaved$,
|
||||
]).pipe(
|
||||
map(([confirmedHandledare, lockedDeltagareList, avropIsSaved]) =>
|
||||
AvropService.calculateStep(confirmedHandledare, lockedDeltagareList, avropIsSaved)
|
||||
)
|
||||
);
|
||||
|
||||
private static calculateStep(
|
||||
confirmedHandledare: boolean,
|
||||
deltagareListIsLocked: boolean,
|
||||
avropIsSaved: boolean
|
||||
): Step {
|
||||
if (avropIsSaved && confirmedHandledare && deltagareListIsLocked) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (confirmedHandledare && deltagareListIsLocked) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (deltagareListIsLocked) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
setSelectedDeltagare(deltagare: Avrop[]): void {
|
||||
this._selectedDeltagareList$.next(deltagare);
|
||||
}
|
||||
|
||||
constructor(private avropApiService: AvropApiService) {}
|
||||
|
||||
lockSelectedDeltagare(): void {
|
||||
if ((this._selectedDeltagareList$?.value?.length ?? -1) <= 0) {
|
||||
throw new Error('För att låsa deltagare behöver några ha markerats först.');
|
||||
}
|
||||
this._deltagareListIsLocked$.next(true);
|
||||
}
|
||||
|
||||
unlockSelectedDeltagare(): void {
|
||||
this._deltagareListIsLocked$.next(false);
|
||||
}
|
||||
|
||||
confirmHandledare(): void {
|
||||
if (!this._selectedHandledare$?.value) {
|
||||
throw new Error('För att kunna tilldela behövs en handledare väljas först.');
|
||||
}
|
||||
this._handledareIsConfirmed$.next(true);
|
||||
}
|
||||
|
||||
unconfirmHandledare(): void {
|
||||
this._handledareIsConfirmed$.next(false);
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
if (!this._handledareIsConfirmed$) {
|
||||
throw new Error('Handledaren måste bekräftas innan avropet kan sparas');
|
||||
}
|
||||
|
||||
if (!this._deltagareListIsLocked$) {
|
||||
throw new Error('Deltagarlistan måste låsas innan avropet kan sparas');
|
||||
}
|
||||
|
||||
await this.avropApiService.tilldelaHandledare(this._selectedDeltagareList$.value, this._selectedHandledare$.value);
|
||||
this._avropIsSaved$.next(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setHandledareState(handledareId: string): void {
|
||||
this.selectableHandledareList$.pipe(first()).subscribe(handledareList => {
|
||||
this._selectedHandledare$.next(handledareList.find(handledare => handledare.id === handledareId));
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedTjanster(selectedFilterOptions: MultiselectFilterOption[]): void {
|
||||
this._selectedTjanster$.next(selectedFilterOptions);
|
||||
}
|
||||
|
||||
setSelectedUtforandeVerksamheter(selectedFilterOptions: MultiselectFilterOption[]): void {
|
||||
this._selectedUtforandeVerksamheter$.next(selectedFilterOptions);
|
||||
}
|
||||
|
||||
setSelectedKommuner(selectedFilterOptions: MultiselectFilterOption[]): void {
|
||||
this._selectedKommuner$.next(selectedFilterOptions);
|
||||
}
|
||||
|
||||
goToStep1(): void {
|
||||
this._selectedHandledare$.next(null);
|
||||
this._selectedDeltagareList$.next([]);
|
||||
this._deltagareListIsLocked$.next(false);
|
||||
this._handledareIsConfirmed$.next(false);
|
||||
}
|
||||
|
||||
removeKommun(kommunToRemove: MultiselectFilterOption) {
|
||||
this.setSelectedKommuner(this._selectedKommuner$.value.filter(selectedKommun => selectedKommun !== kommunToRemove));
|
||||
}
|
||||
|
||||
removeUtforandeVerksamhet(utforandeVerksamhetToRemove: MultiselectFilterOption) {
|
||||
this.setSelectedUtforandeVerksamheter(
|
||||
this._selectedUtforandeVerksamheter$.value.filter(
|
||||
selectedUtforandeVerksamhet => selectedUtforandeVerksamhet !== utforandeVerksamhetToRemove
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
removeTjanst(tjanstToRemove: MultiselectFilterOption) {
|
||||
this.setSelectedTjanster(this._selectedTjanster$.value.filter(selectedTjanst => selectedTjanst !== tjanstToRemove));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
19
apps/mina-sidor-fa/src/app/pages/logout/logout.component.ts
Normal file
19
apps/mina-sidor-fa/src/app/pages/logout/logout.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { environment } from '@msfa-environment';
|
||||
import { AuthenticationService } from '@msfa-services/api/authentication.service';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-logout',
|
||||
templateUrl: './logout.component.html',
|
||||
styleUrls: ['./logout.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LogoutComponent implements OnInit {
|
||||
loginUrl = environment.loginUrl;
|
||||
|
||||
constructor(private authenticationService: AuthenticationService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.authenticationService.logout();
|
||||
}
|
||||
}
|
||||
12
apps/mina-sidor-fa/src/app/pages/logout/logout.module.ts
Normal file
12
apps/mina-sidor-fa/src/app/pages/logout/logout.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { DigiNgButtonModule } from '@af/digi-ng/_button/button';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { LogoutComponent } from './logout.component';
|
||||
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [LogoutComponent],
|
||||
imports: [CommonModule, RouterModule.forChild([{ path: '', component: LogoutComponent }]), DigiNgButtonModule],
|
||||
})
|
||||
export class LogoutModule {}
|
||||
@@ -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 {}
|
||||
11
apps/mina-sidor-fa/src/app/pages/messages/messages.module.ts
Normal file
11
apps/mina-sidor-fa/src/app/pages/messages/messages.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
|
||||
import { MessagesComponent } from './messages.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [MessagesComponent],
|
||||
imports: [CommonModule, RouterModule.forChild([{ path: '', component: MessagesComponent }]), LayoutModule],
|
||||
})
|
||||
export class MessagesModule {}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user