feature(Periodisk redovisning): Formulär för periodisk redovisning (TV-771)
Squashed commit of the following: commit eee14a464fe2fe2a99074f0fe92eecfc92cd05fa Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Wed Oct 20 14:09:48 2021 +0200 styling commit b95bac31ac2b33b5c383a32f06ababf3e5f00245 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Wed Oct 20 13:35:03 2021 +0200 Update periodisk-redovisning.validator.ts commit aeda04cd6705e72b5621a3079904617322ce3036 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Wed Oct 20 13:34:15 2021 +0200 Deltagaren har inte deltagit i några aktiviteter denna period checkbox commit f6ee1ff62d5001e8319bfff04ceb6950ebce9cff Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Wed Oct 20 11:12:57 2021 +0200 form validation and dialog done commit 93e5345d13caf5ab25dc581d58efe92f85acb2dd Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Wed Oct 20 09:16:03 2021 +0200 hidden checkboxes commit 68c2f17ec8417ce5a0404d5b0c00e4800b738143 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Wed Oct 20 08:19:01 2021 +0200 Update app.module.ts commit 2a1dfa6559b9b86839de8ddd1d8cd7c821a56b3a Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Oct 19 21:56:22 2021 +0200 form array with checkboxes done commit 32f26800656d13d1c6c30b20c8187b20fda3c71c Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Oct 19 17:04:33 2021 +0200 activity form array commit db2974cfcca453390ebb4f637daf9d9064b527da Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Oct 19 15:56:39 2021 +0200 add radiobuttons commit 2c4099b48337aaad1cb5b0cc4794ee94e6bb508c Merge: 1ae24a9025b12092Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Oct 19 14:07:46 2021 +0200 Merge branch 'develop' into feature/TV-771-periodisk-redovisning commit 1ae24a905a6c915dcc7d5e3b0cf77a8b62b44d7c Merge: 79e0cf39 794bbc9a Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Tue Oct 19 10:14:29 2021 +0200 Merge branch 'feature/TV-771-periodisk-redovisning' of ssh://bitbucket.arbetsformedlingen.se:7999/tea/mina-sidor-fa-web into feature/TV-771-periodisk-redovisning commit 79e0cf394055527ba09f0d1ae97ddc7c519f2236 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Tue Oct 19 10:14:18 2021 +0200 Updated periods commit 794bbc9a71a0e638196d961ed8b3093de5a64e49 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Tue Oct 19 09:30:24 2021 +0200 Update package-lock.json commit 56351afb1f92060b9f743233a69a785114a3ee96 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Oct 18 17:03:19 2021 +0200 Update periodisk-redovisning-form.component.ts commit 213e6c888a8e388381cf4370d2f5020987b29c4f Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Oct 18 17:01:17 2021 +0200 Update extract-avrop-periods.ts commit 4bcd9669b70070654111f650e6b20d8d8981b3a1 Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Oct 18 17:00:47 2021 +0200 avrop periods commit cee788517c34107a2f651313038c343bb4fc702e Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Oct 18 15:46:48 2021 +0200 clean up commit 3d1d2414270a0de1111ba8b16194dc82ec5bbe79 Merge: b6304eed9104fc31Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se> Date: Mon Oct 18 15:37:08 2021 +0200 Merge branch 'develop' into feature/periodisk-redovisning commit b6304eedf683ba9679e38628d9d3cc33c07103a7 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Fri Oct 15 14:37:59 2021 +0200 Added testdata to test around with inside the component commit d036a771e9139ed6523f71078fa0cb76b936c88b Merge: cb2198415d2f63b9Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Fri Oct 15 14:03:25 2021 +0200 Merge branch 'develop' into feature/periodisk-redovisning ... and 3 more commits
This commit is contained in:
@@ -1,30 +1,37 @@
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||
import localeSe from '@angular/common/locales/sv';
|
||||
import { ErrorHandler, LOCALE_ID, NgModule } from '@angular/core';
|
||||
import { ErrorHandler, LOCALE_ID, NgModule, Provider } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ApmErrorHandler } from '@elastic/apm-rum-angular';
|
||||
import { environment } from '@msfa-environment';
|
||||
import { AuthInterceptor } from '@msfa-interceptors/auth.interceptor';
|
||||
import { CustomErrorHandler } from '@msfa-interceptors/custom-error-handler';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { ToastListModule } from './components/toast-list/toast-list.module';
|
||||
import { LoggingModule } from './logging.module';
|
||||
import { AvropModule } from './pages/avrop/avrop.module';
|
||||
import { CustomErrorHandler } from '@msfa-interceptors/custom-error-handler';
|
||||
|
||||
registerLocaleData(localeSe);
|
||||
const providers: Provider[] = [
|
||||
ApmErrorHandler,
|
||||
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{ provide: LOCALE_ID, useValue: 'sv-SE' },
|
||||
];
|
||||
// Skip error handler in Dev until "Uncaught Error: ApplicationRef.tick is called recursively" is fixed
|
||||
if (environment.production) {
|
||||
providers.push({
|
||||
provide: ErrorHandler,
|
||||
useClass: CustomErrorHandler,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [LoggingModule, BrowserModule, HttpClientModule, AppRoutingModule, ToastListModule, AvropModule],
|
||||
providers: [
|
||||
ApmErrorHandler,
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: CustomErrorHandler,
|
||||
},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{ provide: LOCALE_ID, useValue: 'sv-SE' },
|
||||
],
|
||||
providers,
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { Feature } from '@msfa-enums/feature.enum';
|
||||
import { environment } from '@msfa-environment';
|
||||
|
||||
const activeFeatures: Feature[] = environment.activeFeatures;
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -55,21 +59,32 @@ const routes: Routes = [
|
||||
m => m.GemensamPlaneringViewModule
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'periodisk-redovisning',
|
||||
data: { title: 'Skapa periodisk redovisning' },
|
||||
loadChildren: () =>
|
||||
import('./pages/report-forms/deltagare-periodisk-redovisning/deltagare-periodisk-redovisning.module').then(
|
||||
m => m.DeltagarePeriodiskRedovisningModule
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'signal',
|
||||
data: { title: 'Skapa signal om arbete eller studier' },
|
||||
loadChildren: () => import('./pages/report-forms/signal-form/signal-form.module').then(m => m.SignalFormModule),
|
||||
},
|
||||
];
|
||||
|
||||
activeFeatures.forEach(feature => {
|
||||
switch (feature) {
|
||||
case Feature.REPORTING_PERIODISK_REDOVISNING:
|
||||
routes.push({
|
||||
path: 'periodisk-redovisning',
|
||||
data: { title: 'Skapa Periodisk redovisning' },
|
||||
loadChildren: () =>
|
||||
import('./pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.module').then(
|
||||
m => m.PeriodiskRedovisningFormModule
|
||||
),
|
||||
});
|
||||
break;
|
||||
case Feature.REPORTING_SIGNAL:
|
||||
routes.push({
|
||||
path: 'signal',
|
||||
data: { title: 'Skapa signal om arbete eller studier' },
|
||||
loadChildren: () => import('./pages/report-forms/signal-form/signal-form.module').then(m => m.SignalFormModule),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
})
|
||||
|
||||
@@ -24,7 +24,14 @@
|
||||
afText="Skapa ny Avvikelserapport (avvikelse)"
|
||||
></digi-ng-link-button>
|
||||
</li>
|
||||
<li>
|
||||
<li *ngIf="periodiskRedovisningButtonVisible">
|
||||
<digi-ng-link-button
|
||||
class="deltagare-tab-reports__button"
|
||||
afRoute="./periodisk-redovisning"
|
||||
afText="Skapa ny Periodisk redovisning"
|
||||
></digi-ng-link-button>
|
||||
</li>
|
||||
<li *ngIf="signalButtonVisible">
|
||||
<digi-ng-link-button
|
||||
class="deltagare-tab-reports__button"
|
||||
afRoute="./signal"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Feature } from '@msfa-enums/feature.enum';
|
||||
import { environment } from '@msfa-environment';
|
||||
import { ReportsData } from '@msfa-models/report.model';
|
||||
|
||||
@Component({
|
||||
@@ -11,6 +13,15 @@ export class DeltagareTabReportsComponent {
|
||||
@Input() reportsData: ReportsData;
|
||||
@Output() reportsPaginated = new EventEmitter<number>();
|
||||
|
||||
_activeFeatures: Feature[] = environment.activeFeatures;
|
||||
|
||||
get signalButtonVisible(): boolean {
|
||||
return this._activeFeatures.includes(Feature.REPORTING_SIGNAL);
|
||||
}
|
||||
get periodiskRedovisningButtonVisible(): boolean {
|
||||
return this._activeFeatures.includes(Feature.REPORTING_PERIODISK_REDOVISNING);
|
||||
}
|
||||
|
||||
emitNewPage(page: number): void {
|
||||
this.reportsPaginated.emit(page);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
||||
import { Feature } from '@msfa-enums/feature.enum';
|
||||
import { RoleEnum } from '@msfa-enums/role.enum';
|
||||
import { environment } from '@msfa-environment';
|
||||
@@ -19,7 +20,6 @@ import { HandledareService } from '@msfa-services/handledare.service';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
|
||||
import { DeltagareCardService } from './deltagare-card.service';
|
||||
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-deltagare-details',
|
||||
@@ -113,7 +113,7 @@ export class DeltagareCardComponent extends UnsubscribeDirective {
|
||||
|
||||
get sensitiveDataVisible(): boolean {
|
||||
return (
|
||||
this._activeFeatures.includes(Feature.SENSITIVE_INFORMATION) &&
|
||||
this._activeFeatures.includes(Feature.DELTAGARE_SENSITIVE_INFORMATION) &&
|
||||
this._userRoles?.some(role => role.type === RoleEnum.MSFA_ReportAndPlanning)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,8 +129,7 @@
|
||||
</ng-container>
|
||||
</dl>
|
||||
<digi-notification-alert
|
||||
*ngIf="error$ | async as error"
|
||||
class="avvikelse-report__alert"
|
||||
*ngIf="submitError$ | async as error"
|
||||
af-variation="danger"
|
||||
af-heading="Någonting gick fel"
|
||||
>
|
||||
|
||||
@@ -40,7 +40,7 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
reportingDateFormName: AvvikelseFormKeys = 'reportingDate';
|
||||
|
||||
submitIsLoading$ = new BehaviorSubject<boolean>(false);
|
||||
error$ = new BehaviorSubject<CustomError>(null);
|
||||
submitError$ = new BehaviorSubject<CustomError>(null);
|
||||
|
||||
genomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
|
||||
map((params: Params) => +params.genomforandeReferens)
|
||||
@@ -110,10 +110,6 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
this.subscriptions.push(
|
||||
this.chosenReason$.subscribe(() => {
|
||||
this.shouldValidate$.next(false);
|
||||
}),
|
||||
this.questionsForChosenReason$.subscribe(questions => {
|
||||
this.clearQuestions();
|
||||
questions.forEach(question => this.addQuestionToForm(question));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -157,7 +153,7 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
this.confirmDialogIsOpen$.next(false);
|
||||
},
|
||||
error: (customError: CustomError) => {
|
||||
this.error$.next({ ...customError, message: customError.error.message });
|
||||
this.submitError$.next({ ...customError, message: customError.error.message });
|
||||
this.submitIsLoading$.next(false);
|
||||
throw { ...customError, avoidToast: true };
|
||||
},
|
||||
@@ -167,7 +163,7 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
cancelConfirmDialog(): void {
|
||||
this.confirmDialogIsOpen$.next(false);
|
||||
this.error$.next(null);
|
||||
this.submitError$.next(null);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
<dd>{{expectedPresenceStartTimeFormControl.value}} - {{expectedPresenceEndTimeFormControl.value}}</dd>
|
||||
</dl>
|
||||
<digi-notification-alert
|
||||
*ngIf="error$ | async as error"
|
||||
*ngIf="submitError$ | async as error"
|
||||
class="franvaro-report-form__alert"
|
||||
af-variation="danger"
|
||||
af-heading="Någonting gick fel"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Avrop } from '@msfa-models/avrop.model';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { FranvaroReason } from '@msfa-models/franvaro-reason.model';
|
||||
import { Franvaro } from '@msfa-models/franvaro.model';
|
||||
import { dateToIsoString } from '@msfa-utils/format-to-date.util';
|
||||
import { formatDate } from '@msfa-utils/format-to-date.util';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
import { FranvaroReportFormService } from './franvaro-report-form.service';
|
||||
@@ -39,7 +39,7 @@ export class FranvaroReportFormComponent {
|
||||
[FranvaroReportFormValidator.isFranvaroReportValid()]
|
||||
);
|
||||
|
||||
error$ = new BehaviorSubject<CustomError>(null);
|
||||
submitError$ = new BehaviorSubject<CustomError>(null);
|
||||
submitLoading$ = new BehaviorSubject<boolean>(false);
|
||||
lastSubmittedFranvaroReport$ = new BehaviorSubject<Date>(null);
|
||||
currentGenomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
|
||||
@@ -142,7 +142,7 @@ export class FranvaroReportFormComponent {
|
||||
|
||||
cancelConfirmDialog(): void {
|
||||
this.confirmDialogOpen$.next(false);
|
||||
this.error$.next(null);
|
||||
this.submitError$.next(null);
|
||||
}
|
||||
|
||||
reasonChanged(): void {
|
||||
@@ -168,7 +168,7 @@ export class FranvaroReportFormComponent {
|
||||
genomforandeReferens: +genomforandeReferens,
|
||||
franvaro: {
|
||||
avvikelseOrsaksKod: reason,
|
||||
datum: dateToIsoString(date),
|
||||
datum: formatDate(date),
|
||||
heldag: wholeDay,
|
||||
startTid: this.showTimePickers ? startTime : '0:00', // BÄR doesn't accept empty string or null
|
||||
slutTid: this.showTimePickers ? endTime : '23:59', // BÄR doesn't accept empty string or null
|
||||
@@ -195,7 +195,7 @@ export class FranvaroReportFormComponent {
|
||||
this.confirmDialogOpen$.next(false);
|
||||
},
|
||||
error: (customError: CustomError) => {
|
||||
this.error$.next({ ...customError, message: customError.error.message });
|
||||
this.submitError$.next({ ...customError, message: customError.error.message });
|
||||
this.submitLoading$.next(false);
|
||||
throw { ...customError, avoidToast: true };
|
||||
},
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { formatDate } from '@msfa-utils/format-to-date.util';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { extractAvropPeriods } from './extract-avrop-periods';
|
||||
|
||||
describe('extract-avrop-periods', function () {
|
||||
describe('when avrop is between 2021-05-01 and 2021-08-15', function () {
|
||||
const startDate = parseISO('2021-05-01');
|
||||
const endDate = parseISO('2021-08-15');
|
||||
const periods = extractAvropPeriods(startDate, endDate);
|
||||
|
||||
it('should yield 3 periods between 2021-05-01 and 2021-08-15', function () {
|
||||
expect(periods.map(period => period.periodId)).toEqual(['2021-05', '2021-06', '2021-07', '2021-08']);
|
||||
});
|
||||
|
||||
it('first period should start at startDate', function () {
|
||||
expect(periods[0].startDate).toEqual(startDate);
|
||||
});
|
||||
|
||||
it('second period should start with first day of month', function () {
|
||||
expect(formatDate(periods[1].startDate)).toEqual('2021-06-01');
|
||||
});
|
||||
|
||||
it('second period should end with last day of month', function () {
|
||||
expect(formatDate(periods[1].endDate)).toEqual('2021-06-30');
|
||||
});
|
||||
|
||||
it('last period should end with last day of avrop', function () {
|
||||
expect(formatDate(periods[periods.length - 1].endDate)).toEqual('2021-08-15');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if startDate is greater than endDate', function () {
|
||||
const startDate = parseISO('2021-09-01');
|
||||
const endDate = parseISO('2021-08-15');
|
||||
expect(() => extractAvropPeriods(startDate, endDate)).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw if startDate is null', function () {
|
||||
const startDate = null;
|
||||
const endDate = parseISO('2021-08-15');
|
||||
expect(() => extractAvropPeriods(startDate, endDate)).toThrowError();
|
||||
});
|
||||
|
||||
it('should throw if endDate is null', function () {
|
||||
const startDate = parseISO('2021-08-15');
|
||||
const endDate = null;
|
||||
expect(() => extractAvropPeriods(startDate, endDate)).toThrowError();
|
||||
});
|
||||
|
||||
it('should work if startDate is end of short month', function () {
|
||||
const startDate = parseISO('2021-02-28');
|
||||
const endDate = parseISO('2021-03-31');
|
||||
const periods = extractAvropPeriods(startDate, endDate);
|
||||
expect(periods.map(period => period.periodId)).toEqual(['2021-02', '2021-03']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { AvropPeriod } from '@msfa-models/avrop-period.model';
|
||||
import { addMonths, endOfMonth, formatISO, startOfMonth, subMonths } from 'date-fns';
|
||||
|
||||
export function dateToPeriodId(date: Date): string {
|
||||
return formatISO(date).slice(0, 7);
|
||||
}
|
||||
|
||||
export function extractAvropPeriods(avropStartDate: Date, avropEndDate: Date): AvropPeriod[] {
|
||||
const today = new Date();
|
||||
|
||||
if (avropStartDate > today) {
|
||||
throw new Error('Avropet har inte börjat ännu.');
|
||||
}
|
||||
if (avropStartDate > avropEndDate) {
|
||||
throw new Error('Avropets startdatum måste komma innan slutdatumet.');
|
||||
}
|
||||
|
||||
const previousPeriod = dateToPeriodId(subMonths(today, 1));
|
||||
const periods: AvropPeriod[] = [] as AvropPeriod[];
|
||||
let dateCounter = avropStartDate;
|
||||
|
||||
while (dateCounter <= avropEndDate && dateToPeriodId(dateCounter) <= previousPeriod) {
|
||||
const startDate = dateCounter === avropStartDate ? avropStartDate : startOfMonth(dateCounter);
|
||||
const endDate = endOfMonth(dateCounter) > avropEndDate ? avropEndDate : endOfMonth(dateCounter);
|
||||
const period: AvropPeriod = { startDate, endDate, periodId: dateToPeriodId(dateCounter) };
|
||||
periods.push(period);
|
||||
dateCounter = addMonths(dateCounter, 1);
|
||||
}
|
||||
return periods;
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<msfa-layout>
|
||||
<msfa-report-layout
|
||||
*ngIf="avrop$ | async as avrop; else skeletonRef"
|
||||
reportTitle="Skapa Periodisk redovisning"
|
||||
[avrop]="avrop"
|
||||
>
|
||||
<div class="periodisk-redovisning-form" *ngIf="genomforandeReferens$ | async as genomforandeReferens">
|
||||
<div
|
||||
class="periodisk-redovisning-form__confirmation"
|
||||
*ngIf="submittedDate$ | async as submittedDate; else formRef"
|
||||
>
|
||||
<digi-notification-alert
|
||||
class="periodisk-redovisning-form__alert"
|
||||
af-variation="success"
|
||||
af-heading="Allt gick bra"
|
||||
af-heading-level="h3"
|
||||
>
|
||||
<p>Periodisk redovisning för deltagare {{avrop.fullName}} är nu inskickad till Arbetsförmedlingen.</p>
|
||||
<dl>
|
||||
<dt>Datum</dt>
|
||||
<dd>{{submittedDate | date:'longDate'}} kl {{submittedDate | date:'shortTime'}}</dd>
|
||||
</dl>
|
||||
</digi-notification-alert>
|
||||
<msfa-back-link route="../">Tillbaka till deltagaren</msfa-back-link>
|
||||
</div>
|
||||
<ng-template #formRef>
|
||||
<form
|
||||
*ngIf="periods$ | async as periods; else loadingRef"
|
||||
class="periodisk-redovisning-form__form"
|
||||
[formGroup]="formGroup"
|
||||
(ngSubmit)="openConfirmDialog()"
|
||||
id="periodisk-redovisning-form"
|
||||
>
|
||||
<digi-ng-form-select
|
||||
afLabel="Period"
|
||||
[afSelectItems]="periodsToFormselectItems(periods)"
|
||||
[formControl]="periodFormControl"
|
||||
[afInvalid]="formControlIsInvalid(periodFormControl)"
|
||||
></digi-ng-form-select>
|
||||
|
||||
<div>
|
||||
<digi-form-fieldset
|
||||
af-legend="Har ni, under perioden, tillhandahållit språkstöd?"
|
||||
af-name="languageSupport"
|
||||
af-form="periodisk-redovisning-form"
|
||||
>
|
||||
<digi-ng-form-radiobutton-group
|
||||
[afRadiobuttonGroupDirection]="radiobuttonGroupDirection.HORIZONTAL"
|
||||
[afRadiobuttons]="[{label:'Ja', value: true}, {label:'Nej', value: false}]"
|
||||
[formControl]="hasOfferedLanguageSupportFormControl"
|
||||
></digi-ng-form-radiobutton-group>
|
||||
</digi-form-fieldset>
|
||||
<digi-form-validation-message
|
||||
*ngIf="formControlIsInvalid(hasOfferedLanguageSupportFormControl)"
|
||||
af-variation="error"
|
||||
>
|
||||
Ett val är obligatoriskt
|
||||
</digi-form-validation-message>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<digi-form-fieldset
|
||||
af-legend="Har ni erbjudit arbete?"
|
||||
af-name="jobOffered"
|
||||
af-form="periodisk-redovisning-form"
|
||||
>
|
||||
<digi-ng-form-radiobutton-group
|
||||
[afRadiobuttonGroupDirection]="radiobuttonGroupDirection.HORIZONTAL"
|
||||
[afRadiobuttons]="[{label:'Ja', value: true}, {label:'Nej', value: false}]"
|
||||
[formControl]="hasOfferedJobFormControl"
|
||||
></digi-ng-form-radiobutton-group>
|
||||
</digi-form-fieldset>
|
||||
<digi-form-validation-message *ngIf="formControlIsInvalid(hasOfferedJobFormControl)" af-variation="error">
|
||||
Ett val är obligatoriskt
|
||||
</digi-form-validation-message>
|
||||
</div>
|
||||
<digi-form-fieldset
|
||||
af-legend="Ange aktiviteter som har utförts under perioden"
|
||||
af-name="activities"
|
||||
af-form="periodisk-redovisning-form"
|
||||
>
|
||||
<div class="periodisk-redovisning-form__no-activities-has-been-conducted-checkbox">
|
||||
<digi-ng-form-checkbox
|
||||
formControlName="noActivitiesHasBeenConducted"
|
||||
afLabel="Deltagaren har inte deltagit i några aktiviteter denna period"
|
||||
[afInvalid]="this.shouldValidate$.value && !!formErrors?.activitiesMismatch"
|
||||
></digi-ng-form-checkbox>
|
||||
<ng-container *ngIf="this.shouldValidate$.value && !!formErrors?.activitiesMismatch">
|
||||
<digi-form-validation-message af-variation="error">
|
||||
{{formErrors?.activitiesMismatch}}
|
||||
</digi-form-validation-message>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div
|
||||
[formArrayName]="ACTIVITES_FORM_NAME"
|
||||
class="periodisk-redovisning-form__activity-checkboxes"
|
||||
*ngFor="let activityFormGroup of activitiesFormArray.controls; let i=index"
|
||||
>
|
||||
<div [formGroupName]="i" class="">
|
||||
<digi-ng-form-checkbox
|
||||
#isSelected
|
||||
formControlName="isSelected"
|
||||
[afLabel]="activitiesFormArrayMetadata[i].name"
|
||||
></digi-ng-form-checkbox>
|
||||
<div class="periodisk-redovisning-form__activity-location-checkboxes">
|
||||
<digi-ng-form-checkbox
|
||||
*ngIf="isSelected.currentValue"
|
||||
formControlName="performedRemotely"
|
||||
[afInvalid]="activityLocationIsInvalid(activityFormGroup)"
|
||||
afLabel="Utfört på distans"
|
||||
></digi-ng-form-checkbox>
|
||||
<digi-ng-form-checkbox
|
||||
*ngIf="isSelected.currentValue"
|
||||
formControlName="performedPhysically"
|
||||
[afInvalid]="activityLocationIsInvalid(activityFormGroup)"
|
||||
afLabel="Utfört på plats"
|
||||
></digi-ng-form-checkbox>
|
||||
<ng-container *ngIf="formControlIsInvalid(activityFormGroup)">
|
||||
<digi-form-validation-message
|
||||
*ngFor="let errorText of errorsToArray(activityFormGroup.errors)"
|
||||
af-variation="error"
|
||||
>
|
||||
{{errorText}}
|
||||
</digi-form-validation-message>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</digi-form-fieldset>
|
||||
|
||||
<footer class="periodisk-redovisning-form__footer">
|
||||
<div class="periodisk-redovisning-form__cta-wrapper">
|
||||
<digi-button af-type="submit" af-size="m">Förhandsgranska</digi-button>
|
||||
<msfa-back-link [showIcon]="false" [asButton]="true" route="../">
|
||||
<span>Avbryt</span>
|
||||
<span class="msfa__a11y-sr-only"> och gå tillbaka till deltagaren</span>
|
||||
</msfa-back-link>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<digi-ng-dialog
|
||||
[afActive]="confirmDialogIsOpen$ | async"
|
||||
(afOnPrimaryClick)="submitAndCloseConfirmDialog(genomforandeReferens)"
|
||||
(afOnInactive)="cancelConfirmDialog()"
|
||||
afHeadingLevel="h2"
|
||||
afPrimaryButtonText="Skicka in"
|
||||
afSecondaryButtonText="Avbryt"
|
||||
(afOnSecondaryClick)="cancelConfirmDialog()"
|
||||
afHeading="Vill du skicka in Periodisk redovisning"
|
||||
afAriaLabel="Förhandsgranska och skicka in Periodisk redovisning"
|
||||
id="confirm-periodisk-redovisning-form"
|
||||
>
|
||||
<msfa-loader *ngIf="submitIsLoading$ | async" type="absolute"></msfa-loader>
|
||||
<dl>
|
||||
<dt>Namn</dt>
|
||||
<dd>{{avrop.fullName}}</dd>
|
||||
<dt>Personnummer</dt>
|
||||
<dd>
|
||||
<msfa-hide-text
|
||||
symbols="********-****"
|
||||
[changingText]="avrop.ssn"
|
||||
ariaLabelType="Personnummer"
|
||||
></msfa-hide-text>
|
||||
</dd>
|
||||
<dt>Tjänst</dt>
|
||||
<dd>{{avrop.tjanst}}</dd>
|
||||
<dt>Startdatum</dt>
|
||||
<dd>
|
||||
<digi-typography-time [afDateTime]="avrop.startDate"></digi-typography-time>
|
||||
</dd>
|
||||
<dt>Slutdatum</dt>
|
||||
<dd>
|
||||
<digi-typography-time [afDateTime]="avrop.endDate"></digi-typography-time>
|
||||
</dd>
|
||||
<ng-container *ngIf="submitData$ | async; let submitData; else loadingRef">
|
||||
<dt>Har ni, under perioden, tillhandahållit språkstöd:</dt>
|
||||
<dd>{{submitData.hasOfferedLanguageSupport ? 'Ja' : 'Nej' }}</dd>
|
||||
|
||||
<dt>Har ni, under perioden, erbjudit arbete:</dt>
|
||||
<dd>{{submitData.hasOfferedJob ? 'Ja' : 'Nej' }}</dd>
|
||||
|
||||
<dt>Aktiviteter som utförts under perioden:</dt>
|
||||
<ng-container *ngIf="submitData.activities.length === 0"
|
||||
>Deltagaren har inte deltagit i några aktiviteter denna period
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let activity of submitData.activities">
|
||||
<dd>
|
||||
{{getActivityMetadata(activity.id).name}}: {{ activity.performedRemotely &&
|
||||
activity.performedPhysically ? 'På distans och på plats' : activity.performedRemotely ? 'På distans' :
|
||||
'På plats'}}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</dl>
|
||||
<digi-notification-alert
|
||||
*ngIf="submitError$ | async as error"
|
||||
af-variation="danger"
|
||||
af-heading="Någonting gick fel"
|
||||
>
|
||||
<p>Kunde inte spara Periodisk redovisning. 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>
|
||||
</ng-template>
|
||||
</div>
|
||||
</msfa-report-layout>
|
||||
</msfa-layout>
|
||||
|
||||
<ng-template #skeletonRef>
|
||||
<digi-ng-skeleton-base
|
||||
[afCount]="3"
|
||||
afText="Laddar data för att kunna skapa Periodisk redovisning"
|
||||
></digi-ng-skeleton-base>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingRef>
|
||||
<msfa-loader type="padded"></msfa-loader>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,38 @@
|
||||
@import 'mixins/list';
|
||||
@import 'variables/gutters';
|
||||
|
||||
.periodisk-redovisning-form {
|
||||
max-width: var(--digi--typography--text--max-width);
|
||||
|
||||
&__confirmation,
|
||||
&__warning,
|
||||
&__form {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $digi--layout--gutter--l;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--digi--layout--gutter);
|
||||
}
|
||||
|
||||
&__cta-wrapper {
|
||||
display: flex;
|
||||
gap: var(--digi--layout--gutter);
|
||||
}
|
||||
|
||||
&__no-activities-has-been-conducted-checkbox {
|
||||
margin-bottom: $digi--layout--gutter--l;
|
||||
}
|
||||
|
||||
&__activity-checkboxes {
|
||||
margin-bottom: var(--digi--layout--gutter--s);
|
||||
}
|
||||
|
||||
&__activity-location-checkboxes {
|
||||
margin-left: var(--digi--layout--gutter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
|
||||
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
|
||||
import { PeriodiskRedovisningFormComponent } from './periodisk-redovisning-form.component';
|
||||
import { PeriodiskRedovisningFormService } from './periodisk-redovisning-form.service';
|
||||
|
||||
describe('PeriodiskRedovisningFormComponent', () => {
|
||||
let component: PeriodiskRedovisningFormComponent;
|
||||
let fixture: ComponentFixture<PeriodiskRedovisningFormComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [PeriodiskRedovisningFormComponent, LayoutComponent],
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
HttpClientTestingModule,
|
||||
DigiNgFormRadiobuttonGroupModule,
|
||||
DigiNgFormCheckboxModule,
|
||||
],
|
||||
providers: [PeriodiskRedovisningFormService],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PeriodiskRedovisningFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { Avrop } from '@msfa-models/avrop.model';
|
||||
import { AvropPeriod } from '@msfa-models/avrop-period.model';
|
||||
import { formatDate } from '@msfa-utils/format-to-date.util';
|
||||
import { subMonths } from 'date-fns';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
import { dateToPeriodId, extractAvropPeriods } from './extract-avrop-periods';
|
||||
import { PeriodiskRedovisningFormService } from './periodisk-redovisning-form.service';
|
||||
import { RadiobuttonGroupDirection } from '@af/digi-ng/_form/form-radiobutton-group';
|
||||
import { RequiredValidator } from '@msfa-validators/required.validator';
|
||||
import { Activity } from '@msfa-models/activity.model';
|
||||
import { GemensamPlaneringApiService } from '@msfa-services/api/gemensam-planering-api.service';
|
||||
import {
|
||||
PeriodiskRedovisningActivityRequest,
|
||||
PeriodiskRedovisningRequest,
|
||||
} from '@msfa-models/api/periodisk-redovisning.request.model';
|
||||
import { DateFormatOptions } from '@msfa-models/date-format-options.model';
|
||||
import {
|
||||
ActivityFormErrors,
|
||||
PeriodiskRedovisningFormData,
|
||||
PeriodiskRedovisningFormErrors,
|
||||
PeriodiskRedovisningFormKeys,
|
||||
} from './periodisk-redovisning-form.model';
|
||||
import { PeriodiskRedovisningValidator } from './periodisk-redovisning.validator';
|
||||
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
|
||||
@Component({
|
||||
selector: 'msfa-periodisk-redovisning-form',
|
||||
templateUrl: './periodisk-redovisning-form.component.html',
|
||||
styleUrls: ['./periodisk-redovisning-form.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PeriodiskRedovisningFormComponent extends UnsubscribeDirective implements OnInit {
|
||||
readonly PERIOD_FORM_NAME: PeriodiskRedovisningFormKeys = 'period';
|
||||
readonly HAS_OFFERED_LANGUAGE_SUPPORT_FORM_NAME: PeriodiskRedovisningFormKeys = 'hasOfferedLanguageSupport';
|
||||
readonly HAS_OFFERED_JOB_FORM_NAME: PeriodiskRedovisningFormKeys = 'hasOfferedJob';
|
||||
readonly ACTIVITES_FORM_NAME: PeriodiskRedovisningFormKeys = 'activities';
|
||||
readonly NO_ACTIVITIES_HAS_BEEN_CONDUCTED_FORM_NAME: PeriodiskRedovisningFormKeys = 'noActivitiesHasBeenConducted';
|
||||
|
||||
today = new Date();
|
||||
shouldValidate$ = new BehaviorSubject<boolean>(false);
|
||||
confirmDialogIsOpen$ = new BehaviorSubject<boolean>(false);
|
||||
submittedDate$ = new BehaviorSubject<Date>(null);
|
||||
submitIsLoading$ = new BehaviorSubject<boolean>(false);
|
||||
radiobuttonGroupDirection = RadiobuttonGroupDirection;
|
||||
activitiesFormArrayMetadata: Activity[];
|
||||
submitError$ = new BehaviorSubject<CustomError>(null);
|
||||
|
||||
genomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
|
||||
map((params: Params) => +params.genomforandeReferens)
|
||||
);
|
||||
avrop$: Observable<Avrop> = this.genomforandeReferens$.pipe(
|
||||
switchMap(genomforandeReferens =>
|
||||
this.periodiskRedovisningFormService.fetchAvropInformation$(genomforandeReferens)
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
availableActivities$: Observable<Activity[]> = this.gemensamPlaneringApiService.fetchActivities$();
|
||||
|
||||
periods$: Observable<AvropPeriod[]> = this.avrop$.pipe(
|
||||
map(avrop => extractAvropPeriods(avrop.startDate, avrop.endDate))
|
||||
);
|
||||
previousPeriod = dateToPeriodId(subMonths(new Date(), 1));
|
||||
formGroup = new FormGroup(
|
||||
{
|
||||
[this.PERIOD_FORM_NAME]: new FormControl(this.previousPeriod, RequiredValidator()),
|
||||
[this.HAS_OFFERED_LANGUAGE_SUPPORT_FORM_NAME]: new FormControl(null, RequiredValidator()),
|
||||
[this.HAS_OFFERED_JOB_FORM_NAME]: new FormControl(null, RequiredValidator()),
|
||||
[this.ACTIVITES_FORM_NAME]: new FormArray([]),
|
||||
[this.NO_ACTIVITIES_HAS_BEEN_CONDUCTED_FORM_NAME]: new FormControl(),
|
||||
},
|
||||
[PeriodiskRedovisningValidator.periodiskRedovisningIsValid()]
|
||||
);
|
||||
|
||||
formData$: Observable<PeriodiskRedovisningFormData> = this.formGroup
|
||||
.valueChanges as Observable<PeriodiskRedovisningFormData>;
|
||||
|
||||
submitData$ = combineLatest([this.genomforandeReferens$, this.formData$]).pipe(
|
||||
map(([genomforandeReferens, formData]) => this.formDataToSubmitData(genomforandeReferens, formData)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
periodsToFormselectItems(periods: AvropPeriod[]): FormSelectItem[] {
|
||||
return periods.map(period => ({
|
||||
name: `${formatDate(period.startDate, 'sv-SE', { month: 'long', year: 'numeric' } as DateFormatOptions)}`,
|
||||
value: period.periodId,
|
||||
}));
|
||||
}
|
||||
|
||||
get formErrors(): PeriodiskRedovisningFormErrors {
|
||||
return this.formGroup.errors as PeriodiskRedovisningFormErrors;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private periodiskRedovisningFormService: PeriodiskRedovisningFormService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
// TODO. GemensamPlaneringApiService is used for fetching activities. Replace with own service once ready
|
||||
private gemensamPlaneringApiService: GemensamPlaneringApiService
|
||||
) {
|
||||
super();
|
||||
|
||||
super.unsubscribeOnDestroy(
|
||||
this.formGroup.valueChanges.subscribe(value => {
|
||||
this.shouldValidate$.next(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get periodFormControl(): AbstractControl | undefined {
|
||||
return this.formGroup.get(this.PERIOD_FORM_NAME);
|
||||
}
|
||||
|
||||
get hasOfferedLanguageSupportFormControl(): AbstractControl | undefined {
|
||||
return this.formGroup.get(this.HAS_OFFERED_LANGUAGE_SUPPORT_FORM_NAME);
|
||||
}
|
||||
|
||||
get hasOfferedJobFormControl(): AbstractControl | undefined {
|
||||
return this.formGroup.get(this.HAS_OFFERED_JOB_FORM_NAME);
|
||||
}
|
||||
|
||||
get noActivitiesHasBeenConductedFormControl(): AbstractControl | undefined {
|
||||
return this.formGroup.get(this.NO_ACTIVITIES_HAS_BEEN_CONDUCTED_FORM_NAME);
|
||||
}
|
||||
|
||||
get activitiesFormArray(): FormArray {
|
||||
return this.formGroup.get(this.ACTIVITES_FORM_NAME) as FormArray;
|
||||
}
|
||||
|
||||
openConfirmDialog(): void {
|
||||
this.shouldValidate$.next(true);
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
this.confirmDialogIsOpen$.next(true);
|
||||
}
|
||||
|
||||
cancelConfirmDialog(): void {
|
||||
this.confirmDialogIsOpen$.next(false);
|
||||
this.submitError$.next(null);
|
||||
}
|
||||
|
||||
submitAndCloseConfirmDialog(genomforandeReferens: number): void {
|
||||
this.submitIsLoading$.next(true);
|
||||
this.submitData$.pipe(take(1)).subscribe(submitData =>
|
||||
this.periodiskRedovisningFormService.submitPeriodiskRedovisning$(submitData).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 };
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
formControlIsInvalid(formControl: AbstractControl): boolean {
|
||||
return formControl.invalid && (formControl.touched || this.shouldValidate$.value);
|
||||
}
|
||||
|
||||
private clearActivities(): void {
|
||||
this.activitiesFormArray.clear();
|
||||
this.activitiesFormArrayMetadata = [];
|
||||
}
|
||||
|
||||
private addActivityToForm(activity: Activity): void {
|
||||
// FormArray doesnt hold any IDs so we need to store these seperately and rebuild structure at submit
|
||||
// It's important that the metadata is updated at the same time as the formArray to avoid sync problems
|
||||
this.activitiesFormArrayMetadata.push(activity);
|
||||
|
||||
this.activitiesFormArray.push(
|
||||
new FormGroup(
|
||||
{
|
||||
isSelected: new FormControl(),
|
||||
performedRemotely: new FormControl(),
|
||||
performedPhysically: new FormControl(),
|
||||
},
|
||||
PeriodiskRedovisningValidator.activityIsValid()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.availableActivities$.subscribe(activities => {
|
||||
this.clearActivities();
|
||||
activities.forEach(activity => this.addActivityToForm(activity));
|
||||
});
|
||||
}
|
||||
|
||||
private formDataToSubmitData(
|
||||
genomforandeReferens: number,
|
||||
formData: PeriodiskRedovisningFormData
|
||||
): PeriodiskRedovisningRequest {
|
||||
const { period, hasOfferedJob, hasOfferedLanguageSupport } = formData;
|
||||
const activities: PeriodiskRedovisningActivityRequest[] = formData.activities
|
||||
.filter(activity => activity.isSelected)
|
||||
.map(({ performedPhysically, performedRemotely }, index) => ({
|
||||
id: this.activitiesFormArrayMetadata[index].id,
|
||||
performedPhysically: performedPhysically ?? false,
|
||||
performedRemotely: performedRemotely ?? false,
|
||||
}));
|
||||
return {
|
||||
genomforandeReferens,
|
||||
period,
|
||||
hasOfferedJob,
|
||||
hasOfferedLanguageSupport,
|
||||
activities,
|
||||
};
|
||||
}
|
||||
|
||||
errorsToArray(errors: { [key: string]: string }): string[] {
|
||||
return Object.values(errors);
|
||||
}
|
||||
|
||||
activityLocationIsInvalid(activityFormGroup: AbstractControl): boolean {
|
||||
const errors = activityFormGroup.errors as ActivityFormErrors;
|
||||
return this.formControlIsInvalid(activityFormGroup) && !!errors.locationCheckboxes;
|
||||
}
|
||||
|
||||
getActivityMetadata(activityId: number) {
|
||||
return this.activitiesFormArrayMetadata.find(activity => activity.id === activityId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface PeriodiskRedovisningFormActivity {
|
||||
isSelected: boolean;
|
||||
performedRemotely: boolean;
|
||||
performedPhysically: boolean;
|
||||
}
|
||||
|
||||
export interface PeriodiskRedovisningFormData {
|
||||
period: string;
|
||||
hasOfferedLanguageSupport: boolean;
|
||||
hasOfferedJob: boolean;
|
||||
noActivitiesHasBeenConducted: boolean;
|
||||
activities: PeriodiskRedovisningFormActivity[];
|
||||
}
|
||||
|
||||
export type PeriodiskRedovisningFormKeys = keyof PeriodiskRedovisningFormData;
|
||||
|
||||
export interface PeriodiskRedovisningFormErrors {
|
||||
activitiesMismatch?: string;
|
||||
}
|
||||
|
||||
export interface ActivityFormErrors {
|
||||
locationCheckboxes?: string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
|
||||
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
|
||||
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
|
||||
import { LoaderModule } from '@msfa-shared/components/loader/loader.module';
|
||||
import { ReportLayoutModule } from '../../../components/report-layout/report-layout.module';
|
||||
import { PeriodiskRedovisningFormComponent } from './periodisk-redovisning-form.component';
|
||||
import { PeriodiskRedovisningFormService } from './periodisk-redovisning-form.service';
|
||||
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
|
||||
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
|
||||
import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
|
||||
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
|
||||
|
||||
@NgModule({
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
declarations: [PeriodiskRedovisningFormComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild([{ path: '', component: PeriodiskRedovisningFormComponent }]),
|
||||
LayoutModule,
|
||||
ReactiveFormsModule,
|
||||
ReportLayoutModule,
|
||||
BackLinkModule,
|
||||
LoaderModule,
|
||||
DigiNgSkeletonBaseModule,
|
||||
DigiNgDialogModule,
|
||||
DigiNgFormSelectModule,
|
||||
DigiNgFormRadiobuttonGroupModule,
|
||||
DigiNgFormCheckboxModule,
|
||||
HideTextModule,
|
||||
],
|
||||
providers: [PeriodiskRedovisningFormService],
|
||||
exports: [PeriodiskRedovisningFormComponent],
|
||||
})
|
||||
export class PeriodiskRedovisningFormModule {}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Avrop } from '@msfa-models/avrop.model';
|
||||
import { PeriodiskRedovisning } from '@msfa-models/periodisk-redovisning.model';
|
||||
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
|
||||
import { PeriodiskRedovisningApiService } from '@msfa-services/api/periodisk-redovisning.api.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PeriodiskRedovisningRequest } from '@msfa-models/api/periodisk-redovisning.request.model';
|
||||
|
||||
@Injectable()
|
||||
export class PeriodiskRedovisningFormService {
|
||||
constructor(
|
||||
private periodiskRedovisningApiService: PeriodiskRedovisningApiService,
|
||||
private deltagareApiService: DeltagareApiService
|
||||
) {}
|
||||
|
||||
fetchPeriodiskRedovisning$(
|
||||
periodStart: string,
|
||||
periodEnd: string,
|
||||
genomforandeReferens: number
|
||||
): Observable<PeriodiskRedovisning> {
|
||||
return this.periodiskRedovisningApiService.fetchPeriodiskRedovisning$(periodStart, periodEnd, genomforandeReferens);
|
||||
}
|
||||
|
||||
fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
|
||||
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
|
||||
}
|
||||
|
||||
submitPeriodiskRedovisning$(submitData: PeriodiskRedovisningRequest): Observable<void> {
|
||||
return this.periodiskRedovisningApiService.postPeriodiskRedovisning$(submitData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
||||
import {
|
||||
ActivityFormErrors,
|
||||
PeriodiskRedovisningFormActivity,
|
||||
PeriodiskRedovisningFormData,
|
||||
PeriodiskRedovisningFormErrors,
|
||||
} from './periodisk-redovisning-form.model';
|
||||
|
||||
export class PeriodiskRedovisningValidator {
|
||||
static periodiskRedovisningIsValid(): ValidatorFn {
|
||||
return (c: AbstractControl): PeriodiskRedovisningFormErrors => {
|
||||
let errors: PeriodiskRedovisningFormErrors;
|
||||
|
||||
const { noActivitiesHasBeenConducted, activities } = c.value as PeriodiskRedovisningFormData;
|
||||
if (noActivitiesHasBeenConducted && activities.filter(activity => activity.isSelected).length > 0) {
|
||||
errors = {
|
||||
...errors,
|
||||
activitiesMismatch:
|
||||
'Ifall kryssrutan "Deltagaren har inte deltagit i några aktiviteter denna period" är förbockad får inga aktiviteter vara valda. ',
|
||||
};
|
||||
} else if (
|
||||
noActivitiesHasBeenConducted !== true &&
|
||||
activities.filter(activity => activity.isSelected).length === 0
|
||||
) {
|
||||
errors = {
|
||||
...errors,
|
||||
activitiesMismatch:
|
||||
'Ifall inga aktiviteter är valda ska "Deltagaren har inte deltagit i några aktiviteter denna period" vara förbockad. ',
|
||||
};
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
}
|
||||
|
||||
static activityIsValid(): ValidatorFn {
|
||||
return (c: AbstractControl): ActivityFormErrors => {
|
||||
let errors: ActivityFormErrors;
|
||||
|
||||
const { performedRemotely, performedPhysically, isSelected } = c.value as PeriodiskRedovisningFormActivity;
|
||||
if (isSelected && !performedPhysically && !performedRemotely) {
|
||||
errors = {
|
||||
...errors,
|
||||
locationCheckboxes: 'Minst en plats måste väljas',
|
||||
};
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { SignalRequest } from '@msfa-models/api/signal.request.model';
|
||||
import { Avrop } from '@msfa-models/avrop.model';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { Signal } from '@msfa-models/signal.model';
|
||||
import { dateToIsoString } from '@msfa-utils/format-to-date.util';
|
||||
import { formatDate } from '@msfa-utils/format-to-date.util';
|
||||
import { add } from 'date-fns';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
|
||||
@@ -125,7 +125,7 @@ export class SignalFormComponent {
|
||||
typ: type,
|
||||
omfattning,
|
||||
omfattning_procent: omfattning === 'deltid' ? percent : null,
|
||||
startdatum: dateToIsoString(startDate),
|
||||
startdatum: formatDate(startDate),
|
||||
};
|
||||
|
||||
this.signalFormService
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
export enum Feature {
|
||||
AVROP,
|
||||
DELTAGARE,
|
||||
ADMINISTRATION,
|
||||
MY_ACCOUNT,
|
||||
MY_ORGANIZATION,
|
||||
LOGGING,
|
||||
RELEASES,
|
||||
MOCK_LOGIN,
|
||||
VERSION_INFO,
|
||||
|
||||
AVROP,
|
||||
DELTAGARE,
|
||||
DELTAGARE_SENSITIVE_INFORMATION,
|
||||
ADMINISTRATION,
|
||||
MY_ACCOUNT,
|
||||
MY_ORGANIZATION,
|
||||
ACCESSIBILITY_REPORT,
|
||||
REPORTING,
|
||||
SENSITIVE_INFORMATION,
|
||||
LOGGING,
|
||||
REPORTING_SIGNAL,
|
||||
REPORTING_PERIODISK_REDOVISNING,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface PeriodiskRedovisningActivityRequest {
|
||||
id: number;
|
||||
performedRemotely: boolean;
|
||||
performedPhysically: boolean;
|
||||
}
|
||||
|
||||
export interface PeriodiskRedovisningRequest {
|
||||
genomforandeReferens: number;
|
||||
period: string;
|
||||
hasOfferedLanguageSupport: boolean;
|
||||
hasOfferedJob: boolean;
|
||||
activities: PeriodiskRedovisningActivityRequest[];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export interface PeriodiskRedovisningActivityResponse {
|
||||
activityId: number;
|
||||
activityName: string;
|
||||
performedRemotely: boolean;
|
||||
}
|
||||
|
||||
export interface PeriodiskRedovisningResponse {
|
||||
genomforandeReferens: number;
|
||||
period: string;
|
||||
hasOfferedLanguageSupport: boolean;
|
||||
hasOfferedJob: boolean;
|
||||
activities: PeriodiskRedovisningActivityResponse[];
|
||||
}
|
||||
|
||||
export function mockOnePeriodiskRedovisningResponse(): PeriodiskRedovisningResponse {
|
||||
return {
|
||||
genomforandeReferens: 100003857,
|
||||
hasOfferedJob: false,
|
||||
hasOfferedLanguageSupport: true,
|
||||
period: '2021-10',
|
||||
activities: [
|
||||
{
|
||||
activityId: 24,
|
||||
activityName: 'Aktivitet 1',
|
||||
performedRemotely: false,
|
||||
},
|
||||
{
|
||||
activityId: 19,
|
||||
activityName: 'Aktivitet 5',
|
||||
performedRemotely: true,
|
||||
},
|
||||
{
|
||||
activityId: 31,
|
||||
activityName: 'Aktivitet 5',
|
||||
performedRemotely: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface AvropPeriod {
|
||||
periodId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PeriodiskRedovisningResponse } from './api/periodisk-redovisning.response.model';
|
||||
|
||||
export interface PeriodiskRedovisningActivity {
|
||||
activityId: number;
|
||||
activityName: string;
|
||||
performedRemotely: boolean;
|
||||
}
|
||||
|
||||
export interface PeriodiskRedovisning {
|
||||
genomforandeReferens: number;
|
||||
period: string;
|
||||
hasOfferedLanguageSupport: boolean;
|
||||
hasOfferedJob: boolean;
|
||||
activities: PeriodiskRedovisningActivity[];
|
||||
}
|
||||
|
||||
export function mapResponseToPeriodiskRedovisning(data: PeriodiskRedovisningResponse): PeriodiskRedovisning {
|
||||
const { genomforandeReferens, period, hasOfferedLanguageSupport, hasOfferedJob, activities } = data;
|
||||
|
||||
return {
|
||||
genomforandeReferens,
|
||||
period,
|
||||
hasOfferedJob,
|
||||
hasOfferedLanguageSupport,
|
||||
activities,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ErrorType } from '@msfa-enums/error-type.enum';
|
||||
import { environment } from '@msfa-environment';
|
||||
import { Params } from '@msfa-models/api/params.model';
|
||||
import { PeriodiskRedovisningRequest } from '@msfa-models/api/periodisk-redovisning.request.model';
|
||||
import {
|
||||
mockOnePeriodiskRedovisningResponse,
|
||||
PeriodiskRedovisningResponse,
|
||||
} from '@msfa-models/api/periodisk-redovisning.response.model';
|
||||
import { CustomError } from '@msfa-models/error/custom-error';
|
||||
import { mapResponseToPeriodiskRedovisning, PeriodiskRedovisning } from '@msfa-models/periodisk-redovisning.model';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PeriodiskRedovisningApiService {
|
||||
private _apiBaseUrl = `${environment.api.url}/rapporter/periodisk-redovisining`;
|
||||
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
public fetchPeriodiskRedovisning$(
|
||||
periodStart: string,
|
||||
periodEnd: string,
|
||||
genomforandeReferens: number
|
||||
): Observable<PeriodiskRedovisning> {
|
||||
const params: Params = {
|
||||
genomforandeReferens: genomforandeReferens.toString(),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
};
|
||||
return of(mapResponseToPeriodiskRedovisning(mockOnePeriodiskRedovisningResponse()));
|
||||
// return this.httpClient
|
||||
// .get<{ data: PeriodiskRedovisningResponse }>(`${this._apiBaseUrl}`, { params })
|
||||
// .pipe(map(({ data }) => (data ? mapResponseToPeriodiskRedovisning(data) : null)));
|
||||
}
|
||||
|
||||
public fetchAllPeriodiskaRedovisningar$(genomforandeReferens: number): Observable<PeriodiskRedovisning[]> {
|
||||
return this.httpClient
|
||||
.get<{ data: PeriodiskRedovisningResponse[] }>(`${this._apiBaseUrl}/${genomforandeReferens}`)
|
||||
.pipe(map(({ data }) => data.map(pr => mapResponseToPeriodiskRedovisning(pr))));
|
||||
}
|
||||
|
||||
public postPeriodiskRedovisning$(requestData: PeriodiskRedovisningRequest): Observable<void> {
|
||||
return this.httpClient.post<void>(`${this._apiBaseUrl}`, requestData).pipe(
|
||||
catchError((error: Error) => {
|
||||
throw new CustomError({
|
||||
error,
|
||||
message: `Kunde inte spara Periodisk redovisning.\n\n${error.message}`,
|
||||
type: ErrorType.API,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
import { DateFormatOptions } from '@msfa-models/date-format-options.model';
|
||||
|
||||
// Takes either 6 or 8 characters string (YYYYMMDD) and formats it to ISO standard (YYYY-MM-DD).
|
||||
export function formatToIsoString(date: string): string {
|
||||
if (date.length === 6) {
|
||||
return `${date.substring(0, 4)}-${date.substring(4)}`;
|
||||
} else if (date.length === 8) {
|
||||
return `${date.substring(0, 4)}-${date.substring(4, 6)}-${date.substring(6)}`;
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
export function formatToDate(date: string): Date {
|
||||
const year = date.substring(0, 4);
|
||||
const month = date.substring(4, 6) || '01';
|
||||
@@ -19,8 +8,8 @@ export function formatToDate(date: string): Date {
|
||||
return new Date(`${year}-${month}-${day}`);
|
||||
}
|
||||
|
||||
export function dateToIsoString(date: Date, locale: string = 'sv-SE'): string {
|
||||
const formatOptions: DateFormatOptions = {
|
||||
export function formatDate(date: Date, locale: string = 'sv-SE', options?: DateFormatOptions): string {
|
||||
const formatOptions: DateFormatOptions = options || {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -4,7 +4,11 @@ import { ValidationError } from '@msfa-models/validation-error.model';
|
||||
export function RequiredValidator(message = 'Fältet är obligatoriskt'): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationError => {
|
||||
if (control) {
|
||||
if (!control.value || (Array.isArray(control.value) && !control.value.length)) {
|
||||
if (
|
||||
control.value === null ||
|
||||
control.value === undefined ||
|
||||
(Array.isArray(control.value) && !control.value.length)
|
||||
) {
|
||||
return { required: message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ export const ACTIVE_FEATURES_TEST: Feature[] = [
|
||||
Feature.AVROP,
|
||||
Feature.REPORTING,
|
||||
Feature.LOGGING,
|
||||
Feature.SENSITIVE_INFORMATION,
|
||||
Feature.DELTAGARE_SENSITIVE_INFORMATION,
|
||||
Feature.RELEASES,
|
||||
Feature.VERSION_INFO,
|
||||
Feature.REPORTING_PERIODISK_REDOVISNING,
|
||||
];
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "mina-sidor-fa-web",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "2.1.0",
|
||||
"name": "mina-sidor-fa-web",
|
||||
"version": "2.2.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -23742,6 +23743,11 @@
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"sshpk-conv": "bin/sshpk-conv",
|
||||
"sshpk-sign": "bin/sshpk-sign",
|
||||
"sshpk-verify": "bin/sshpk-verify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -40478,7 +40484,9 @@
|
||||
"resolved": "http://nexus.arbetsformedlingen.se/repository/npm/ajv-formats/-/ajv-formats-2.1.0.tgz",
|
||||
"integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==",
|
||||
"peer": true,
|
||||
"requires": {}
|
||||
"requires": {
|
||||
"ajv": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"alphanum-sort": {
|
||||
"version": "1.0.2",
|
||||
|
||||
Reference in New Issue
Block a user