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: 1ae24a90 25b12092
Author: 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: b6304eed 9104fc31
Author: 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: cb219841 5d2f63b9
Author: 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:
Daniel Appelgren
2021-10-20 14:46:45 +02:00
parent 6c88067bbc
commit abf2b15407
30 changed files with 1009 additions and 70 deletions

View File

@@ -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 {}

View File

@@ -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)],
})

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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)
);
}

View File

@@ -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"
>

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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 };
},

View File

@@ -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']);
});
});

View File

@@ -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;
}

View File

@@ -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">&nbsp;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>

View File

@@ -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);
}
}

View File

@@ -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();
});
});

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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;
};
}
}

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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[];
}

View File

@@ -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,
},
],
};
}

View File

@@ -0,0 +1,5 @@
export interface AvropPeriod {
periodId: string;
startDate: Date;
endDate: Date;
}

View File

@@ -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,
};
}

View File

@@ -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,
});
})
);
}
}

View File

@@ -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',

View File

@@ -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 };
}
}

View File

@@ -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
View File

@@ -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",