Compare commits
10 Commits
1de7624200
...
33149f2fcb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33149f2fcb | ||
|
|
79f5ef34a5 | ||
|
|
6ebc01fc59 | ||
|
|
3ffd591cf5 | ||
|
|
48d864a628 | ||
|
|
65c29e065d | ||
|
|
fb6239afbe | ||
|
|
8909f7c3d1 | ||
|
|
7c48ec175e | ||
|
|
b943474138 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,3 +1,22 @@
|
||||
### [2.5.1](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/compare/diff?targetBranch=refs%2Ftags%2Fv2.5.0&sourceBranch=refs%2Ftags%2Fv2.5.1) (2022-01-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Avvikelserapporten:** Använd nya dialogen ([TV-845](https://jira.arbetsformedlingen.se/browse/TV-845)) ([8909f7c](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/8909f7c3d1e91833988c69d1f8485cd217654e32))
|
||||
* **Nya deltagare:** Använd nya dialogen ([TV-845](https://jira.arbetsformedlingen.se/browse/TV-845)) ([b943474](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/b943474138442cec88f371e8477126f8dacc7e07))
|
||||
* **automatisk utloggning:** Använder nu den nya dialog-komponenten ([TV-845](https://jira.arbetsformedlingen.se/browse/TV-845)) ([1de7624](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/1de762420054ca6782660dd950c938541f543291)), closes [feature/TV-845-ui-dialog-idle-timout-dialog-2](https://jira.arbetsformedlingen.se/browse/TV-845-ui-dialog-idle-timout-dialog-2) [feature/TV-845-ui-dialog-2](https://jira.arbetsformedlingen.se/browse/TV-845-ui-dialog-2) [feature/TV-845-ui-dialog-2](https://jira.arbetsformedlingen.se/browse/TV-845-ui-dialog-2)
|
||||
* **Deltagarlista:** Nu går det att sortera när man klickar på kolumnnamnet ([TV-989](https://jira.arbetsformedlingen.se/browse/TV-989)) ([fb6239a](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/fb6239afbe2cfe2652f0f9b407f684ddc77e239a))
|
||||
* **Signal om arbete eller studier:** Uppdaterad UX för val av sysselsättning (fyra radioknappar, ingen drop-down ([TV-440](https://jira.arbetsformedlingen.se/browse/TV-440)) ([65c29e0](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/65c29e065d66cda8e6c6fa71af30558d303d9d39))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **informativ-rapport:** Added file-upload inside informativ-rapport to feature toggling ([92cf04e](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/92cf04e802cce05c1ed799cabfabf73b1c370c0c))
|
||||
* **gemensam-planering:** Implemented new API response for GP ([a158b4a](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/a158b4ac21f9dbab84865f461010dab63c50a7fa))
|
||||
* **login:** Removed login with username/password from prod builds ([a6f3792](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/a6f37923abc70627e8b0b98f18f5f3791321da3d))
|
||||
* **texter:** Updated button text and changed aria texts for filter functionality inside utförande verksamheter ([TV-953](https://jira.arbetsformedlingen.se/browse/TV-953)) ([8fb58d6](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/8fb58d698ce26e5031174040978f69ba1af1f57e))
|
||||
|
||||
## [2.5.0](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/compare/diff?targetBranch=refs%2Ftags%2Fv2.4.0&sourceBranch=refs%2Ftags%2Fv2.5.0) (2021-12-14)
|
||||
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [AppComponent],
|
||||
imports: [RouterTestingModule, HttpClientTestingModule],
|
||||
imports: [AppModule, RouterTestingModule, HttpClientTestingModule],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export class AppComponent extends UnsubscribeDirective implements OnInit {
|
||||
private _dialogRef: UiDialogRef;
|
||||
private _userIsIdle$: Observable<boolean> = this.idleService.isIdle$;
|
||||
private _idleDialogConfig: UiDialogConfig = {
|
||||
includeBasicFooter: true,
|
||||
primaryButtonText: 'Fortsätt sessionen',
|
||||
primaryAction: () => this.setUserAsActive(),
|
||||
secondaryButtonText: 'Logga ut',
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-administration',
|
||||
templateUrl: './administration.component.html',
|
||||
styleUrls: ['./administration.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AdministrationComponent {}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AdministrationRoutingModule } from './administration-routing.module';
|
||||
import { AdministrationComponent } from './administration.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdministrationComponent],
|
||||
declarations: [],
|
||||
imports: [CommonModule, AdministrationRoutingModule],
|
||||
})
|
||||
export class AdministrationModule {}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
||||
import { SortOrder } from '@msfa-enums/sort-order.enum';
|
||||
import { EmployeeEditRequest } from '@msfa-models/api/employee-edit.request.model';
|
||||
import { EmployeeInviteResponse } from '@msfa-models/api/employee-invite.response.model';
|
||||
import { EmployeeCompactResponse } from '@msfa-models/api/employee.response.model';
|
||||
import { EmployeeParams } from '@msfa-models/api/params.model';
|
||||
import { Employee, EmployeesData } from '@msfa-models/employee.model';
|
||||
import { Sort } from '@msfa-models/sort.model';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { EmployeeApiService } from '@msfa-services/api/employee.api.service';
|
||||
|
||||
const DEFAULT_PARAMS: EmployeeParams = {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
sort: 'name',
|
||||
order: SortOrder.ASC,
|
||||
search: '',
|
||||
onlyEmployeesWithoutAuthorization: false,
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AdministrationService extends UnsubscribeDirective {
|
||||
private _currentEmployeeId$ = new BehaviorSubject<string>(null);
|
||||
private _params$ = new BehaviorSubject<EmployeeParams>(DEFAULT_PARAMS);
|
||||
sort$: Observable<Sort<keyof EmployeeCompactResponse>> = this._params$.pipe(
|
||||
map(({ sort, order }) => ({ key: sort, order }))
|
||||
);
|
||||
onlyEmployeesWithoutAuthorization$: Observable<boolean> = this._params$.pipe(
|
||||
map(({ onlyEmployeesWithoutAuthorization }) => onlyEmployeesWithoutAuthorization)
|
||||
);
|
||||
private _employee$ = new BehaviorSubject<Employee>(null);
|
||||
employee$: Observable<Employee> = this._employee$.asObservable();
|
||||
private _lastUpdatedEmployeeId$ = new BehaviorSubject<string>(null);
|
||||
lastUpdatedEmployeeId$: Observable<string> = this._lastUpdatedEmployeeId$.asObservable();
|
||||
private _lastDeletedEmployee$ = new BehaviorSubject<Employee>(null);
|
||||
lastDeletedEmployee$: Observable<Employee> = this._lastDeletedEmployee$.asObservable();
|
||||
private _employeeToDelete$ = new BehaviorSubject<Employee>(null);
|
||||
employeeToDelete$: Observable<Employee> = this._employeeToDelete$.asObservable();
|
||||
private _employeesLoading$ = new BehaviorSubject<boolean>(false);
|
||||
employeesLoading$: Observable<boolean> = this._employeesLoading$.asObservable();
|
||||
|
||||
constructor(private employeeApiService: EmployeeApiService) {
|
||||
super();
|
||||
super.unsubscribeOnDestroy(
|
||||
combineLatest([this._currentEmployeeId$, this._lastUpdatedEmployeeId$])
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
([prevEmployeeId], [currEmployeeId, currLastUpdatedEmployeeId]) =>
|
||||
!currLastUpdatedEmployeeId && prevEmployeeId === currEmployeeId
|
||||
),
|
||||
filter(([currentEmployeeId]) => !!currentEmployeeId),
|
||||
switchMap(([currentEmployeeId]) =>
|
||||
this._fetchEmployee$(currentEmployeeId).pipe(filter(employee => !!employee))
|
||||
)
|
||||
)
|
||||
.subscribe(employee => {
|
||||
this._employee$.next(employee);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
employeesData$: Observable<EmployeesData> = combineLatest([this._params$, this._lastDeletedEmployee$]).pipe(
|
||||
switchMap(([params]) => this._fetchEmployees$(params))
|
||||
);
|
||||
|
||||
resetParams(): void {
|
||||
this._params$.next(DEFAULT_PARAMS);
|
||||
}
|
||||
|
||||
setCurrentEmployeeId(currentEmployeeId: string): void {
|
||||
if (this._currentEmployeeId$.getValue() !== currentEmployeeId) {
|
||||
this._employee$.next(null);
|
||||
this._currentEmployeeId$.next(currentEmployeeId);
|
||||
}
|
||||
}
|
||||
|
||||
resetLastUpdatedEmployeeId(): void {
|
||||
this._lastUpdatedEmployeeId$.next(null);
|
||||
}
|
||||
|
||||
private _fetchEmployees$(employeeParams: EmployeeParams): Observable<EmployeesData> {
|
||||
this._employeesLoading$.next(true);
|
||||
|
||||
return this.employeeApiService.fetchEmployees$(employeeParams).pipe(tap(() => this._employeesLoading$.next(false)));
|
||||
}
|
||||
|
||||
private _fetchEmployee$(ciamUserId: string): Observable<Employee> {
|
||||
return this.employeeApiService.fetchEmployee$(ciamUserId);
|
||||
}
|
||||
|
||||
setSearchString(value: string): void {
|
||||
this._params$.next({
|
||||
...this._params$.getValue(),
|
||||
search: value,
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
|
||||
setOnlyEmployeesWithoutAuthorization(value: boolean): void {
|
||||
this._params$.next({
|
||||
...this._params$.getValue(),
|
||||
onlyEmployeesWithoutAuthorization: value,
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
|
||||
setEmployeeToDelete(employee: Employee): void {
|
||||
this._employeeToDelete$.next(employee);
|
||||
}
|
||||
|
||||
deleteEmployee(employee: Employee): Observable<Employee> {
|
||||
return this.employeeApiService.deleteEmployee(employee).pipe(
|
||||
tap(() => {
|
||||
this._lastDeletedEmployee$.next(employee);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setSort(sort: keyof EmployeeCompactResponse): void {
|
||||
const params = this._params$.getValue();
|
||||
const order = params.sort === sort && params.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||
|
||||
this._params$.next({ ...params, sort, order });
|
||||
}
|
||||
|
||||
setPage(page: number): void {
|
||||
this._params$.next({ ...this._params$.getValue(), page });
|
||||
}
|
||||
|
||||
postEmployeeInvitation$(emails: string[]): Observable<EmployeeInviteResponse | null> {
|
||||
return this.employeeApiService.postEmployeeInvitation(emails);
|
||||
}
|
||||
|
||||
updateEmployee$(ciamUserId: string, data: EmployeeEditRequest): Observable<boolean> {
|
||||
return this.employeeApiService.updateEmployee$(ciamUserId, data);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
<ng-container *ngIf="deleteEmployeeData$ | async as deleteEmployeeData">
|
||||
<digi-ng-dialog
|
||||
*ngIf="deleteEmployeeData.toDelete"
|
||||
[afActive]="deleteEmployeeData.toDelete"
|
||||
(afOnPrimaryClick)="deleteEmployeeModelPrimaryClick(deleteEmployeeData)"
|
||||
(afOnSecondaryClick)="closeDeleteEmployeeModal()"
|
||||
(afOnInactive)="closeDeleteEmployeeModal()"
|
||||
afHeading="Ta bort personalkonto"
|
||||
afHeadingLevel="h2"
|
||||
[afPrimaryButtonText]="getPrimaryButtonText(deleteEmployeeData.lastDeleted)"
|
||||
[afSecondaryButtonText]="getSecondaryButtonText(deleteEmployeeData.lastDeleted)"
|
||||
>
|
||||
<ng-container *ngIf="deleteEmployeeData.lastDeleted; else deletionWarning">
|
||||
<digi-notification-alert af-variation="success" af-heading="Allt gick bra" af-heading-level="h3">
|
||||
<p>Personalkonto för {{deleteEmployeeData.lastDeleted.fullName}} är borttaget.</p>
|
||||
</digi-notification-alert>
|
||||
</ng-container>
|
||||
<ng-template #deletionWarning>
|
||||
<p>Är du säker på att du vill ta bort personalkontot för {{deleteEmployeeData.toDelete.fullName}}?</p>
|
||||
</ng-template>
|
||||
<!-- <digi-ng-dialog-->
|
||||
<!-- *ngIf="deleteEmployeeData.toDelete"-->
|
||||
<!-- [afActive]="deleteEmployeeData.toDelete"-->
|
||||
<!-- (afOnPrimaryClick)="deleteEmployeeModelPrimaryClick(deleteEmployeeData)"-->
|
||||
<!-- (afOnSecondaryClick)="closeDeleteEmployeeModal()"-->
|
||||
<!-- (afOnInactive)="closeDeleteEmployeeModal()"-->
|
||||
<!-- afHeading="Ta bort personalkonto"-->
|
||||
<!-- afHeadingLevel="h2"-->
|
||||
<!-- [afPrimaryButtonText]="getPrimaryButtonText(deleteEmployeeData.lastDeleted)"-->
|
||||
<!-- [afSecondaryButtonText]="getSecondaryButtonText(deleteEmployeeData.lastDeleted)"-->
|
||||
<!-- >-->
|
||||
<!-- <ng-container *ngIf="deleteEmployeeData.lastDeleted; else deletionWarning">-->
|
||||
<!-- <digi-notification-alert af-variation="success" af-heading="Allt gick bra" af-heading-level="h3">-->
|
||||
<!-- <p>Personalkonto för {{deleteEmployeeData.lastDeleted.fullName}} är borttaget.</p>-->
|
||||
<!-- </digi-notification-alert>-->
|
||||
<!-- </ng-container>-->
|
||||
<!-- <ng-template #deletionWarning>-->
|
||||
<!-- <p>Är du säker på att du vill ta bort personalkontot för {{deleteEmployeeData.toDelete.fullName}}?</p>-->
|
||||
<!-- </ng-template>-->
|
||||
|
||||
<ng-container *ngIf="errorDuringDeletion$ | async as error">
|
||||
<digi-notification-alert af-variation="danger" af-heading="Någonting gick fel" af-heading-level="h3">
|
||||
<p>
|
||||
Vi kunde inte radera personalkontot för {{deleteEmployeeData.toDelete.fullName}}. Ladda om sidan och försök
|
||||
igen.
|
||||
</p>
|
||||
<p class="msfa__small-text" *ngIf="error.message">{{error.message}}</p>
|
||||
</digi-notification-alert>
|
||||
</ng-container>
|
||||
</digi-ng-dialog>
|
||||
<!-- <ng-container *ngIf="errorDuringDeletion$ | async as error">-->
|
||||
<!-- <digi-notification-alert af-variation="danger" af-heading="Någonting gick fel" af-heading-level="h3">-->
|
||||
<!-- <p>-->
|
||||
<!-- Vi kunde inte radera personalkontot för {{deleteEmployeeData.toDelete.fullName}}. Ladda om sidan och försök-->
|
||||
<!-- igen.-->
|
||||
<!-- </p>-->
|
||||
<!-- <p class="msfa__small-text" *ngIf="error.message">{{error.message}}</p>-->
|
||||
<!-- </digi-notification-alert>-->
|
||||
<!-- </ng-container>-->
|
||||
<!-- </digi-ng-dialog>-->
|
||||
</ng-container>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Employee } from '@msfa-models/employee.model';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { EmployeeService } from '@msfa-services/api/employee.service';
|
||||
import { AdministrationService } from '../../administration.service';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@@ -21,8 +21,8 @@ export class EmployeeDeleteComponent {
|
||||
@Input() returnToEmployeeList = false;
|
||||
|
||||
deleteEmployeeData$: Observable<DeleteEmployeeData> = combineLatest([
|
||||
this.employeeService.employeeToDelete$,
|
||||
this.employeeService.lastDeletedEmployee$,
|
||||
this.administrationService.employeeToDelete$,
|
||||
this.administrationService.lastDeletedEmployee$,
|
||||
]).pipe(
|
||||
map(([employeeToDelete, lastDeletedEmployee]) => {
|
||||
return {
|
||||
@@ -42,7 +42,7 @@ export class EmployeeDeleteComponent {
|
||||
return lastDeleted ? null : 'Avbryt';
|
||||
}
|
||||
|
||||
constructor(private employeeService: EmployeeService, private router: Router) {}
|
||||
constructor(private administrationService: AdministrationService, private router: Router) {}
|
||||
|
||||
deleteEmployeeModelPrimaryClick(deleteEmployeeData: DeleteEmployeeData): void {
|
||||
const { lastDeleted, toDelete } = deleteEmployeeData;
|
||||
@@ -54,7 +54,7 @@ export class EmployeeDeleteComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteEmployeeSubscription = this.employeeService.deleteEmployee(toDelete).subscribe({
|
||||
const deleteEmployeeSubscription = this.administrationService.deleteEmployee(toDelete).subscribe({
|
||||
error: (error: CustomError) => {
|
||||
this._errorDuringDeletion$.next(error);
|
||||
},
|
||||
@@ -65,7 +65,7 @@ export class EmployeeDeleteComponent {
|
||||
}
|
||||
|
||||
closeDeleteEmployeeModal(): void {
|
||||
this.employeeService.setEmployeeToDelete(null);
|
||||
this.administrationService.setEmployeeToDelete(null);
|
||||
this._errorDuringDeletion$.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
@@ -7,7 +6,11 @@ import { EmployeeDeleteComponent } from './employee-delete.component';
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [EmployeeDeleteComponent],
|
||||
imports: [CommonModule, RouterModule, DigiNgDialogModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
// DigiNgDialogModule
|
||||
],
|
||||
exports: [EmployeeDeleteComponent],
|
||||
})
|
||||
export class EmployeeDeleteModule {}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Employee } from '@msfa-models/employee.model';
|
||||
import { Role } from '@msfa-models/role.model';
|
||||
import { EmployeeService } from '@msfa-services/api/employee.service';
|
||||
import { AdministrationService } from '../../administration.service';
|
||||
import { RoleService } from '@msfa-services/role.service';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
@@ -14,22 +14,22 @@ import { BehaviorSubject, Observable } from 'rxjs';
|
||||
})
|
||||
export class EmployeeCardComponent implements OnDestroy {
|
||||
private _employeeId$ = new BehaviorSubject<string>(this.activatedRoute.snapshot.params['employeeId']);
|
||||
employee$: Observable<Employee> = this.employeeService.employee$;
|
||||
lastUpdatedEmployeeId$: Observable<string> = this.employeeService.lastUpdatedEmployeeId$;
|
||||
employee$: Observable<Employee> = this.administrationService.employee$;
|
||||
lastUpdatedEmployeeId$: Observable<string> = this.administrationService.lastUpdatedEmployeeId$;
|
||||
allRoles: Role[] = this.roleService.allRoles;
|
||||
accordionsExpanded = [];
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private employeeService: EmployeeService,
|
||||
private administrationService: AdministrationService,
|
||||
private roleService: RoleService
|
||||
) {
|
||||
this.employeeService.setCurrentEmployeeId(this.employeeId);
|
||||
this.administrationService.setCurrentEmployeeId(this.employeeId);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.employeeService.resetLastUpdatedEmployeeId();
|
||||
this.employeeService.setCurrentEmployeeId(null);
|
||||
this.administrationService.resetLastUpdatedEmployeeId();
|
||||
this.administrationService.setCurrentEmployeeId(null);
|
||||
}
|
||||
|
||||
get employeeId(): string {
|
||||
@@ -41,7 +41,7 @@ export class EmployeeCardComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
closeUpdatedNotificationAlert(): void {
|
||||
this.employeeService.resetLastUpdatedEmployeeId();
|
||||
this.administrationService.resetLastUpdatedEmployeeId();
|
||||
}
|
||||
|
||||
toggleAccordionExpanded(currentId: number): void {
|
||||
@@ -52,7 +52,7 @@ export class EmployeeCardComponent implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
hasAccess(employee: Employee, role: Role): boolean{
|
||||
hasAccess(employee: Employee, role: Role): boolean {
|
||||
return employee.roles.includes(role.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { Role } from '@msfa-models/role.model';
|
||||
import { Tjanst } from '@msfa-models/tjanst.model';
|
||||
import { UtforandeVerksamhet } from '@msfa-models/utforande-verksamhet.model';
|
||||
import { EmployeeService } from '@msfa-services/api/employee.service';
|
||||
import { AdministrationService } from '../../administration.service';
|
||||
import { RoleService } from '@msfa-services/role.service';
|
||||
import { UtforandeVerksamheterService } from '@msfa-services/utforande-verksamheter/utforande-verksamheter.service';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
@@ -29,14 +29,14 @@ export class EmployeeFormComponent implements OnInit {
|
||||
);
|
||||
|
||||
errorWhileUpdating$: Observable<CustomError> = this._errorWhileUpdating$.asObservable();
|
||||
employee$ = this.employeeService.employee$;
|
||||
employee$ = this.administrationService.employee$;
|
||||
tjanster$: Observable<Tjanst[]> = this.employeeFormService.fetchTjanster$();
|
||||
availableRoles: Role[] = this.roleService.allRoles;
|
||||
|
||||
isLoadingUtforandeVerksamheter$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private employeeService: EmployeeService,
|
||||
private administrationService: AdministrationService,
|
||||
private roleService: RoleService,
|
||||
private employeeFormService: EmployeeFormService,
|
||||
private utforandeVerksamheterService: UtforandeVerksamheterService,
|
||||
@@ -49,11 +49,11 @@ export class EmployeeFormComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.employeeService.setCurrentEmployeeId(this.employeeId);
|
||||
this.administrationService.setCurrentEmployeeId(this.employeeId);
|
||||
}
|
||||
|
||||
updateEmployee(employeeFormData: EmployeeEditRequest): void {
|
||||
const updateEmployeeSubscription = this.employeeService
|
||||
const updateEmployeeSubscription = this.administrationService
|
||||
.updateEmployee$(this.employeeId, employeeFormData)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@@ -78,7 +78,7 @@ export class EmployeeFormComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
setEmployeeToDelete(employee: Employee): void {
|
||||
this.employeeService.setEmployeeToDelete(employee);
|
||||
this.administrationService.setEmployeeToDelete(employee);
|
||||
}
|
||||
|
||||
closeError(): void {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
|
||||
import { EmployeeInviteResponse } from '@msfa-models/api/employee-invite.response.model';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { EmployeeService } from '@msfa-services/api/employee.service';
|
||||
import { AdministrationService } from '../../administration.service';
|
||||
import { CommaSeparatedEmailValidator } from '@msfa-utils/validators/email.validator';
|
||||
import { RequiredValidator } from '@msfa-utils/validators/required.validator';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
@@ -21,7 +21,7 @@ export class EmployeeInviteComponent {
|
||||
lastInvites$: Observable<EmployeeInviteResponse> = this._lastInvites$.asObservable();
|
||||
submitIsLoading$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
constructor(private employeeService: EmployeeService) {}
|
||||
constructor(private administrationService: AdministrationService) {}
|
||||
|
||||
get emailsControl(): AbstractControl {
|
||||
return this.formGroup.get('emails');
|
||||
@@ -93,7 +93,7 @@ export class EmployeeInviteComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const post = this.employeeService.postEmployeeInvitation(this.emailsControlValueAsArray).subscribe({
|
||||
const post = this.administrationService.postEmployeeInvitation$(this.emailsControlValueAsArray).subscribe({
|
||||
next: data => {
|
||||
this.submitIsLoading$.next(false);
|
||||
this._lastInvites$.next(data);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UiIconType } from '@ui/icon/icon-type.enum';
|
||||
import { EmployeeCompactResponse } from '@msfa-models/api/employee.response.model';
|
||||
import { Employee, EmployeesData } from '@msfa-models/employee.model';
|
||||
import { Sort } from '@msfa-models/sort.model';
|
||||
import { EmployeeService } from '@msfa-services/api/employee.service';
|
||||
import { AdministrationService } from '../../administration.service';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { UiLinkButtonType } from '@ui/link-button/link-button-type.enum';
|
||||
|
||||
@@ -16,16 +16,17 @@ import { UiLinkButtonType } from '@ui/link-button/link-button-type.enum';
|
||||
export class EmployeesComponent implements OnDestroy {
|
||||
UiLinkButtonType = UiLinkButtonType;
|
||||
private _searchValue$ = new BehaviorSubject<string>('');
|
||||
onlyEmployeesWithoutAuthorization$: Observable<boolean> = this.employeeService.onlyEmployeesWithoutAuthorization$;
|
||||
employeesData$: Observable<EmployeesData> = this.employeeService.employeesData$;
|
||||
employeesLoading$: Observable<boolean> = this.employeeService.employeesLoading$;
|
||||
sort$: Observable<Sort<keyof EmployeeCompactResponse>> = this.employeeService.sort$;
|
||||
onlyEmployeesWithoutAuthorization$: Observable<boolean> = this.administrationService
|
||||
.onlyEmployeesWithoutAuthorization$;
|
||||
employeesData$: Observable<EmployeesData> = this.administrationService.employeesData$;
|
||||
employeesLoading$: Observable<boolean> = this.administrationService.employeesLoading$;
|
||||
sort$: Observable<Sort<keyof EmployeeCompactResponse>> = this.administrationService.sort$;
|
||||
iconType = UiIconType;
|
||||
|
||||
constructor(private employeeService: EmployeeService) {}
|
||||
constructor(private administrationService: AdministrationService) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.employeeService.resetParams();
|
||||
this.administrationService.resetParams();
|
||||
}
|
||||
|
||||
get searchValue(): string {
|
||||
@@ -33,7 +34,7 @@ export class EmployeesComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
setSearchString(): void {
|
||||
this.employeeService.setSearchString(this.searchValue);
|
||||
this.administrationService.setSearchString(this.searchValue);
|
||||
}
|
||||
|
||||
setSearchValue($event: CustomEvent<{ target: { value: string } }>): void {
|
||||
@@ -41,18 +42,18 @@ export class EmployeesComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
handleEmployeesSort(key: keyof EmployeeCompactResponse): void {
|
||||
this.employeeService.setSort(key);
|
||||
this.administrationService.setSort(key);
|
||||
}
|
||||
|
||||
setNewPage(page: number): void {
|
||||
this.employeeService.setPage(page);
|
||||
this.administrationService.setPage(page);
|
||||
}
|
||||
|
||||
setOnlyEmployeesWithoutAuthorization(checked: boolean): void {
|
||||
this.employeeService.setOnlyEmployeesWithoutAuthorization(checked);
|
||||
this.administrationService.setOnlyEmployeesWithoutAuthorization(checked);
|
||||
}
|
||||
|
||||
setEmployeeToDelete(employee: Employee): void {
|
||||
this.employeeService.setEmployeeToDelete(employee);
|
||||
this.administrationService.setEmployeeToDelete(employee);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,17 +85,7 @@
|
||||
Läs mer information här
|
||||
</digi-button>
|
||||
|
||||
<digi-ng-dialog
|
||||
class="avrop-dialog"
|
||||
[afActive]="displayAvropDialog$ | async"
|
||||
(afOnInactive)="closeAvropDialog()"
|
||||
(afOnPrimaryClick)="closeAvropDialog()"
|
||||
afHeading="Information"
|
||||
afHeadingLevel="h2"
|
||||
afPrimaryButtonText="Stäng"
|
||||
afSecondaryButtonText=""
|
||||
>
|
||||
<h3>Genomförandereferens</h3>
|
||||
<ng-template #informationDialog>
|
||||
<p>
|
||||
Genomförandereferens är det referensnummer du ska använda dig av i kontakten med Arbetsförmedlingen.
|
||||
Du kan också använda genomförandereferensen till att leta fram en order i leverantörsportalen.
|
||||
@@ -117,7 +107,8 @@
|
||||
språkstöd som ingår i upphandlingen av olika tjänster och utbildningar. Du hittar mer information om
|
||||
språkstöd och tolk i förfrågningsunderlaget för specifik upphandling.
|
||||
</p>
|
||||
</digi-ng-dialog>
|
||||
<!-- </ui-dialog-layout>-->
|
||||
</ng-template>
|
||||
</div>
|
||||
<msfa-avrop-list
|
||||
[availableAvrop]="avropData.data"
|
||||
|
||||
@@ -3,6 +3,8 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AvropComponent } from './avrop.component';
|
||||
import { AvropModule } from './avrop.module';
|
||||
import { ApmModule } from '@elastic/apm-rum-angular';
|
||||
|
||||
describe('AvropComponent', () => {
|
||||
let component: AvropComponent;
|
||||
@@ -12,7 +14,7 @@ describe('AvropComponent', () => {
|
||||
waitForAsync(() => {
|
||||
void TestBed.configureTestingModule({
|
||||
declarations: [AvropComponent],
|
||||
imports: [RouterTestingModule, HttpClientTestingModule],
|
||||
imports: [AvropModule, RouterTestingModule, HttpClientTestingModule, ApmModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
})
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { Avrop, AvropAndMeta } from '@msfa-models/avrop.model';
|
||||
import { Handledare } from '@msfa-models/handledare.model';
|
||||
import { AvropService } from '@msfa-services/avrop.service';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { UiDialog } from '@ui/dialog/ui-dialog.service';
|
||||
import { UiDialogRef } from '@ui/dialog/ui-dialog-ref';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-avrop',
|
||||
@@ -11,6 +13,8 @@ import { BehaviorSubject, Observable } from 'rxjs';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AvropComponent implements OnDestroy {
|
||||
@ViewChild('informationDialog') informationDialog: TemplateRef<unknown>;
|
||||
uiDialogRef: UiDialogRef;
|
||||
readonly totalAmountOfSteps = 3;
|
||||
currentStep$: Observable<number> = this.avropService.currentStep$;
|
||||
error$: Observable<string> = this.avropService.error$;
|
||||
@@ -23,9 +27,8 @@ export class AvropComponent implements OnDestroy {
|
||||
avropIsSubmitted$: Observable<boolean> = this.avropService.avropIsSubmitted$;
|
||||
avropLoading$: Observable<boolean> = this.avropService.avropLoading$;
|
||||
showUnauthorizedError$: Observable<boolean> = this.avropService.showUnauthorizedError$;
|
||||
displayAvropDialog$ = new BehaviorSubject(false);
|
||||
|
||||
constructor(private avropService: AvropService) {}
|
||||
constructor(private avropService: AvropService, private uiDialog: UiDialog) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.returnToStep1();
|
||||
@@ -76,10 +79,6 @@ export class AvropComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
openAvropDialog(): void {
|
||||
this.displayAvropDialog$.next(true);
|
||||
}
|
||||
|
||||
closeAvropDialog(): void {
|
||||
this.displayAvropDialog$.next(false);
|
||||
this.uiDialogRef = this.uiDialog.open(this.informationDialog, { heading: 'Information', includeBasicFooter: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
|
||||
import { AvropComponent } from './avrop.component';
|
||||
import { AvropFiltersModule } from './components/avrop-filters/avrop-filters.module';
|
||||
import { AvropListModule } from './components/avrop-list/avrop-list.module';
|
||||
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
|
||||
import { UiDialogModule } from '@ui/dialog/ui-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
@@ -24,7 +24,7 @@ import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
|
||||
UiLoaderModule,
|
||||
HandledarePickerFormModule,
|
||||
UnauthorizedAlertModule,
|
||||
DigiNgDialogModule,
|
||||
UiDialogModule,
|
||||
],
|
||||
})
|
||||
export class AvropModule {}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<ui-dialog-layout [isLoading]="submitIsLoading$ | async">
|
||||
<h2 uiDialogHeading>Vill du skicka in Avvikelserapport (avvikelse)</h2>
|
||||
|
||||
<msfa-report-description-list [avrop]="data.avrop">
|
||||
<dt>Orsak till avvikelse:</dt>
|
||||
<dd>{{data.chosenReason.name }}</dd>
|
||||
<ng-container *ngFor="let question of data.avvikelseSubmitData.avvikelseAlternativ.frageformular">
|
||||
<dd>{{question.svar.length === 0 ? 'Inget svar' : question.svar }}</dd>
|
||||
</ng-container>
|
||||
<dt>Dag för avvikelse:</dt>
|
||||
<dd>{{data.avvikelseSubmitData.avvikelseAlternativ.rapporteringsdatum }}</dd>
|
||||
</msfa-report-description-list>
|
||||
|
||||
<digi-notification-alert
|
||||
*ngIf="submitError$ | async as error"
|
||||
class="franvaro-report-form__alert"
|
||||
af-variation="danger"
|
||||
af-heading="Någonting gick fel"
|
||||
>
|
||||
<p>Kunde inte spara Avvikelserapport (frånvaro). Ladda om sidan och försök igen.</p>
|
||||
<p class="msfa__small-text" *ngIf="error.message">{{error.message}}</p>
|
||||
</digi-notification-alert>
|
||||
|
||||
<ng-container uiDialogFooter>
|
||||
<digi-button af-type="button" (click)="submitAndCloseConfirmDialog(data.avvikelseSubmitData)"
|
||||
>Skicka in</digi-button
|
||||
>
|
||||
<digi-button af-type="button" af-variation="secondary" (click)="close()"> Avbryt </digi-button>
|
||||
</ng-container>
|
||||
</ui-dialog-layout>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AvvikelseConfirmDialogComponent } from './avvikelse-confirm-dialog.component';
|
||||
import { AvvikelseReportFormModule } from '../avvikelse-report-form.module';
|
||||
|
||||
xdescribe('AvvikelseConfirmDialogComponent', () => {
|
||||
let component: AvvikelseConfirmDialogComponent;
|
||||
let fixture: ComponentFixture<AvvikelseConfirmDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AvvikelseConfirmDialogComponent],
|
||||
imports: [AvvikelseReportFormModule],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AvvikelseConfirmDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { UiDialogRef } from '@ui/dialog/ui-dialog-ref';
|
||||
import { DeltagareAvrop } from '@msfa-models/avrop.model';
|
||||
import { AvvikelseReason } from '@msfa-models/avvikelse-reason.model';
|
||||
import { AvvikelseReportRequest } from '@msfa-models/api/avvikelse-request.model';
|
||||
import { AvvikelseReportFormService } from '../avvikelse-report-form.service';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export interface AvvikelseConfirmDialogData {
|
||||
avrop: DeltagareAvrop;
|
||||
chosenReason: AvvikelseReason;
|
||||
avvikelseSubmitData: AvvikelseReportRequest;
|
||||
}
|
||||
@Component({
|
||||
selector: 'msfa-avvikelse-confirm-dialog',
|
||||
templateUrl: './avvikelse-confirm-dialog.component.html',
|
||||
styleUrls: ['./avvikelse-confirm-dialog.component.scss'],
|
||||
})
|
||||
export class AvvikelseConfirmDialogComponent {
|
||||
submitIsLoading$ = new BehaviorSubject<boolean>(false);
|
||||
submitError$ = new BehaviorSubject<CustomError>(null);
|
||||
|
||||
constructor(public uiDialogRef: UiDialogRef, private avvikelseReportFormService: AvvikelseReportFormService) {}
|
||||
|
||||
get data(): AvvikelseConfirmDialogData {
|
||||
return this.uiDialogRef.config.data as AvvikelseConfirmDialogData;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.uiDialogRef.close();
|
||||
}
|
||||
|
||||
submitAndCloseConfirmDialog(avvikelseSubmitData: AvvikelseReportRequest): void {
|
||||
this.submitIsLoading$.next(true);
|
||||
this.avvikelseReportFormService.createAvvikelse$(avvikelseSubmitData).subscribe({
|
||||
next: () => {
|
||||
this.submitIsLoading$.next(false);
|
||||
this.uiDialogRef.close({ submitted: new Date() });
|
||||
},
|
||||
error: (customError: CustomError) => {
|
||||
this.submitError$.next({ ...customError, message: customError.error.message });
|
||||
|
||||
this.submitIsLoading$.next(false);
|
||||
throw { ...customError, avoidToast: true };
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,11 @@
|
||||
</div>
|
||||
|
||||
<ng-template #formRef>
|
||||
<form class="avvikelse-report-form__form" [formGroup]="avvikelseFormGroup" (ngSubmit)="openConfirmDialog()">
|
||||
<form
|
||||
class="avvikelse-report-form__form"
|
||||
[formGroup]="avvikelseFormGroup"
|
||||
(ngSubmit)="openConfirmDialog(avrop)"
|
||||
>
|
||||
<div class="avvikelse-report-form__form-item">
|
||||
<ui-select
|
||||
*ngIf="reasonsAsUiSelectOptions$ | async as reason; else loadingRef"
|
||||
@@ -90,40 +94,15 @@
|
||||
</ui-link-button>
|
||||
</div>
|
||||
</form>
|
||||
<digi-ng-dialog
|
||||
[afActive]="confirmDialogIsOpen$ | async"
|
||||
(afOnPrimaryClick)="submitAndCloseConfirmDialog()"
|
||||
(afOnInactive)="cancelConfirmDialog()"
|
||||
afHeadingLevel="h2"
|
||||
afPrimaryButtonText="Skicka in"
|
||||
afSecondaryButtonText="Avbryt"
|
||||
(afOnSecondaryClick)="cancelConfirmDialog()"
|
||||
afHeading="Vill du skicka in Avvikelserapport (avvikelse)"
|
||||
afAriaLabel="Förhandsgranska och skicka in Avvikelserapport (avvikelse)"
|
||||
id="confirmAvvikelserapport"
|
||||
|
||||
<digi-notification-alert
|
||||
*ngIf="submitError$ | async as error"
|
||||
af-variation="danger"
|
||||
af-heading="Någonting gick fel"
|
||||
>
|
||||
<ui-loader *ngIf="submitIsLoading$ | async" uiType="absolute"></ui-loader>
|
||||
<msfa-report-description-list [avrop]="avrop">
|
||||
<dt>Orsak till avvikelse:</dt>
|
||||
<dd>{{(chosenReason$ | async)?.name }}</dd>
|
||||
<ng-container *ngIf="avvikelseSubmitData$ | async as avvikelseSubmitData; else loadingRef">
|
||||
<ng-container *ngFor="let question of avvikelseSubmitData.avvikelseAlternativ.frageformular">
|
||||
<dt>{{getCurrentQuestionFromId(question.fraga).name}}</dt>
|
||||
<dd>{{question.svar.length === 0 ? 'Inget svar' : question.svar }}</dd>
|
||||
</ng-container>
|
||||
<dt>Dag för avvikelse:</dt>
|
||||
<dd>{{avvikelseSubmitData.avvikelseAlternativ.rapporteringsdatum }}</dd>
|
||||
</ng-container>
|
||||
</msfa-report-description-list>
|
||||
<digi-notification-alert
|
||||
*ngIf="submitError$ | async as error"
|
||||
af-variation="danger"
|
||||
af-heading="Någonting gick fel"
|
||||
>
|
||||
<p>Kunde inte spara Avvikelserapport (avvikelse). Ladda om sidan och försök igen.</p>
|
||||
<p *ngIf="error.message" class="msfa__small-text">{{error.message}}</p>
|
||||
</digi-notification-alert>
|
||||
</digi-ng-dialog>
|
||||
<p>Kunde inte spara Avvikelserapport (avvikelse). Försök igen om en stund.</p>
|
||||
<p *ngIf="error.message" class="msfa__small-text">{{error.message}}</p>
|
||||
</digi-notification-alert>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
|
||||
import { AvvikelseReportFormComponent } from './avvikelse-report-form.component';
|
||||
import { AvvikelseReportFormService } from './avvikelse-report-form.service';
|
||||
import { ApmModule } from '@elastic/apm-rum-angular';
|
||||
import { AvvikelseReportFormModule } from './avvikelse-report-form.module';
|
||||
|
||||
describe('AvvikelseReportFormComponent', () => {
|
||||
let component: AvvikelseReportFormComponent;
|
||||
@@ -17,13 +16,7 @@ describe('AvvikelseReportFormComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [AvvikelseReportFormComponent, LayoutComponent],
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
HttpClientTestingModule,
|
||||
ReactiveFormsModule,
|
||||
DigiNgFormDatepickerModule,
|
||||
ApmModule,
|
||||
],
|
||||
imports: [AvvikelseReportFormModule, RouterTestingModule, HttpClientTestingModule, ApmModule],
|
||||
providers: [AvvikelseReportFormService],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -12,8 +12,14 @@ import { RegexValidator } from '@msfa-utils/validators/regex.validator';
|
||||
import { RequiredValidator } from '@msfa-validators/required.validator';
|
||||
import { addDays } from 'date-fns';
|
||||
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
import { first, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||
import { AvvikelseReportFormService } from './avvikelse-report-form.service';
|
||||
import { UiDialog } from '@ui/dialog/ui-dialog.service';
|
||||
import { UiDialogRef } from '@ui/dialog/ui-dialog-ref';
|
||||
import {
|
||||
AvvikelseConfirmDialogComponent,
|
||||
AvvikelseConfirmDialogData,
|
||||
} from './avvikelse-confirm-dialog/avvikelse-confirm-dialog.component';
|
||||
|
||||
interface Params {
|
||||
genomforandeReferens: string;
|
||||
@@ -34,20 +40,22 @@ type AvvikelseFormKeys = keyof AvvikelseFormData;
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
confirmDialogRef: UiDialogRef;
|
||||
shouldValidate$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
reasonFormName: AvvikelseFormKeys = 'reason';
|
||||
questionsFormName: AvvikelseFormKeys = 'questions';
|
||||
reportingDateFormName: AvvikelseFormKeys = 'reportingDate';
|
||||
|
||||
submitIsLoading$ = new BehaviorSubject<boolean>(false);
|
||||
submitError$ = new BehaviorSubject<CustomError>(null);
|
||||
|
||||
genomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
|
||||
map((params: Params) => +params.genomforandeReferens)
|
||||
);
|
||||
avrop: DeltagareAvrop;
|
||||
avrop$: Observable<DeltagareAvrop> = this.genomforandeReferens$.pipe(
|
||||
switchMap(genomforandeReferens => this.avvikelseReportFormService.fetchAvropInformation$(genomforandeReferens)),
|
||||
tap(avrop => (this.avrop = avrop)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
@@ -60,8 +68,6 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
chosenReasonId$: Observable<string>;
|
||||
chosenReason$: Observable<AvvikelseReason>;
|
||||
questionsForChosenReason$: Observable<AvvikelseQuestion[]>;
|
||||
avvikelseSubmitData$: Observable<AvvikelseReportRequest>;
|
||||
confirmDialogIsOpen$ = new BehaviorSubject<boolean>(false);
|
||||
submittedDate$ = new BehaviorSubject<Date | null>(null);
|
||||
private subscriptions: Subscription[] = [];
|
||||
private todayDateISO = new Date().toISOString().slice(0, 10);
|
||||
@@ -70,11 +76,19 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
[this.reportingDateFormName]: new FormControl(this.todayDateISO, [RequiredValidator('Datum är obligatoriskt')]),
|
||||
[this.questionsFormName]: new FormArray([]),
|
||||
});
|
||||
private formData$: Observable<AvvikelseFormData> = this.avvikelseFormGroup
|
||||
.valueChanges as Observable<AvvikelseFormData>;
|
||||
private currentQuestions: AvvikelseQuestion[];
|
||||
|
||||
constructor(private avvikelseReportFormService: AvvikelseReportFormService, private activatedRoute: ActivatedRoute) {}
|
||||
constructor(
|
||||
private avvikelseReportFormService: AvvikelseReportFormService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private uiDialog: UiDialog
|
||||
) {
|
||||
this.chosenReasonId$ = this.reasonFormControl.valueChanges as Observable<string>;
|
||||
this.chosenReason$ = combineLatest([this.chosenReasonId$, this.reasons$]).pipe(
|
||||
map(([chosenReasonId, reasons]) => reasons.find(reason => reason.id.toString() === chosenReasonId)),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
get reasonFormControl(): AbstractControl | undefined {
|
||||
return this.avvikelseFormGroup.get(this.reasonFormName);
|
||||
@@ -93,11 +107,6 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.chosenReasonId$ = this.reasonFormControl.valueChanges as Observable<string>;
|
||||
this.chosenReason$ = combineLatest([this.chosenReasonId$, this.reasons$]).pipe(
|
||||
map(([chosenReasonId, reasons]) => reasons.find(reason => reason.id.toString() === chosenReasonId))
|
||||
);
|
||||
|
||||
this.questionsForChosenReason$ = combineLatest([this.chosenReasonId$, this.allAvvikelseQuestions$]).pipe(
|
||||
map(([chosenOrsak, allAvvikelseQuestions]) => {
|
||||
return allAvvikelseQuestions.filter(question => question.id.startsWith(chosenOrsak.toString() + '_'));
|
||||
@@ -105,21 +114,15 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this.subscriptions.push(
|
||||
this.chosenReason$.subscribe(() => {
|
||||
this.chosenReason$.subscribe(chosenReason => {
|
||||
this.shouldValidate$.next(false);
|
||||
}),
|
||||
|
||||
this.questionsForChosenReason$.subscribe(questions => {
|
||||
this.clearQuestions();
|
||||
questions.forEach(question => this.addQuestionToForm(question));
|
||||
})
|
||||
);
|
||||
|
||||
this.avvikelseSubmitData$ = combineLatest([this.genomforandeReferens$, this.chosenReasonId$, this.formData$]).pipe(
|
||||
map(([genomforandeReferens, chosenReason, formData]) =>
|
||||
this.makeAvvikelseSubmitData(genomforandeReferens, chosenReason, formData)
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
questionIsRequired(question: AvvikelseQuestion): boolean {
|
||||
@@ -138,6 +141,7 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
private _isAfterStartDate(startDate: Date): boolean {
|
||||
return new Date() > startDate;
|
||||
}
|
||||
|
||||
private _isBeforeLastPossibleReportDay(endDate: Date): boolean {
|
||||
// Reporting is allowed at latest 5 days past avrop end date.
|
||||
// Because it's workdays and not calendar days we temporarily set this to much more. This date should be fetched from API in the future
|
||||
@@ -158,44 +162,41 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
openConfirmDialog(): void {
|
||||
openConfirmDialog(avrop: DeltagareAvrop): void {
|
||||
this.shouldValidate$.next(true);
|
||||
markControlsAsDirty(Object.values(this.avvikelseFormGroup.controls));
|
||||
this.avvikelseFormGroup.markAllAsTouched();
|
||||
|
||||
if (this.avvikelseFormGroup.valid) {
|
||||
this.confirmDialogIsOpen$.next(true);
|
||||
combineLatest([this.chosenReason$, this.genomforandeReferens$])
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap(([chosenReason, genomforandeReferens]) => {
|
||||
const avvikelseSubmitData: AvvikelseReportRequest = this._makeAvvikelseSubmitData(
|
||||
genomforandeReferens,
|
||||
chosenReason.id.toString(),
|
||||
this.avvikelseFormGroup.value as AvvikelseFormData
|
||||
);
|
||||
|
||||
const data: AvvikelseConfirmDialogData = { chosenReason, avvikelseSubmitData, avrop };
|
||||
|
||||
return this.uiDialog.open<{ submitted: Date }>(AvvikelseConfirmDialogComponent, { data }).afterClosed$;
|
||||
})
|
||||
)
|
||||
.subscribe(closedResult => {
|
||||
this.submitError$.next(null);
|
||||
if (closedResult.data?.submitted) {
|
||||
this.submittedDate$.next(closedResult.data.submitted);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
submitAndCloseConfirmDialog(): void {
|
||||
this.submitIsLoading$.next(true);
|
||||
this.avvikelseSubmitData$.pipe(take(1)).subscribe(avvikelseSubmitData =>
|
||||
this.avvikelseReportFormService.createAvvikelse$(avvikelseSubmitData).subscribe({
|
||||
next: () => {
|
||||
this.submitIsLoading$.next(false);
|
||||
this.submittedDate$.next(new Date());
|
||||
this.confirmDialogIsOpen$.next(false);
|
||||
},
|
||||
error: (customError: CustomError) => {
|
||||
this.submitError$.next({ ...customError, message: customError.error.message });
|
||||
this.submitIsLoading$.next(false);
|
||||
throw { ...customError, avoidToast: true };
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
cancelConfirmDialog(): void {
|
||||
this.confirmDialogIsOpen$.next(false);
|
||||
this.submitError$.next(null);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
private makeAvvikelseSubmitData(
|
||||
private _makeAvvikelseSubmitData(
|
||||
genomforandeReferens: number,
|
||||
chosenReason: string,
|
||||
formData: AvvikelseFormData
|
||||
@@ -219,6 +220,8 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
private addQuestionToForm(question: AvvikelseQuestion): void {
|
||||
// FormArray doesnt hold any IDs so we need to store these seperately and rebuild structure at submit
|
||||
// TODO we can actually just put id in the formgroup, as we do in slutredovisning-form-step1.component.ts. That would simplify this file.
|
||||
|
||||
this.currentQuestions.push(question);
|
||||
|
||||
this.questionsFormArray.push(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
|
||||
import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
@@ -17,10 +16,12 @@ import { ReportDescriptionListModule } from '../../../components/report-descript
|
||||
import { ReportLayoutModule } from '../../../components/report-layout/report-layout.module';
|
||||
import { AvvikelseReportFormComponent } from './avvikelse-report-form.component';
|
||||
import { AvvikelseReportFormService } from './avvikelse-report-form.service';
|
||||
import { AvvikelseConfirmDialogComponent } from './avvikelse-confirm-dialog/avvikelse-confirm-dialog.component';
|
||||
import { UiDialogModule } from '@ui/dialog/ui-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [AvvikelseReportFormComponent],
|
||||
declarations: [AvvikelseReportFormComponent, AvvikelseConfirmDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild([{ path: '', component: AvvikelseReportFormComponent }]),
|
||||
@@ -34,10 +35,10 @@ import { AvvikelseReportFormService } from './avvikelse-report-form.service';
|
||||
UiLoaderModule,
|
||||
UiSelectModule,
|
||||
ReportDescriptionListModule,
|
||||
DigiNgDialogModule,
|
||||
UiTextareaModule,
|
||||
UiLinkButtonModule,
|
||||
PreventDoubleSubmitModule,
|
||||
UiDialogModule,
|
||||
],
|
||||
providers: [AvvikelseReportFormService],
|
||||
exports: [AvvikelseReportFormComponent],
|
||||
|
||||
@@ -154,9 +154,9 @@ export class GemensamPlaneringFormComponent {
|
||||
return new Date() > startDate;
|
||||
}
|
||||
private _isBeforeLastPossibleReportDay(endDate: Date): boolean {
|
||||
// Reporting is allowed at latest 5 days past avrop end date.
|
||||
// Reporting is allowed at latest 60 working days past avrop end date. To calculate calendar days we remove weekends like this 60*7/5=84
|
||||
// Because it's workdays and not calendar days we temporarily set this to much more. This date should be fetched from API in the future
|
||||
const lastPossibleReportDay = addDays(endDate, 15);
|
||||
const lastPossibleReportDay = addDays(endDate, 84);
|
||||
return lastPossibleReportDay > new Date();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,35 +21,27 @@
|
||||
</div>
|
||||
|
||||
<ng-template #formRef>
|
||||
<h2>Har den sökande fått arbete eller påbörjat studier?</h2>
|
||||
<p>Ange sysselsättning (obligatoriskt)</p>
|
||||
<form class="signal-form__form" [formGroup]="signalFormGroup" id="signal-form" (ngSubmit)="openConfirmDialog()">
|
||||
<ui-select
|
||||
[formControl]="typeFormControl"
|
||||
uiLabel="Typ av sysselsättning"
|
||||
uiPlaceholder="Välj typ av sysselsättning"
|
||||
[uiOptions]="typeSelectItems"
|
||||
[uiRequired]="true"
|
||||
[uiAnnounceIfOptional]="true"
|
||||
[uiInvalid]="formControlIsInvalid(typeFormName)"
|
||||
[uiValidationMessage]="formErrors.type"
|
||||
></ui-select>
|
||||
<digi-form-fieldset af-legend="Omfattning" af-name="omfattning" af-form="signal-form">
|
||||
<digi-form-fieldset af-name="type" af-form="signal-form">
|
||||
<ui-radiobutton-group
|
||||
[formControl]="omfattningFormControl"
|
||||
[formControl]="typeFormControl"
|
||||
[uiRequired]="true"
|
||||
[uiAnnounceIfOptional]="true"
|
||||
[uiRadiobuttons]="omfattningRadioButtons"
|
||||
[uiInvalid]="formControlIsInvalid(omfattningFormName)"
|
||||
[uiValidationMessage]="formErrors.omfattning"
|
||||
[uiRadiobuttons]="typeRadioButtons"
|
||||
[uiInvalid]="formControlIsInvalid(typeFormName)"
|
||||
[uiValidationMessage]="formErrors.type"
|
||||
></ui-radiobutton-group>
|
||||
</digi-form-fieldset>
|
||||
<ui-input
|
||||
*ngIf="showPercentFormControl"
|
||||
*ngIf="isDeltid"
|
||||
class="signal-form__number-input"
|
||||
[formControl]="percentFormControl"
|
||||
uiType="number"
|
||||
[uiMin]="1"
|
||||
[uiMax]="99"
|
||||
uiLabel="Antal procent vid deltid"
|
||||
uiLabel="Ange omfattning i procent"
|
||||
[uiRequired]="true"
|
||||
[uiAnnounceIfOptional]="true"
|
||||
[uiInvalid]="formControlIsInvalid(percentFormName)"
|
||||
@@ -59,7 +51,7 @@
|
||||
<div class="signal-form__form-item">
|
||||
<digi-ng-form-datepicker
|
||||
[formControl]="startDateFormControl"
|
||||
[afLabel]="'Startdatum för ' + (typeFormControl.value || 'arbete/utbildning')"
|
||||
[afLabel]="datePickerLabel"
|
||||
[afDisableValidStyle]="true"
|
||||
[afRequired]="true"
|
||||
[afInvalid]="formControlIsInvalid(startDateFormName)"
|
||||
@@ -97,12 +89,10 @@
|
||||
>
|
||||
<ui-loader *ngIf="submitLoading$ | async" uiType="absolute"></ui-loader>
|
||||
<msfa-report-description-list [avrop]="avrop">
|
||||
<dt>Typ av sysselsättning</dt>
|
||||
<dt>Sysselsättning</dt>
|
||||
<dd>{{convertTypeValueToLabel(typeFormControl.value)}}</dd>
|
||||
<dt>Omfattning</dt>
|
||||
<dd>{{convertOmfattningValueToLabel(omfattningFormControl.value)}}</dd>
|
||||
<ng-container *ngIf="showPercentFormControl">
|
||||
<dt>Antal procent vid deltid</dt>
|
||||
<ng-container *ngIf="isDeltid">
|
||||
<dt>Omfattning i procent</dt>
|
||||
<dd>{{percentFormControl.value}}%</dd>
|
||||
</ng-container>
|
||||
<dt>Startdatum</dt>
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
.signal-form {
|
||||
max-width: var(--digi--typography--text--max-width);
|
||||
|
||||
h2 {
|
||||
margin-bottom: var(--digi--layout--gutter--s);
|
||||
}
|
||||
|
||||
&__confirmation,
|
||||
&__warning,
|
||||
&__form {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SelectOption } from '@ui/select/select-option.model';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
@@ -10,7 +9,7 @@ import { Radiobutton } from '@ui/radiobutton-group/radiobutton.model';
|
||||
import { add } from 'date-fns';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
import { SignalFormData, SignalFormKeys, SignalOmfattning, SignalType } from './signal-form.model';
|
||||
import { SignalFormData, SignalFormKeys, SignalType } from './signal-form.model';
|
||||
import { SignalFormService } from './signal-form.service';
|
||||
import { SignalFormValidator } from './signal-form.validator';
|
||||
|
||||
@@ -22,7 +21,6 @@ import { SignalFormValidator } from './signal-form.validator';
|
||||
})
|
||||
export class SignalFormComponent {
|
||||
readonly typeFormName: SignalFormKeys = 'type';
|
||||
readonly omfattningFormName: SignalFormKeys = 'omfattning';
|
||||
readonly percentFormName: SignalFormKeys = 'percent';
|
||||
readonly startDateFormName: SignalFormKeys = 'startDate';
|
||||
|
||||
@@ -31,7 +29,6 @@ export class SignalFormComponent {
|
||||
signalFormGroup = new FormGroup(
|
||||
{
|
||||
type: new FormControl(null),
|
||||
omfattning: new FormControl(SignalOmfattning.Heltid),
|
||||
percent: new FormControl(50),
|
||||
startDate: new FormControl(new Date()),
|
||||
},
|
||||
@@ -49,23 +46,17 @@ export class SignalFormComponent {
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
typeSelectItems: SelectOption[] = [
|
||||
{ name: 'Arbete', value: SignalType.Arbete },
|
||||
{ name: 'Utbildning', value: SignalType.Utbildning },
|
||||
typeRadioButtons: Radiobutton[] = [
|
||||
{ label: 'Arbete heltid', value: SignalType.Arbete_Heltid },
|
||||
{ label: 'Arbete deltid', value: SignalType.Arbete_Deltid },
|
||||
{ label: 'Utbildning heltid', value: SignalType.Utbildning_Heltid },
|
||||
{ label: 'Utbildning deltid', value: SignalType.Utbildning_Deltid },
|
||||
];
|
||||
|
||||
omfattningRadioButtons: Radiobutton[] = [
|
||||
{ label: 'Heltid', value: SignalOmfattning.Heltid },
|
||||
{ label: 'Deltid', value: SignalOmfattning.Deltid },
|
||||
];
|
||||
|
||||
constructor(private signalFormService: SignalFormService, private activatedRoute: ActivatedRoute) {}
|
||||
constructor(private signalFormService: SignalFormService, private activatedRoute: ActivatedRoute) { }
|
||||
|
||||
convertTypeValueToLabel(type: SignalType): string {
|
||||
return this.typeSelectItems?.find(selectItem => selectItem.value === type)?.name;
|
||||
}
|
||||
convertOmfattningValueToLabel(type: SignalOmfattning): string {
|
||||
return this.omfattningRadioButtons?.find(radiobuttons => radiobuttons.value === type)?.label;
|
||||
return this.typeRadioButtons?.find(radiobuttons => radiobuttons.value === type)?.label;
|
||||
}
|
||||
|
||||
get formErrors(): { [key: string]: string } {
|
||||
@@ -75,9 +66,7 @@ export class SignalFormComponent {
|
||||
get typeFormControl(): FormControl {
|
||||
return this.signalFormGroup.get(this.typeFormName) as FormControl;
|
||||
}
|
||||
get omfattningFormControl(): FormControl {
|
||||
return this.signalFormGroup.get(this.omfattningFormName) as FormControl;
|
||||
}
|
||||
|
||||
get percentFormControl(): FormControl {
|
||||
return this.signalFormGroup.get(this.percentFormName) as FormControl;
|
||||
}
|
||||
@@ -85,14 +74,27 @@ export class SignalFormComponent {
|
||||
return this.signalFormGroup.get(this.startDateFormName) as FormControl;
|
||||
}
|
||||
|
||||
get showPercentFormControl(): boolean {
|
||||
return this.omfattningFormControl.value === SignalOmfattning.Deltid;
|
||||
get isDeltid(): boolean {
|
||||
return this.typeFormControl.value === SignalType.Arbete_Deltid || this.typeFormControl.value === SignalType.Utbildning_Deltid;
|
||||
}
|
||||
|
||||
get startDateFormValueAsDate(): Date {
|
||||
return new Date(this.startDateFormControl.value);
|
||||
}
|
||||
|
||||
get datePickerLabel(): string {
|
||||
switch (this.typeFormControl.value) {
|
||||
case SignalType.Arbete_Heltid:
|
||||
case SignalType.Arbete_Deltid:
|
||||
return 'Startdatum för arbete';
|
||||
case SignalType.Utbildning_Heltid:
|
||||
case SignalType.Utbildning_Deltid:
|
||||
return 'Startdatum för utbildning';
|
||||
default:
|
||||
return 'Startdatum för arbete/utbildning';
|
||||
}
|
||||
}
|
||||
|
||||
formControlIsInvalid(formControlName: string): boolean {
|
||||
return this.formErrors[formControlName] && this.shouldValidate$.getValue();
|
||||
}
|
||||
@@ -117,36 +119,32 @@ export class SignalFormComponent {
|
||||
|
||||
private typeToRequest(type: string): SignalRequestType {
|
||||
switch (type) {
|
||||
case SignalType.Arbete:
|
||||
return SignalRequestType.Work;
|
||||
case SignalType.Utbildning:
|
||||
return SignalRequestType.Education;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
private omfattningToRequest(omfattning: string): SignalRequestOmfattning {
|
||||
switch (omfattning) {
|
||||
case 'heltid':
|
||||
return SignalRequestOmfattning.Heltid;
|
||||
case 'deltid':
|
||||
return SignalRequestOmfattning.Deltid;
|
||||
case SignalType.Arbete_Heltid:
|
||||
return SignalRequestType.Arbete;
|
||||
case SignalType.Arbete_Deltid:
|
||||
return SignalRequestType.Arbete;
|
||||
case SignalType.Utbildning_Heltid:
|
||||
return SignalRequestType.Utbildning;
|
||||
case SignalType.Utbildning_Deltid:
|
||||
return SignalRequestType.Utbildning;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _formDataToRequest(genomforandereferens: number, formData: SignalFormData): SignalRequest {
|
||||
const { type, omfattning, startDate, percent } = formData;
|
||||
const { type, startDate, percent } = formData;
|
||||
|
||||
const requestType: SignalRequestType = this.typeToRequest(type);
|
||||
const requestOmfattning: SignalRequestOmfattning = this.omfattningToRequest(omfattning);
|
||||
const requestOmfattning: SignalRequestOmfattning = (this.typeFormControl.value as string).includes(SignalRequestOmfattning.Heltid)
|
||||
? SignalRequestOmfattning.Heltid
|
||||
: SignalRequestOmfattning.Deltid;
|
||||
|
||||
return {
|
||||
genomforandereferens,
|
||||
type: requestType,
|
||||
omfattning: requestOmfattning,
|
||||
omfattningPercent: requestOmfattning === SignalRequestOmfattning.Deltid ? percent : null,
|
||||
omfattningPercent: requestOmfattning === SignalRequestOmfattning.Deltid ? percent : 100,
|
||||
startDate: formatDate(startDate),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ export enum SignalOmfattning {
|
||||
Deltid = 'deltid',
|
||||
}
|
||||
export enum SignalType {
|
||||
Arbete = 'arbete',
|
||||
Utbildning = 'utbildning',
|
||||
Arbete_Heltid = 'arbete_heltid',
|
||||
Arbete_Deltid = 'arbete_deltid',
|
||||
Utbildning_Heltid = 'utbildning_heltid',
|
||||
Utbildning_Deltid = 'utbildning_deltid',
|
||||
}
|
||||
|
||||
export interface SignalFormData {
|
||||
|
||||
@@ -64,9 +64,9 @@ export class SlutredovisningFormComponent {
|
||||
}
|
||||
|
||||
private _isBeforeLastPossibleReportDay(date: Date): boolean {
|
||||
// Reporting is allowed at latest 60 days past avrop end date.
|
||||
// Reporting is allowed at latest 60 working days past avrop end date. To calculate calendar days we remove weekends like this 60*7/5=84
|
||||
// Because it's workdays and not calendar days we temporarily set this to much more. This date should be fetched from API in the future
|
||||
const lastPossibleReportDay = addDays(date, 100);
|
||||
const lastPossibleReportDay = addDays(date, 84);
|
||||
return lastPossibleReportDay > new Date();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ export class SignalViewComponent {
|
||||
);
|
||||
typeToText(type: SignalResponseType): string {
|
||||
switch (type) {
|
||||
case SignalResponseType.Work:
|
||||
case SignalResponseType.Arbete:
|
||||
return 'Arbete';
|
||||
case SignalResponseType.Education:
|
||||
case SignalResponseType.Utbildning:
|
||||
return 'Utbildning';
|
||||
default:
|
||||
return 'Okänd';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<h2>Händelser för {{deltagare.fullName}}</h2>
|
||||
|
||||
<div class="deltagare-list-handelser" *ngIf="deltagare">
|
||||
<h3 *ngIf="activeHandelseMotivation" class="deltagare-list-handelser__sub-heading">{{deltagare.fullName}}</h3>
|
||||
<p>Genomförandereferens: <strong>{{deltagare.genomforandeReferens}}</strong></p>
|
||||
|
||||
@@ -3,15 +3,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { DeltagareListHandelserDialogComponent } from './deltagare-list-handelser-dialog.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { DeltagareListModule } from '../../deltagare-list.module';
|
||||
|
||||
describe('DeltagareListHandelserDialogComponent', () => {
|
||||
xdescribe('DeltagareListHandelserDialogComponent', () => {
|
||||
let component: DeltagareListHandelserDialogComponent;
|
||||
let fixture: ComponentFixture<DeltagareListHandelserDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
imports: [HttpClientTestingModule],
|
||||
imports: [DeltagareListModule, HttpClientTestingModule],
|
||||
declarations: [DeltagareListHandelserDialogComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="deltagare-list__column-head" *ngFor="let column of columnHeaders">
|
||||
{{column.label}}
|
||||
<button
|
||||
*ngIf="column.label !== 'Status'"
|
||||
*ngIf="column.key"
|
||||
class="deltagare-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="deltagare-list__sort-icon"
|
||||
@@ -29,6 +29,7 @@
|
||||
></digi-icon-caret-down>
|
||||
</ng-container>
|
||||
</button>
|
||||
<span *ngIf="!column.key"> {{column.label}} </span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { DeltagareListTableComponent } from './deltagare-list-table.component';
|
||||
import { DeltagareListTableModule } from './deltagare-list-table.module';
|
||||
|
||||
describe('DeltagareListComponent', () => {
|
||||
let component: DeltagareListTableComponent;
|
||||
@@ -11,7 +12,7 @@ describe('DeltagareListComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [DeltagareListTableComponent],
|
||||
imports: [RouterTestingModule],
|
||||
imports: [DeltagareListTableModule, RouterTestingModule],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DeltagareListTableComponent);
|
||||
|
||||
@@ -55,7 +55,7 @@ export class DeltagareListTableComponent {
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
key: 'genomforandeReferens',
|
||||
key: null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -85,7 +85,9 @@ export class DeltagareListTableComponent {
|
||||
openHandelser(singleDeltagare: DeltagareCompact): void {
|
||||
this.uiDialog.open(this.handelserDialogComponent, {
|
||||
data: singleDeltagare,
|
||||
heading: 'Händelser för ' + singleDeltagare.fullName,
|
||||
primaryButtonText: 'Stäng',
|
||||
includeBasicFooter: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum SignalRequestType {
|
||||
Work = 'work',
|
||||
Education = 'education',
|
||||
Arbete = 'arbete',
|
||||
Utbildning = 'utbildning',
|
||||
}
|
||||
|
||||
export enum SignalRequestOmfattning {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum SignalResponseType {
|
||||
Work = 'work',
|
||||
Education = 'education',
|
||||
Arbete = 'arbete',
|
||||
Utbildning = 'utbildning',
|
||||
}
|
||||
|
||||
export enum SignalResponseOmfattning {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from '@msfa-environment';
|
||||
import { EmployeeParams, Params } from '@msfa-models/api/params.model';
|
||||
import { EmployeeResponse, EmployeesDataResponse } from '@msfa-models/api/employee.response.model';
|
||||
import { catchError, map, mapTo, take } from 'rxjs/operators';
|
||||
import {
|
||||
Employee,
|
||||
EmployeeCompact,
|
||||
mapResponseToEmployee,
|
||||
mapResponseToEmployeeCompact,
|
||||
} from '@msfa-models/employee.model';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PaginationMeta } from '@msfa-models/pagination-meta.model';
|
||||
import { EmployeeInviteResponse } from '@msfa-models/api/employee-invite.response.model';
|
||||
import { EmployeeEditRequest } from '@msfa-models/api/employee-edit.request.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EmployeeApiService {
|
||||
private _apiBaseUrl = `${environment.api.url}/users`;
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
fetchEmployees$(employeeParams: EmployeeParams): Observable<{ data: EmployeeCompact[]; meta: PaginationMeta }> {
|
||||
const { sort, order, limit, page, search, onlyEmployeesWithoutAuthorization } = employeeParams;
|
||||
const params: Params = {
|
||||
sort,
|
||||
order,
|
||||
limit: limit.toString(),
|
||||
page: page.toString(),
|
||||
};
|
||||
if (search) {
|
||||
params.search = search;
|
||||
}
|
||||
|
||||
if (onlyEmployeesWithoutAuthorization) {
|
||||
params.onlyEmployeesWithoutAuthorization = onlyEmployeesWithoutAuthorization.toString();
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.get<EmployeesDataResponse>(this._apiBaseUrl, { params })
|
||||
.pipe(
|
||||
map(({ data, meta }) => {
|
||||
return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta };
|
||||
}),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte hämta personal.\n\n${error.message}`,
|
||||
name: `GET ${this._apiBaseUrl}`,
|
||||
method: 'EmployeeApiService._fetchEmployees$',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fetchEmployee$(ciamUserId: string): Observable<Employee> {
|
||||
const apiUrl = `${this._apiBaseUrl}/${ciamUserId}`;
|
||||
return this.httpClient.get<{ data: EmployeeResponse }>(apiUrl).pipe(
|
||||
map(({ data }) => mapResponseToEmployee(data)),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte hämta personal.\n\n${error.message}`,
|
||||
name: `GET ${this._apiBaseUrl}/{ciamUserId}`,
|
||||
data: { ciamUserId },
|
||||
method: 'EmployeeApiService._fetchEmployee$',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteEmployee(employee: Employee): Observable<Employee> {
|
||||
return this.httpClient.delete<void>(`${this._apiBaseUrl}/${employee.id}`).pipe(
|
||||
map(() => employee),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte ta bort personal.\n\n${error.message}`,
|
||||
name: `DELETE ${this._apiBaseUrl}/{ciamUserId}`,
|
||||
method: 'EmployeeApiService.deleteEmployee',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
postEmployeeInvitation(emails: string[]): Observable<EmployeeInviteResponse | null> {
|
||||
const apiUrl = `${this._apiBaseUrl}/invite`;
|
||||
return this.httpClient
|
||||
.patch<{ data: EmployeeInviteResponse }>(apiUrl, { emails })
|
||||
.pipe(
|
||||
take(1),
|
||||
map(({ data }) => data),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte bjuda in personal.\n\n${error.message}`,
|
||||
name: `PATCH ${apiUrl}`,
|
||||
data: { emails },
|
||||
method: 'EmployeeApiService.postEmployeeInvitation',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateEmployee$(ciamUserId: string, data: EmployeeEditRequest): Observable<boolean> {
|
||||
return this.httpClient.put<boolean>(`${this._apiBaseUrl}/${ciamUserId}`, data).pipe(
|
||||
mapTo(true),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte redigera personal.\n\n${error.message}`,
|
||||
name: `PATCH ${this._apiBaseUrl}/{ciamUserId}`,
|
||||
data: { ciamUserId },
|
||||
method: 'EmployeeApiService.updateEmployee$',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
||||
import { SortOrder } from '@msfa-enums/sort-order.enum';
|
||||
import { environment } from '@msfa-environment';
|
||||
import { EmployeeEditRequest } from '@msfa-models/api/employee-edit.request.model';
|
||||
import { EmployeeInviteResponse } from '@msfa-models/api/employee-invite.response.model';
|
||||
import {
|
||||
EmployeeCompactResponse,
|
||||
EmployeeResponse,
|
||||
EmployeesDataResponse,
|
||||
} from '@msfa-models/api/employee.response.model';
|
||||
import { EmployeeParams, Params } from '@msfa-models/api/params.model';
|
||||
import {
|
||||
Employee,
|
||||
EmployeesData,
|
||||
mapResponseToEmployee,
|
||||
mapResponseToEmployeeCompact,
|
||||
} from '@msfa-models/employee.model';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { Sort } from '@msfa-models/sort.model';
|
||||
import { ErrorService } from '@msfa-services/error.service';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
|
||||
const DEFAULT_PARAMS: EmployeeParams = {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
sort: 'name',
|
||||
order: SortOrder.ASC,
|
||||
search: '',
|
||||
onlyEmployeesWithoutAuthorization: false,
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EmployeeService extends UnsubscribeDirective {
|
||||
private _apiBaseUrl = `${environment.api.url}/users`;
|
||||
private _currentEmployeeId$ = new BehaviorSubject<string>(null);
|
||||
private _params$ = new BehaviorSubject<EmployeeParams>(DEFAULT_PARAMS);
|
||||
public sort$: Observable<Sort<keyof EmployeeCompactResponse>> = this._params$.pipe(
|
||||
map(({ sort, order }) => ({ key: sort, order }))
|
||||
);
|
||||
public onlyEmployeesWithoutAuthorization$: Observable<boolean> = this._params$.pipe(
|
||||
map(({ onlyEmployeesWithoutAuthorization }) => onlyEmployeesWithoutAuthorization)
|
||||
);
|
||||
private _employee$ = new BehaviorSubject<Employee>(null);
|
||||
public employee$: Observable<Employee> = this._employee$.asObservable();
|
||||
private _lastUpdatedEmployeeId$ = new BehaviorSubject<string>(null);
|
||||
public lastUpdatedEmployeeId$: Observable<string> = this._lastUpdatedEmployeeId$.asObservable();
|
||||
private _lastDeletedEmployee$ = new BehaviorSubject<Employee>(null);
|
||||
public lastDeletedEmployee$: Observable<Employee> = this._lastDeletedEmployee$.asObservable();
|
||||
private _employeeToDelete$ = new BehaviorSubject<Employee>(null);
|
||||
public employeeToDelete$: Observable<Employee> = this._employeeToDelete$.asObservable();
|
||||
private _employeesLoading$ = new BehaviorSubject<boolean>(false);
|
||||
public employeesLoading$: Observable<boolean> = this._employeesLoading$.asObservable();
|
||||
|
||||
constructor(private httpClient: HttpClient, private errorService: ErrorService) {
|
||||
super();
|
||||
super.unsubscribeOnDestroy(
|
||||
combineLatest([this._currentEmployeeId$, this._lastUpdatedEmployeeId$])
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
([prevEmployeeId], [currEmployeeId, currLastUpdatedEmployeeId]) =>
|
||||
!currLastUpdatedEmployeeId && prevEmployeeId === currEmployeeId
|
||||
),
|
||||
filter(([currentEmployeeId]) => !!currentEmployeeId),
|
||||
switchMap(([currentEmployeeId]) =>
|
||||
this._fetchEmployee$(currentEmployeeId).pipe(filter(employee => !!employee))
|
||||
)
|
||||
)
|
||||
.subscribe(employee => {
|
||||
this._employee$.next(employee);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public employeesData$: Observable<EmployeesData> = combineLatest([this._params$, this._lastDeletedEmployee$]).pipe(
|
||||
switchMap(([params]) => this._fetchEmployees$(params))
|
||||
);
|
||||
|
||||
public resetParams(): void {
|
||||
this._params$.next(DEFAULT_PARAMS);
|
||||
}
|
||||
|
||||
public setCurrentEmployeeId(currentEmployeeId: string): void {
|
||||
if (this._currentEmployeeId$.getValue() !== currentEmployeeId) {
|
||||
this._employee$.next(null);
|
||||
this._currentEmployeeId$.next(currentEmployeeId);
|
||||
}
|
||||
}
|
||||
|
||||
public resetLastUpdatedEmployeeId(): void {
|
||||
this._lastUpdatedEmployeeId$.next(null);
|
||||
}
|
||||
|
||||
private _fetchEmployees$(employeeParams: EmployeeParams): Observable<EmployeesData> {
|
||||
const { sort, order, limit, page, search, onlyEmployeesWithoutAuthorization } = employeeParams;
|
||||
const params: Params = {
|
||||
sort,
|
||||
order,
|
||||
limit: limit.toString(),
|
||||
page: page.toString(),
|
||||
};
|
||||
|
||||
if (search) {
|
||||
params.search = search;
|
||||
}
|
||||
|
||||
if (onlyEmployeesWithoutAuthorization) {
|
||||
params.onlyEmployeesWithoutAuthorization = onlyEmployeesWithoutAuthorization.toString();
|
||||
}
|
||||
this._employeesLoading$.next(true);
|
||||
|
||||
return this.httpClient
|
||||
.get<EmployeesDataResponse>(this._apiBaseUrl, { params })
|
||||
.pipe(
|
||||
map(({ data, meta }) => {
|
||||
this._employeesLoading$.next(false);
|
||||
return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta };
|
||||
}),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte hämta personal.\n\n${error.message}`,
|
||||
name: `GET ${this._apiBaseUrl}`,
|
||||
method: 'EmployeeService._fetchEmployees$',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _fetchEmployee$(ciamUserId: string): Observable<Employee> {
|
||||
const apiUrl = `${this._apiBaseUrl}/${ciamUserId}`;
|
||||
return this.httpClient.get<{ data: EmployeeResponse }>(apiUrl).pipe(
|
||||
map(({ data }) => mapResponseToEmployee(data)),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte hämta personal.\n\n${error.message}`,
|
||||
name: `GET ${this._apiBaseUrl}/{ciamUserId}`,
|
||||
data: { ciamUserId },
|
||||
method: 'EmployeeService._fetchEmployee$',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public setSearchString(value: string): void {
|
||||
this._params$.next({
|
||||
...this._params$.getValue(),
|
||||
search: value,
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
|
||||
public setOnlyEmployeesWithoutAuthorization(value: boolean): void {
|
||||
this._params$.next({
|
||||
...this._params$.getValue(),
|
||||
onlyEmployeesWithoutAuthorization: value,
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
|
||||
public setEmployeeToDelete(employee: Employee): void {
|
||||
this._employeeToDelete$.next(employee);
|
||||
}
|
||||
|
||||
public deleteEmployee(employee: Employee): Observable<Employee> {
|
||||
return this.httpClient.delete<void>(`${this._apiBaseUrl}/${employee.id}`).pipe(
|
||||
tap(() => {
|
||||
this._lastDeletedEmployee$.next(employee);
|
||||
}),
|
||||
map(() => employee),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte ta bort personal.\n\n${error.message}`,
|
||||
name: `DELETE ${this._apiBaseUrl}/{ciamUserId}`,
|
||||
method: 'EmployeeService.deleteEmployee',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public setSort(sort: keyof EmployeeCompactResponse): void {
|
||||
const params = this._params$.getValue();
|
||||
const order = params.sort === sort && params.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||
|
||||
this._params$.next({ ...params, sort, order });
|
||||
}
|
||||
|
||||
public setPage(page: number): void {
|
||||
this._params$.next({ ...this._params$.getValue(), page });
|
||||
}
|
||||
|
||||
public postEmployeeInvitation(emails: string[]): Observable<EmployeeInviteResponse | null> {
|
||||
const apiUrl = `${this._apiBaseUrl}/invite`;
|
||||
return this.httpClient
|
||||
.patch<{ data: EmployeeInviteResponse }>(apiUrl, { emails })
|
||||
.pipe(
|
||||
take(1),
|
||||
map(({ data }) => data),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte bjuda in personal.\n\n${error.message}`,
|
||||
name: `PATCH ${apiUrl}`,
|
||||
data: { emails },
|
||||
method: 'EmployeeService.postEmployeeInvitation',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public updateEmployee$(ciamUserId: string, data: EmployeeEditRequest): Observable<boolean> {
|
||||
return this.httpClient.put<boolean>(`${this._apiBaseUrl}/${ciamUserId}`, data).pipe(
|
||||
take(1),
|
||||
tap(() => {
|
||||
this._employee$.next(null);
|
||||
this._lastUpdatedEmployeeId$.next(ciamUserId);
|
||||
}),
|
||||
map(() => true),
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte redigera personal.\n\n${error.message}`,
|
||||
name: `PATCH ${this._apiBaseUrl}/{ciamUserId}`,
|
||||
data: { ciamUserId },
|
||||
method: 'EmployeeService.updateEmployee$',
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
### [2.5.1](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/compare/diff?targetBranch=refs%2Ftags%2Fv2.5.0&sourceBranch=refs%2Ftags%2Fv2.5.1) (2022-01-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Avvikelserapporten:** Använd nya dialogen ([TV-845](https://jira.arbetsformedlingen.se/browse/TV-845)) ([8909f7c](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/8909f7c3d1e91833988c69d1f8485cd217654e32))
|
||||
* **Nya deltagare:** Använd nya dialogen ([TV-845](https://jira.arbetsformedlingen.se/browse/TV-845)) ([b943474](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/b943474138442cec88f371e8477126f8dacc7e07))
|
||||
* **automatisk utloggning:** Använder nu den nya dialog-komponenten ([TV-845](https://jira.arbetsformedlingen.se/browse/TV-845)) ([1de7624](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/1de762420054ca6782660dd950c938541f543291)), closes [feature/TV-845-ui-dialog-idle-timout-dialog-2](https://jira.arbetsformedlingen.se/browse/TV-845-ui-dialog-idle-timout-dialog-2) [feature/TV-845-ui-dialog-2](https://jira.arbetsformedlingen.se/browse/TV-845-ui-dialog-2) [feature/TV-845-ui-dialog-2](https://jira.arbetsformedlingen.se/browse/TV-845-ui-dialog-2)
|
||||
* **Deltagarlista:** Nu går det att sortera när man klickar på kolumnnamnet ([TV-989](https://jira.arbetsformedlingen.se/browse/TV-989)) ([fb6239a](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/fb6239afbe2cfe2652f0f9b407f684ddc77e239a))
|
||||
* **Signal om arbete eller studier:** Uppdaterad UX för val av sysselsättning (fyra radioknappar, ingen drop-down ([TV-440](https://jira.arbetsformedlingen.se/browse/TV-440)) ([65c29e0](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/65c29e065d66cda8e6c6fa71af30558d303d9d39))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **informativ-rapport:** Added file-upload inside informativ-rapport to feature toggling ([92cf04e](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/92cf04e802cce05c1ed799cabfabf73b1c370c0c))
|
||||
* **gemensam-planering:** Implemented new API response for GP ([a158b4a](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/a158b4ac21f9dbab84865f461010dab63c50a7fa))
|
||||
* **login:** Removed login with username/password from prod builds ([a6f3792](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/a6f37923abc70627e8b0b98f18f5f3791321da3d))
|
||||
* **texter:** Updated button text and changed aria texts for filter functionality inside utförande verksamheter ([TV-953](https://jira.arbetsformedlingen.se/browse/TV-953)) ([8fb58d6](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/commits/8fb58d698ce26e5031174040978f69ba1af1f57e))
|
||||
|
||||
## [2.5.0](https://bitbucket.arbetsformedlingen.se/projects/tea/repos/mina-sidor-fa-web/compare/diff?targetBranch=refs%2Ftags%2Fv2.4.0&sourceBranch=refs%2Ftags%2Fv2.5.0) (2021-12-14)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="ui-dialog-layout">
|
||||
<ui-loader *ngIf="isLoading" uiType="absolute"></ui-loader>
|
||||
<div class="ui-dialog-layout__heading">
|
||||
<ng-content select="[uiDialogHeading]"></ng-content>
|
||||
</div>
|
||||
|
||||
<div class="ui-dialog-layout__scrollable-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
<footer class="ui-dialog-layout__footer">
|
||||
<ng-content select="[uiDialogFooter]"></ng-content>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
@import 'variables/shadows';
|
||||
@import 'variables/gutters';
|
||||
|
||||
.ui-dialog-layout {
|
||||
padding-top: $digi--layout--gutter--s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $digi--layout--gutter--s;
|
||||
max-height: 90vh;
|
||||
&__heading {
|
||||
flex: 1 1 2rem;
|
||||
}
|
||||
&__scrollable-content {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
position: absolute;
|
||||
top: var(--digi--layout--gutter);
|
||||
right: var(--digi--layout--gutter--s);
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__close-button-text {
|
||||
font-size: var(--digi--typography--font-size--s);
|
||||
}
|
||||
|
||||
&__footer {
|
||||
flex: 1 0 4rem;
|
||||
min-height: var(--digi--layout--gutter);
|
||||
display: flex;
|
||||
margin-top: $digi--layout--gutter--l;
|
||||
gap: var(--digi--layout--gutter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UiDialogLayoutComponent } from './ui-dialog-layout.component';
|
||||
|
||||
describe('UiDialogLayoutComponent', () => {
|
||||
let component: UiDialogLayoutComponent;
|
||||
let fixture: ComponentFixture<UiDialogLayoutComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ UiDialogLayoutComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UiDialogLayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-dialog-layout',
|
||||
templateUrl: './ui-dialog-layout.component.html',
|
||||
styleUrls: ['./ui-dialog-layout.component.scss'],
|
||||
})
|
||||
export class UiDialogLayoutComponent {
|
||||
@Input() uiHeading: string;
|
||||
@Input() isLoading = false;
|
||||
}
|
||||
@@ -21,12 +21,18 @@ export class UiDialogRef<CloseResponseData = unknown, InputDataType = unknown> {
|
||||
overlay.backdropClick().subscribe(() => this._close('backdropClick', null));
|
||||
}
|
||||
|
||||
get includeBasicFooter(): boolean {
|
||||
return this.config.includeBasicFooter;
|
||||
}
|
||||
get primaryButtonText(): string {
|
||||
return this.config.primaryButtonText;
|
||||
}
|
||||
get secondaryButtonText(): string {
|
||||
return this.config.secondaryButtonText;
|
||||
}
|
||||
get heading(): string {
|
||||
return this.config.heading;
|
||||
}
|
||||
|
||||
close(data?: CloseResponseData): void {
|
||||
this._close('close', data);
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<div class="ui-dialog">
|
||||
<ng-container [ngSwitch]="contentType">
|
||||
<ng-container *ngSwitchCase="'string'">
|
||||
<div class="box">
|
||||
<h2 class="ui-dialog__heading" *ngIf="heading">{{heading}}</h2>
|
||||
<div [ngClass]="{'ui-dialog__scrollable-content': includeBasicFooter}">
|
||||
<ng-container [ngSwitch]="contentType">
|
||||
<ng-container *ngSwitchCase="'string'">
|
||||
<div [innerHTML]="content"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<footer class="ui-dialog__footer">
|
||||
<digi-button af-type="button" (click)="close()">Stäng</digi-button>
|
||||
</footer>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'template'">
|
||||
<ng-container *ngTemplateOutlet="content; context: context"></ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'template'">
|
||||
<ng-container *ngTemplateOutlet="content; context: context"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'component'">
|
||||
<ng-container *ngComponentOutlet="content"></ng-container>
|
||||
<ng-container *ngSwitchCase="'component'">
|
||||
<ng-container *ngComponentOutlet="content"></ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<footer class="ui-dialog__footer">
|
||||
<footer class="ui-dialog__footer" *ngIf="includeBasicFooter">
|
||||
<digi-button af-type="button" (click)="primaryAction()">{{primaryButtonText}}</digi-button>
|
||||
<digi-button af-type="button" af-variation="secondary" *ngIf="secondaryButtonText" (click)="secondaryAction()">
|
||||
{{secondaryButtonText}}
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
box-shadow: $msfa__shadow;
|
||||
padding: $digi--layout--gutter--s $digi--layout--gutter--xl $digi--layout--gutter--l;
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__heading {
|
||||
flex: 1 1 2rem;
|
||||
}
|
||||
|
||||
&__scrollable-content {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
position: absolute;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UiDialogComponent } from './ui-dialog.component';
|
||||
|
||||
describe('DialogComponent', () => {
|
||||
xdescribe('DialogComponent', () => {
|
||||
let component: UiDialogComponent;
|
||||
let fixture: ComponentFixture<UiDialogComponent>;
|
||||
|
||||
|
||||
@@ -28,11 +28,18 @@ export class UiDialogComponent implements OnInit {
|
||||
get secondaryButtonText(): string {
|
||||
return this.uiDialogRef.secondaryButtonText;
|
||||
}
|
||||
get heading(): string {
|
||||
return this.uiDialogRef.heading;
|
||||
}
|
||||
|
||||
primaryAction(): void {
|
||||
this.uiDialogRef.primaryAction();
|
||||
}
|
||||
|
||||
get includeBasicFooter(): boolean {
|
||||
return this.uiDialogRef.includeBasicFooter;
|
||||
}
|
||||
|
||||
secondaryAction(): void {
|
||||
this.uiDialogRef.secondaryAction();
|
||||
}
|
||||
|
||||
@@ -4,10 +4,16 @@ export const UI_DIALOG_DATA = 'UI_DIALOG_DATA';
|
||||
|
||||
export interface UiDialogConfig<DialogInputData = unknown> {
|
||||
data?: DialogInputData;
|
||||
minWidth?: string;
|
||||
maxWidth?: string;
|
||||
minHeight?: string;
|
||||
maxHeight?: string;
|
||||
heading?: string;
|
||||
|
||||
/**
|
||||
* primaryButtonText defaults to 'Stäng'
|
||||
*/
|
||||
includeBasicFooter?: boolean;
|
||||
primaryButtonText?: string;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,11 +3,15 @@ import { CommonModule } from '@angular/common';
|
||||
import { UiDialogComponent } from './ui-dialog.component';
|
||||
import { UiIconModule } from '@ui/icon/icon.module';
|
||||
import { UiDialog } from '@ui/dialog/ui-dialog.service';
|
||||
import { UiDialogLayoutComponent } from './ui-dialog-layout/ui-dialog-layout.component';
|
||||
import { UiLoaderModule } from '@ui/loader/loader.module';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [UiDialogComponent],
|
||||
imports: [CommonModule, UiIconModule],
|
||||
declarations: [UiDialogComponent, UiDialogLayoutComponent],
|
||||
imports: [CommonModule, UiIconModule, UiLoaderModule, OverlayModule],
|
||||
providers: [UiDialog],
|
||||
exports: [UiDialogLayoutComponent],
|
||||
})
|
||||
export class UiDialogModule {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { UiDialogRef } from '@ui/dialog/ui-dialog-ref';
|
||||
import { UiDialogComponent } from './ui-dialog.component';
|
||||
import { UI_DIALOG_DATA, UiDialogConfig } from '@ui/dialog/ui-dialog.model';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UiDialog {
|
||||
constructor(private overlay: Overlay, private injector: Injector) {}
|
||||
|
||||
@@ -19,22 +19,24 @@ export class UiDialog {
|
||||
});
|
||||
}
|
||||
|
||||
open<DialogContent = unknown, T = unknown>(
|
||||
open<CloseResponseData = unknown, InputDataType = unknown>(
|
||||
content: string | TemplateRef<unknown> | Type<unknown>,
|
||||
config: UiDialogConfig<T> = {}
|
||||
): UiDialogRef<DialogContent> {
|
||||
config: UiDialogConfig<InputDataType> = { includeBasicFooter: false }
|
||||
): UiDialogRef<CloseResponseData> {
|
||||
const positionStrategy = this.overlay.position().global().centerHorizontally().centerVertically();
|
||||
const configs = new OverlayConfig({
|
||||
positionStrategy,
|
||||
minWidth: '40rem',
|
||||
minHeight: '40rem',
|
||||
minWidth: config.minWidth ?? '20rem',
|
||||
minHeight: config.minHeight ?? '10rem',
|
||||
maxWidth: config.maxWidth ?? '60rem',
|
||||
maxHeight: config.maxHeight ?? '60rem',
|
||||
hasBackdrop: true,
|
||||
scrollStrategy: this.overlay.scrollStrategies.close(),
|
||||
scrollStrategy: this.overlay.scrollStrategies.block(),
|
||||
backdropClass: 'cdk-overlay-dark-backdrop',
|
||||
});
|
||||
|
||||
const overlayRef = this.overlay.create(configs);
|
||||
const uiDialogRef = new UiDialogRef<DialogContent, T>(overlayRef, content, config);
|
||||
const uiDialogRef = new UiDialogRef<CloseResponseData, InputDataType>(overlayRef, content, config);
|
||||
|
||||
const injector = UiDialog._createInjector(uiDialogRef, this.injector, config);
|
||||
overlayRef.attach(new ComponentPortal(UiDialogComponent, null, injector));
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mina-sidor-fa-web",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mina-sidor-fa-web",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mina-sidor-fa-web",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.1",
|
||||
"license": "MIT",
|
||||
"repository": "https://bitbucket.arbetsformedlingen.se/projects/TEA/repos/mina-sidor-fa-web",
|
||||
"engines": {
|
||||
|
||||
@@ -48,14 +48,14 @@ trap exitMessage EXIT
|
||||
# ----------------------------------
|
||||
# RUN ESLINT
|
||||
# ----------------------------------
|
||||
echo -e "${CYAN}Running e2e tests using Cypress${NOCOLOR}"
|
||||
echo -e "${CYAN}Running lint${NOCOLOR}"
|
||||
npm run lint
|
||||
echo -e "${GREEN}Tests finished${NOCOLOR}"
|
||||
|
||||
# ----------------------------------
|
||||
# TESTS
|
||||
# ----------------------------------
|
||||
echo -e "${CYAN}Running e2e tests using Cypress${NOCOLOR}"
|
||||
echo -e "${CYAN}Running tests${NOCOLOR}"
|
||||
npm run test
|
||||
echo -e "${GREEN}Tests finished${NOCOLOR}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user