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

Squashed commit of the following:

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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