From abf2b154075f7ceb38e6787c15ea9d1285c2c975 Mon Sep 17 00:00:00 2001 From: Daniel Appelgren Date: Wed, 20 Oct 2021 14:46:45 +0200 Subject: [PATCH] =?UTF-8?q?feature(Periodisk=20redovisning):=20Formul?= =?UTF-8?q?=C3=A4r=20f=C3=B6r=20periodisk=20redovisning=20(TV-771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit eee14a464fe2fe2a99074f0fe92eecfc92cd05fa Author: Daniel Appelgren Date: Wed Oct 20 14:09:48 2021 +0200 styling commit b95bac31ac2b33b5c383a32f06ababf3e5f00245 Author: Daniel Appelgren Date: Wed Oct 20 13:35:03 2021 +0200 Update periodisk-redovisning.validator.ts commit aeda04cd6705e72b5621a3079904617322ce3036 Author: Daniel Appelgren 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 Date: Wed Oct 20 11:12:57 2021 +0200 form validation and dialog done commit 93e5345d13caf5ab25dc581d58efe92f85acb2dd Author: Daniel Appelgren Date: Wed Oct 20 09:16:03 2021 +0200 hidden checkboxes commit 68c2f17ec8417ce5a0404d5b0c00e4800b738143 Author: Daniel Appelgren Date: Wed Oct 20 08:19:01 2021 +0200 Update app.module.ts commit 2a1dfa6559b9b86839de8ddd1d8cd7c821a56b3a Author: Daniel Appelgren Date: Tue Oct 19 21:56:22 2021 +0200 form array with checkboxes done commit 32f26800656d13d1c6c30b20c8187b20fda3c71c Author: Daniel Appelgren Date: Tue Oct 19 17:04:33 2021 +0200 activity form array commit db2974cfcca453390ebb4f637daf9d9064b527da Author: Daniel Appelgren Date: Tue Oct 19 15:56:39 2021 +0200 add radiobuttons commit 2c4099b48337aaad1cb5b0cc4794ee94e6bb508c Merge: 1ae24a90 25b12092 Author: Daniel Appelgren 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 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 Date: Tue Oct 19 10:14:18 2021 +0200 Updated periods commit 794bbc9a71a0e638196d961ed8b3093de5a64e49 Author: Daniel Appelgren Date: Tue Oct 19 09:30:24 2021 +0200 Update package-lock.json commit 56351afb1f92060b9f743233a69a785114a3ee96 Author: Daniel Appelgren Date: Mon Oct 18 17:03:19 2021 +0200 Update periodisk-redovisning-form.component.ts commit 213e6c888a8e388381cf4370d2f5020987b29c4f Author: Daniel Appelgren Date: Mon Oct 18 17:01:17 2021 +0200 Update extract-avrop-periods.ts commit 4bcd9669b70070654111f650e6b20d8d8981b3a1 Author: Daniel Appelgren Date: Mon Oct 18 17:00:47 2021 +0200 avrop periods commit cee788517c34107a2f651313038c343bb4fc702e Author: Daniel Appelgren Date: Mon Oct 18 15:46:48 2021 +0200 clean up commit 3d1d2414270a0de1111ba8b16194dc82ec5bbe79 Merge: b6304eed 9104fc31 Author: Daniel Appelgren Date: Mon Oct 18 15:37:08 2021 +0200 Merge branch 'develop' into feature/periodisk-redovisning commit b6304eedf683ba9679e38628d9d3cc33c07103a7 Author: Erik Tiekstra 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 Date: Fri Oct 15 14:03:25 2021 +0200 Merge branch 'develop' into feature/periodisk-redovisning ... and 3 more commits --- apps/mina-sidor-fa/src/app/app.module.ts | 31 ++- .../deltagare-details.module.ts | 41 ++- .../deltagare-tab-reports.component.html | 9 +- .../deltagare-tab-reports.component.ts | 11 + .../deltagare-card.component.ts | 4 +- .../avvikelse-report-form.component.html | 3 +- .../avvikelse-report-form.component.ts | 10 +- .../franvaro-report-form.component.html | 2 +- .../franvaro-report-form.component.ts | 10 +- .../extract-avrop-periods.spec.ts | 56 +++++ .../extract-avrop-periods.ts | 30 +++ .../periodisk-redovisning-form.component.html | 219 ++++++++++++++++ .../periodisk-redovisning-form.component.scss | 38 +++ ...riodisk-redovisning-form.component.spec.ts | 38 +++ .../periodisk-redovisning-form.component.ts | 233 ++++++++++++++++++ .../periodisk-redovisning-form.model.ts | 23 ++ .../periodisk-redovisning-form.module.ts | 39 +++ .../periodisk-redovisning-form.service.ts | 31 +++ .../periodisk-redovisning.validator.ts | 51 ++++ .../signal-form/signal-form.component.ts | 4 +- .../src/app/shared/enums/feature.enum.ts | 17 +- .../periodisk-redovisning.request.model.ts | 13 + .../periodisk-redovisning.response.model.ts | 39 +++ .../app/shared/models/avrop-period.model.ts | 5 + .../models/periodisk-redovisning.model.ts | 27 ++ .../api/periodisk-redovisning.api.service.ts | 57 +++++ .../app/shared/utils/format-to-date.util.ts | 15 +- .../utils/validators/required.validator.ts | 6 +- .../src/environments/active-features.ts | 3 +- package-lock.json | 14 +- 30 files changed, 1009 insertions(+), 70 deletions(-) create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.spec.ts create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.ts create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.html create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.scss create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.spec.ts create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.ts create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.model.ts create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.module.ts create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.service.ts create mode 100644 apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning.validator.ts create mode 100644 apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.request.model.ts create mode 100644 apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.response.model.ts create mode 100644 apps/mina-sidor-fa/src/app/shared/models/avrop-period.model.ts create mode 100644 apps/mina-sidor-fa/src/app/shared/models/periodisk-redovisning.model.ts create mode 100644 apps/mina-sidor-fa/src/app/shared/services/api/periodisk-redovisning.api.service.ts diff --git a/apps/mina-sidor-fa/src/app/app.module.ts b/apps/mina-sidor-fa/src/app/app.module.ts index 606a076..eebefba 100644 --- a/apps/mina-sidor-fa/src/app/app.module.ts +++ b/apps/mina-sidor-fa/src/app/app.module.ts @@ -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 {} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/deltagare-details.module.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/deltagare-details.module.ts index 217e0d2..ca2445e 100644 --- a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/deltagare-details.module.ts +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/deltagare-details.module.ts @@ -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)], }) diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/components/deltagare-tab-reports/deltagare-tab-reports.component.html b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/components/deltagare-tab-reports/deltagare-tab-reports.component.html index 62baef3..0d646d5 100644 --- a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/components/deltagare-tab-reports/deltagare-tab-reports.component.html +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/components/deltagare-tab-reports/deltagare-tab-reports.component.html @@ -24,7 +24,14 @@ afText="Skapa ny Avvikelserapport (avvikelse)" > -
  • +
  • + +
  • +
  • (); + _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); } diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/deltagare-card.component.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/deltagare-card.component.ts index 8627b87..6a96a07 100644 --- a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/deltagare-card.component.ts +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/deltagare-card.component.ts @@ -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) ); } diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.html b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.html index a78a58d..b074dbd 100644 --- a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.html +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.html @@ -129,8 +129,7 @@ diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.ts index c19343e..6c68f32 100644 --- a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.ts +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/avvikelse-report-form/avvikelse-report-form.component.ts @@ -40,7 +40,7 @@ export class AvvikelseReportFormComponent implements OnInit, OnDestroy { reportingDateFormName: AvvikelseFormKeys = 'reportingDate'; submitIsLoading$ = new BehaviorSubject(false); - error$ = new BehaviorSubject(null); + submitError$ = new BehaviorSubject(null); genomforandeReferens$: Observable = 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 { diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/franvaro-report-form/franvaro-report-form.component.html b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/franvaro-report-form/franvaro-report-form.component.html index 92d2ff6..ca2cb5a 100644 --- a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/franvaro-report-form/franvaro-report-form.component.html +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/franvaro-report-form/franvaro-report-form.component.html @@ -318,7 +318,7 @@
    {{expectedPresenceStartTimeFormControl.value}} - {{expectedPresenceEndTimeFormControl.value}}
    (null); + submitError$ = new BehaviorSubject(null); submitLoading$ = new BehaviorSubject(false); lastSubmittedFranvaroReport$ = new BehaviorSubject(null); currentGenomforandeReferens$: Observable = 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 }; }, diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.spec.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.spec.ts new file mode 100644 index 0000000..397b984 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.spec.ts @@ -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']); + }); +}); diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.ts new file mode 100644 index 0000000..4b176b7 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/extract-avrop-periods.ts @@ -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; +} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.html b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.html new file mode 100644 index 0000000..1b7bd26 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.html @@ -0,0 +1,219 @@ + + +
    +
    + +

    Periodisk redovisning för deltagare {{avrop.fullName}} är nu inskickad till Arbetsförmedlingen.

    +
    +
    Datum
    +
    {{submittedDate | date:'longDate'}} kl {{submittedDate | date:'shortTime'}}
    +
    +
    + Tillbaka till deltagaren +
    + +
    + + +
    + + + + + Ett val är obligatoriskt + +
    + +
    + + + + + Ett val är obligatoriskt + +
    + +
    + + + + {{formErrors?.activitiesMismatch}} + + +
    +
    +
    + +
    + + + + + {{errorText}} + + +
    +
    +
    +
    + +
    +
    + Förhandsgranska + + Avbryt +  och gå tillbaka till deltagaren + +
    +
    +
    + + + +
    +
    Namn
    +
    {{avrop.fullName}}
    +
    Personnummer
    +
    + +
    +
    Tjänst
    +
    {{avrop.tjanst}}
    +
    Startdatum
    +
    + +
    +
    Slutdatum
    +
    + +
    + +
    Har ni, under perioden, tillhandahållit språkstöd:
    +
    {{submitData.hasOfferedLanguageSupport ? 'Ja' : 'Nej' }}
    + +
    Har ni, under perioden, erbjudit arbete:
    +
    {{submitData.hasOfferedJob ? 'Ja' : 'Nej' }}
    + +
    Aktiviteter som utförts under perioden:
    + Deltagaren har inte deltagit i några aktiviteter denna period + + +
    + {{getActivityMetadata(activity.id).name}}: {{ activity.performedRemotely && + activity.performedPhysically ? 'På distans och på plats' : activity.performedRemotely ? 'På distans' : + 'På plats'}} +
    +
    +
    +
    + +

    Kunde inte spara Periodisk redovisning. Ladda om sidan och försök igen.

    +

    {{error.message}}

    +
    +
    +
    +
    +
    +
    + + + + + + + + diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.scss b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.scss new file mode 100644 index 0000000..48d87e1 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.scss @@ -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); + } +} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.spec.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.spec.ts new file mode 100644 index 0000000..4050658 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.ts new file mode 100644 index 0000000..18f475d --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.component.ts @@ -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(false); + confirmDialogIsOpen$ = new BehaviorSubject(false); + submittedDate$ = new BehaviorSubject(null); + submitIsLoading$ = new BehaviorSubject(false); + radiobuttonGroupDirection = RadiobuttonGroupDirection; + activitiesFormArrayMetadata: Activity[]; + submitError$ = new BehaviorSubject(null); + + genomforandeReferens$: Observable = this.activatedRoute.params.pipe( + map((params: Params) => +params.genomforandeReferens) + ); + avrop$: Observable = this.genomforandeReferens$.pipe( + switchMap(genomforandeReferens => + this.periodiskRedovisningFormService.fetchAvropInformation$(genomforandeReferens) + ), + shareReplay(1) + ); + + availableActivities$: Observable = this.gemensamPlaneringApiService.fetchActivities$(); + + periods$: Observable = 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 = this.formGroup + .valueChanges as Observable; + + 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); + } +} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.model.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.model.ts new file mode 100644 index 0000000..167f115 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.model.ts @@ -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; +} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.module.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.module.ts new file mode 100644 index 0000000..94c4760 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.module.ts @@ -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 {} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.service.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.service.ts new file mode 100644 index 0000000..b2bc273 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning-form.service.ts @@ -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 { + return this.periodiskRedovisningApiService.fetchPeriodiskRedovisning$(periodStart, periodEnd, genomforandeReferens); + } + + fetchAvropInformation$(genomforandeReferens: number): Observable { + return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens); + } + + submitPeriodiskRedovisning$(submitData: PeriodiskRedovisningRequest): Observable { + return this.periodiskRedovisningApiService.postPeriodiskRedovisning$(submitData); + } +} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning.validator.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning.validator.ts new file mode 100644 index 0000000..570ac94 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/periodisk-redovisning-form/periodisk-redovisning.validator.ts @@ -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; + }; + } +} diff --git a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/signal-form/signal-form.component.ts b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/signal-form/signal-form.component.ts index 3a5ecfe..101bdef 100644 --- a/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/signal-form/signal-form.component.ts +++ b/apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/report-forms/signal-form/signal-form.component.ts @@ -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 diff --git a/apps/mina-sidor-fa/src/app/shared/enums/feature.enum.ts b/apps/mina-sidor-fa/src/app/shared/enums/feature.enum.ts index 6c3093f..0cf775f 100644 --- a/apps/mina-sidor-fa/src/app/shared/enums/feature.enum.ts +++ b/apps/mina-sidor-fa/src/app/shared/enums/feature.enum.ts @@ -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, } diff --git a/apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.request.model.ts b/apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.request.model.ts new file mode 100644 index 0000000..1cda0f3 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.request.model.ts @@ -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[]; +} diff --git a/apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.response.model.ts b/apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.response.model.ts new file mode 100644 index 0000000..0807691 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/shared/models/api/periodisk-redovisning.response.model.ts @@ -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, + }, + ], + }; +} diff --git a/apps/mina-sidor-fa/src/app/shared/models/avrop-period.model.ts b/apps/mina-sidor-fa/src/app/shared/models/avrop-period.model.ts new file mode 100644 index 0000000..a5d7713 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/shared/models/avrop-period.model.ts @@ -0,0 +1,5 @@ +export interface AvropPeriod { + periodId: string; + startDate: Date; + endDate: Date; +} diff --git a/apps/mina-sidor-fa/src/app/shared/models/periodisk-redovisning.model.ts b/apps/mina-sidor-fa/src/app/shared/models/periodisk-redovisning.model.ts new file mode 100644 index 0000000..d0a18b2 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/shared/models/periodisk-redovisning.model.ts @@ -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, + }; +} diff --git a/apps/mina-sidor-fa/src/app/shared/services/api/periodisk-redovisning.api.service.ts b/apps/mina-sidor-fa/src/app/shared/services/api/periodisk-redovisning.api.service.ts new file mode 100644 index 0000000..22e2a14 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/shared/services/api/periodisk-redovisning.api.service.ts @@ -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 { + 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 { + return this.httpClient + .get<{ data: PeriodiskRedovisningResponse[] }>(`${this._apiBaseUrl}/${genomforandeReferens}`) + .pipe(map(({ data }) => data.map(pr => mapResponseToPeriodiskRedovisning(pr)))); + } + + public postPeriodiskRedovisning$(requestData: PeriodiskRedovisningRequest): Observable { + return this.httpClient.post(`${this._apiBaseUrl}`, requestData).pipe( + catchError((error: Error) => { + throw new CustomError({ + error, + message: `Kunde inte spara Periodisk redovisning.\n\n${error.message}`, + type: ErrorType.API, + }); + }) + ); + } +} diff --git a/apps/mina-sidor-fa/src/app/shared/utils/format-to-date.util.ts b/apps/mina-sidor-fa/src/app/shared/utils/format-to-date.util.ts index c5c97c1..705b1cf 100644 --- a/apps/mina-sidor-fa/src/app/shared/utils/format-to-date.util.ts +++ b/apps/mina-sidor-fa/src/app/shared/utils/format-to-date.util.ts @@ -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', diff --git a/apps/mina-sidor-fa/src/app/shared/utils/validators/required.validator.ts b/apps/mina-sidor-fa/src/app/shared/utils/validators/required.validator.ts index 995711f..5983d82 100644 --- a/apps/mina-sidor-fa/src/app/shared/utils/validators/required.validator.ts +++ b/apps/mina-sidor-fa/src/app/shared/utils/validators/required.validator.ts @@ -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 }; } } diff --git a/apps/mina-sidor-fa/src/environments/active-features.ts b/apps/mina-sidor-fa/src/environments/active-features.ts index 18c9646..597bee8 100644 --- a/apps/mina-sidor-fa/src/environments/active-features.ts +++ b/apps/mina-sidor-fa/src/environments/active-features.ts @@ -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, ]; diff --git a/package-lock.json b/package-lock.json index adf5465..76a614a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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",