feat(gemensam-planering): Implemented gemensam-planering form. (TV-700)

Squashed commit of the following:

commit 2d07f37e30009c7f701af35aed65839535044bb3
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Oct 6 09:33:21 2021 +0200

    Updated error handling

commit 12290b9436a06ecf0b2b8509016b14748ca17a18
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Oct 6 09:10:48 2021 +0200

    Updated after PR

commit cc2fb38528069819acbc39c7b1f6d71ecae666a1
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Oct 6 08:46:33 2021 +0200

    Updated proxy.conf

commit ee919de929d7b7316cd7050015fbad9c662b8718
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Oct 6 08:44:00 2021 +0200

    Updated api-endpoint

commit 249ef70e14fa8db0c388ffb27f5173815a07c768
Merge: c8296cbf cc0a9aae
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Oct 6 08:39:43 2021 +0200

    Merge branch 'develop' into feature/TV-700-erik

commit c8296cbff42d747df8c17cf3858f22956fd1e910
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Oct 6 07:44:41 2021 +0200

    Fixed some linting and tests

commit ec0bf7cd3616859742e461ffd65a2289f5c50cd6
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Oct 6 07:37:28 2021 +0200

    Changes after PR

commit aa6cee5248299056e043170b8803335529277062
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Tue Oct 5 14:56:45 2021 +0200

    Fixed some styling

commit 86de8306679fcff5ed8595f97f696cb43f38f4ac
Merge: 3b1822d8 5cee9695
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Tue Oct 5 14:46:52 2021 +0200

    Merged develop and resolved conflicts

commit 3b1822d8c8f197b789d1db5832b8e99351e8afa3
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Tue Oct 5 14:22:06 2021 +0200

    Updated GP

commit a63dfb716a3888f3e5830fe224de4cd16b1922c2
Merge: e2a8cb1c 07ec3c4a
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Tue Oct 5 07:29:23 2021 +0200

    Merge branch 'develop' into feature/TV-700

commit e2a8cb1cef2ada931f8a82ae0e9849aabb77ac4d
Author: Chingiz <chingiz.esenbaev@arbetsformedlingen.se>
Date:   Mon Oct 4 20:16:42 2021 +0200

    lamnas over till team 1
This commit is contained in:
Erik Tiekstra
2021-10-06 09:41:34 +02:00
parent cc0a9aae7f
commit 3d941fddfa
24 changed files with 522 additions and 250 deletions

View File

@@ -42,12 +42,12 @@
</ng-container>
</h2>
<div class="avrop__progress-bar" *ngIf="currentStep < 4">
<span>Steg {{ currentStep }} av {{ totalAmountOfSteps }}:</span>
<digi-ng-progress-progressbar
[afSteps]="totalAmountOfSteps"
[afActiveStep]="currentStep"
></digi-ng-progress-progressbar>
<div class="avrop__progress-bar">
<digi-progressbar
[afTotalSteps]="totalAmountOfSteps"
[afCompletedSteps]="currentStep - 1"
af-steps-label="steg avklarade"
></digi-progressbar>
</div>
</div>
<div class="avrop__content" *ngIf="avropData.data.length; else noAvrop">

View File

@@ -19,6 +19,7 @@
&__progress-bar {
z-index: $msfa__z-index-default;
min-width: 12rem;
}
&__select-handledare {

View File

@@ -1,55 +1,47 @@
<section class="report-layout" *ngIf="contactInformation$ | async as contactInformation; else skeletonRef">
<section class="report-layout">
<digi-typography>
<h1>{{ reportTitle }}</h1>
<p class="report-layout__description">{{description}}</p>
<div class="report-layout__deltagare-info">
<h2>{{reportSubTitle}}</h2>
<div class="report-layout__name">{{contactInformation.firstName + ' ' + contactInformation.lastName}}</div>
<div class="report-layout__personnummer">
<span>Personnummer:</span>
<msfa-hide-text
symbols="********-****"
[changingText]="contactInformation.ssn"
ariaLabelType="Personnumer"
></msfa-hide-text>
</div>
<span *ngIf="service">Tjänst: KROM</span>
<ng-container *ngIf="startDate && endDate && !isPeriodDate">
<span>Startdatum: {{startDate}}</span>
<span>Slutdatum: {{endDate}}</span>
</ng-container>
<span *ngIf="startDate && endDate && isPeriodDate">Avser period: {{startDate}} - {{endDate}}</span>
<p class="report-layout__description" *ngIf="description">{{description}}</p>
<div class="report-layout__deltagare-info" *ngIf="avrop">
<h2 *ngIf="reportSubTitle">{{reportSubTitle}}</h2>
<dl>
<dt>Namn</dt>
<dd>{{avrop.fullName}}</dd>
</dl>
<dl>
<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>
<ng-container *ngIf="!isPeriodDate; else periodDateRef">
<dt>Startdatum</dt>
<dd>{{avrop.startDate | date:'longDate'}}</dd>
<dt>Slutdatum</dt>
<dd>{{avrop.endDate | date:'longDate'}}</dd>
</ng-container>
<ng-template #periodDateRef>
<dt>Avser period</dt>
<dd>{{avrop.startDate | date:'longDate'}} - {{avrop.endDate | date:'longDate'}}</dd>
</ng-template>
</dl>
</div>
<div class="report-layout__notification-alert">
<digi-notification-alert
*ngIf="showSuccessNotification"
af-variation="success"
af-heading="Allt gick bra"
af-heading-level="h3"
>
<p>Din {{reportTitle.toLocaleLowerCase()}} är nu inskickad till Arbetsförmedlingen.</p>
</digi-notification-alert>
<digi-notification-alert
*ngIf="showDangerNotification"
af-variation="danger"
af-heading="Någonting gick fel"
af-heading-level="h3"
>
<p>Vi kunde inte skicka in din {{reportTitle.toLocaleLowerCase()}}.</p>
</digi-notification-alert>
<div class="report-layout__progress-bar">
<digi-progressbar
[afTotalSteps]="totalAmountOfSteps"
[afCompletedSteps]="currentStep - 1"
af-steps-label="steg avklarade"
></digi-progressbar>
</div>
<digi-ng-progress-progressbar
[afSteps]="totalAmountOfSteps"
[afActiveStep]="currentStep"
></digi-ng-progress-progressbar>
<div class="report-layout__main-content">
<ng-content></ng-content>
</div>
</digi-typography>
</section>
<ng-template #skeletonRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar skapa rapportsida"></digi-ng-skeleton-base>
</ng-template>

View File

@@ -1,32 +1,11 @@
@import 'apps/mina-sidor-fa/src/styles/variables/gutters';
.report-layout {
&__name {
margin-top: 0;
font-weight: var(--digi--typography--font-weight--semibold);
}
&__deltagare-info {
display: flex;
flex-direction: column;
margin-bottom: $digi--layout--gutter--xl;
font-size: var(--digi--typography--font-size--m);
font-weight: var(--digi--typography--font-weight--semibold);
}
&__personnummer {
display: flex;
}
&__personnummer msfa-hide-text {
margin-left: var(--digi--layout--gutter--s);
}
&__notification-alert {
margin-bottom: $digi--layout--gutter--xl;
}
&__main-content {
&__progress-bar {
margin: $digi--layout--gutter--xl 0;
}
}

View File

@@ -1,9 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ContactInformation } from '@msfa-models/contact-information.model';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { Avrop } from '@msfa-models/avrop.model';
@Component({
selector: 'msfa-report-layout',
@@ -12,23 +8,17 @@ import { switchMap } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReportLayoutComponent {
@Input() reportTitle = 'Report Title';
@Input() reportSubTitle = 'Report Sub Title';
@Input() description = 'Report description ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem';
@Input() reportTitle: string;
@Input() reportSubTitle: string;
@Input() description: string;
@Input() startDate: string;
@Input() endDate: string;
@Input() service: string;
@Input() isPeriodDate = false;
@Input() avrop: Avrop;
@Input() totalAmountOfSteps = 3;
@Input() currentStep = 1;
@Input() showSuccessNotification = false;
@Input() showDangerNotification = false;
contactInformation$: Observable<ContactInformation> = this.activatedRoute.params.pipe(
switchMap(({ genomforandeReferens }) =>
this.deltagareApiService.fetchContactInformation$(genomforandeReferens)
)
);
constructor(private deltagareApiService: DeltagareApiService, private activatedRoute: ActivatedRoute) { }
@Input() submitted = false;
}

View File

@@ -1,22 +1,15 @@
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
import { CommonModule } from '@angular/common';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { ReportLayoutComponent } from './report-layout.component';
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
import { HideTextModule } from '@msfa-shared/components/hide-text/hide-text.module';
import { RouterModule } from '@angular/router';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ReportLayoutComponent],
imports: [
CommonModule,
RouterModule,
LayoutModule,
DigiNgProgressProgressbarModule,
DigiNgSkeletonBaseModule,
HideTextModule],
imports: [CommonModule, RouterModule, LayoutModule, DigiNgProgressProgressbarModule, HideTextModule],
exports: [ReportLayoutComponent],
})
export class ReportLayoutModule {}

View File

@@ -7,7 +7,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { DeltagareAvvikelseComponent } from './deltagare-avvikelse.component';
import { DeltagareAvvikelseService } from './deltagare-avvikelse.service';
describe('DeltagareAvvikelseComponent', () => {
let component: DeltagareAvvikelseComponent;
@@ -22,8 +22,9 @@ describe('DeltagareAvvikelseComponent', () => {
HttpClientTestingModule,
ReactiveFormsModule,
DigiNgFormRadiobuttonGroupModule,
DigiNgFormDatepickerModule
]
DigiNgFormDatepickerModule,
],
providers: [DeltagareAvvikelseService],
}).compileComponents();
});

View File

@@ -1,43 +1,126 @@
<msfa-layout>
<form [formGroup]="planeringFormGroup" (ngSubmit)="onFormSubmitted()">
<msfa-report-layout
reportTitle="Gemensam Planering"
reportSubTitle="Aktiviteter"
[totalAmountOfSteps]="totalAmountOfSteps"
[isPeriodDate]="true"
[currentStep]="currentStep"
[startDate]="'2021-09-01'"
[endDate]="'2021-09-30'"
(currentStepEvent)="currentStep = $event"
(sendRequestEvent)="sendRequest = $event"
>
<div class="gemensam-planering_activity" *ngIf="currentStep === 1">
<h3>Deltar arbetssökande på distans?</h3>
<digi-ng-form-radiobutton-group
[afRadiobuttons]="distanceChoice"
[formControlName]="participatedOnDistansFormControlName"
[afRadiobuttonGroupDirection]="direction"
></digi-ng-form-radiobutton-group>
<msfa-report-layout
*ngIf="avrop$ | async as avrop; else skeletonRef"
reportTitle="Gemensam planering"
[totalAmountOfSteps]="totalAmountOfSteps"
[currentStep]="currentStep"
[avrop]="avrop"
[submitted]="lastSubmittedGP$ | async"
[isPeriodDate]="true"
>
<div class="gemensam-planering" *ngIf="currentGenomforandeReferens$ | async as genomforandeReferens">
<div class="gemensam-planering__confirmation" *ngIf="lastSubmittedGP$ | async as lastSubmittedGP; else formRef">
<digi-notification-alert af-variation="success" af-heading="Allt gick bra" af-heading-level="h3">
<p>
Gemensam planering för deltagare {{avrop.fullName}} är nu inskickad till Arbetsförmedlingen och inväntar
godkännande.
</p>
<dl>
<dt>Datum</dt>
<dd>{{lastSubmittedGP | date:'longDate'}} kl {{lastSubmittedGP | date:'shortTime'}}</dd>
</dl>
</digi-notification-alert>
<msfa-back-link [route]="['/deltagare/'+ genomforandeReferens]">Tillbaka till deltagaren</msfa-back-link>
</div>
<div class="gemensam-planering_activity" *ngIf="currentStep === 2">Förhandsgranska</div>
<div class="gemensam-planering__step-buttons-wrapper">
<ng-container *ngIf="currentStep > 1">
<digi-button
class="gemensam-planering__step-buttons-wrapper--space-right"
af-variation="secondary"
af-size="m"
(afOnClick)="previousStep()"
>
Tillbaka
</digi-button>
</ng-container>
<ng-container *ngIf="currentStep === (totalAmountOfSteps -1)">
<digi-button af-size="m" (afOnClick)="nextStep()"> Förhandsgranska </digi-button>
</ng-container>
<ng-container *ngIf="currentStep === totalAmountOfSteps">
<digi-button af-size="m" (afOnClick)="sendRequest(true)"> Skicka in </digi-button>
</ng-container>
</div>
</msfa-report-layout>
</form>
<ng-template #formRef>
<form
class="gemensam-planering__form"
[formGroup]="gpFormGroup"
(ngSubmit)="openConfirmDialog()"
id="gemensam-planering-form"
>
<ng-container *ngIf="currentStep === 1">
<digi-form-fieldset
af-legend="Deltar arbetssökande på distans?"
af-name="distance"
af-form="gemensam-planering-form"
>
<digi-ng-form-radiobutton-group
[afRadiobuttons]="distanceRadiobuttons"
formControlName="distance"
[afRequired]="true"
[afRadiobuttonGroupDirection]="RadiobuttonGroupDirection.HORIZONTAL"
></digi-ng-form-radiobutton-group>
</digi-form-fieldset>
<digi-form-fieldset af-legend="Aktiviteter" af-name="aktivitetsIds" af-form="gemensam-planering-form">
<p>
Varje gemensam planering måste innehålla de två obligatoriska aktiviteterna samt en frivillig aktivitet
som en del av det individuella stödet för varje deltagare.
</p>
<ng-container *ngIf="activities$ | async as activities; else loadingRef">
<ul class="gemensam-planering__activity-list">
<li class="gemensam-planering__activity-item" *ngFor="let activity of activities;">
<digi-form-checkbox
[afLabel]="activity.name + (isActivityObligatory(activity.id) ? ' (obligatorisk)' : '')"
[afValue]="activity.id"
[afValidation]="showActivityAsInvalid(activity.id) ? 'error' : 'neutral'"
[afChecked]="isActivityChecked(activity.id) || isActivityObligatory(activity.id)"
(afOnChange)="updateActivityIds(activity.id, $event.detail.target.checked)"
></digi-form-checkbox>
</li>
</ul>
<digi-form-validation-message
*ngIf="shouldValidate && gpFormGroup.errors?.activityIds"
af-variation="error"
>{{gpFormGroup.errors.activityIds}}</digi-form-validation-message
>
</ng-container>
</digi-form-fieldset>
</ng-container>
<ng-container *ngIf="currentStep === 2">
<dl>
<dt>Deltar arbetssökande på distans?</dt>
<dd>{{gpFormGroup.value.distance ? 'Ja' : 'Nej'}}</dd>
<dt>Aktiviteter</dt>
<dd>
<ul class="gemensam-planering__activity-list" *ngFor="let activity of activities$ | async">
<li
class="gemensam-planering__activity-item"
*ngIf="activityIdsFormArray.value.includes(activity.id)"
>
<digi-icon-check-circle
class="msfa__digi-icon gemensam-planering__activity-check"
aria-hidden="true"
></digi-icon-check-circle>
{{activity.name}}
</li>
</ul>
</dd>
</dl>
</ng-container>
<footer class="gemensam-planering__footer">
<ng-container *ngIf="currentStep > 1">
<digi-button
class="gemensam-planering__step-buttons-wrapper--space-right"
af-variation="secondary"
af-size="m"
(afOnClick)="goToStep1()"
>Tillbaka</digi-button
>
</ng-container>
<ng-container *ngIf="currentStep === 1">
<digi-button af-size="m" (afOnClick)="goToPreview()">Förhandsgranska</digi-button>
</ng-container>
<ng-container *ngIf="currentStep === 2">
<digi-button af-type="submit" af-size="m">Bekräfta och skicka in</digi-button>
</ng-container>
</footer>
</form>
<msfa-confirm-dialog
[openConfirmDialog]="confirmDialogOpen"
reportToConfirm="gemensam planering"
(confirmDialogChanged)="closeConfirmDialogAndProceed($event, genomforandeReferens)"
></msfa-confirm-dialog>
</ng-template>
</div>
</msfa-report-layout>
</msfa-layout>
<ng-template #skeletonRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar data för gemensam planering"></digi-ng-skeleton-base>
</ng-template>
<ng-template #loadingRef>
<msfa-loader type="padded"></msfa-loader>
</ng-template>

View File

@@ -1,8 +1,43 @@
@import 'mixins/list';
@import 'variables/gutters';
.gemensam-planering {
&__pages {
margin: 5rem 0rem;
&__confirmation,
&__form {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
}
&__step-buttons-wrapper--space-right {
margin-right: 1rem;
&__activity-list {
@include msfa__reset-list;
margin-bottom: var(--digi--layout--gutter--s);
}
&__activity-item {
display: flex;
align-items: center;
gap: var(--digi--layout--gutter--s);
margin-top: var(--digi--layout--gutter--s);
}
&__activity-check {
color: var(--digi--ui--color--border--success);
}
&__footer {
display: flex;
gap: var(--digi--layout--gutter);
}
::ng-deep {
.digi-form-fieldset {
margin: 0;
&__legend {
margin-bottom: var(--digi--layout--gutter--s);
padding: 0;
}
}
}
}

View File

@@ -1,8 +1,18 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { RadiobuttonGroupDirection, RadiobuttonModel } from '@af/digi-ng/_form/form-radiobutton-group';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { RequiredValidator } from '@msfa-validators/required.validator';
import { GemensamPlaneringApiService } from '@msfa-services/api/gemensam-planering-api.service';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { ConfirmDialog } from '@msfa-enums/confirm-dialog.enum';
import { Activity } from '@msfa-models/activity.model';
import { Avrop } from '@msfa-models/avrop.model';
import {
GemensamPlanering,
mapGemensamPlaneringToGemensamPlaneringPostRequest,
} from '@msfa-models/gemensam-planering.model';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators';
import { GemensamPlaneringService } from './gemensam-planering.service';
import { GemensamPlaneringValidator } from './gemensam-planering.validator';
@Component({
selector: 'msfa-deltagare-gemensam-planering',
@@ -10,54 +20,124 @@ import { GemensamPlaneringApiService } from '@msfa-services/api/gemensam-planeri
styleUrls: ['./deltagare-gemensam-planering.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareGemensamPlaneringComponent implements OnInit {
export class DeltagareGemensamPlaneringComponent {
totalAmountOfSteps = 2;
currentStep = 1;
direction = RadiobuttonGroupDirection.HORIZONTAL;
planeringFormGroup: FormGroup | null = null;
obligatoryActivityIds = [165, 188];
shouldValidate = false;
RadiobuttonGroupDirection = RadiobuttonGroupDirection;
confirmDialogOpen = false;
private _lastSubmittedGP$ = new BehaviorSubject<Date>(null);
lastSubmittedGP$: Observable<Date> = this._lastSubmittedGP$.asObservable();
readonly participatedOnDistansFormControlName = 'participatedOnDistans';
distanceChoice: RadiobuttonModel[] = [
activities$: Observable<Activity[]> = this.gemensamPlaneringService.activities$;
currentGenomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
map(params => params.genomforandeReferens as string),
distinctUntilChanged(
([prevGenomforandeReferens], [currGenomforandeReferens]) => prevGenomforandeReferens === currGenomforandeReferens
),
map(genomforandeReferens => +genomforandeReferens)
);
avrop$: Observable<Avrop> = this.currentGenomforandeReferens$.pipe(
switchMap(genomforandeReferens => this.gemensamPlaneringService.fetchAvropInformation$(genomforandeReferens)),
shareReplay(1)
);
gpFormGroup = new FormGroup(
{
label: 'Ja',
value: 'Ja',
},
{
label: 'Nej',
value: 'Nej',
distance: new FormControl(false),
activityIds: new FormArray(this.obligatoryActivityIds.map(id => new FormControl(id))),
},
[GemensamPlaneringValidator.isGemensamPlaneringValid(this.obligatoryActivityIds)]
);
distanceRadiobuttons: RadiobuttonModel[] = [
{ label: 'Ja', value: true },
{ label: 'Nej', value: false },
];
constructor(private gemensamPlaneringService: GemensamPlaneringApiService) {}
get participatedOnDistansFormControl(): AbstractControl | undefined {
return this.planeringFormGroup?.get(this.participatedOnDistansFormControlName);
get activityIdsFormArray(): FormArray {
return this.gpFormGroup.get('activityIds') as FormArray;
}
ngOnInit(): void {
this.planeringFormGroup = new FormGroup({
participatedOnDistans: new FormControl('', [RequiredValidator()]),
});
get selectedActivityIds(): number[] {
return this.activityIdsFormArray.value as number[];
}
nextStep(): void {
if (this.planeringFormGroup?.valid && this.currentStep < this.totalAmountOfSteps) {
console.log(this.participatedOnDistansFormControl.value);
this.currentStep++;
console.log(this.currentStep);
get selectedActivityIdsExcludingObligatory(): number[] {
return this.selectedActivityIds.filter(id => this.obligatoryActivityIds.indexOf(id) === -1);
}
isActivityChecked(id: number): boolean {
return this.selectedActivityIds.includes(id);
}
isActivityObligatory(id: number): boolean {
return this.obligatoryActivityIds.includes(id);
}
showActivityAsInvalid(id: number): boolean {
if (this.shouldValidate) {
if (this.isActivityObligatory(id) && !this.isActivityChecked(id)) {
return true;
}
if (!this.isActivityObligatory(id) && !this.selectedActivityIdsExcludingObligatory.length) {
return true;
}
}
return false;
}
constructor(private gemensamPlaneringService: GemensamPlaneringService, private activatedRoute: ActivatedRoute) {}
updateActivityIds(activityId: string, checked: boolean): void {
if (checked) {
this.activityIdsFormArray.push(new FormControl(+activityId));
} else {
const index: number = this.selectedActivityIds.findIndex(id => id === +activityId);
if (index > -1) {
this.activityIdsFormArray.removeAt(index);
}
}
}
previousStep(): void {
if (this.currentStep > 1) {
this.currentStep--;
console.log(this.currentStep);
goToPreview(): void {
this.shouldValidate = true;
if (this.gpFormGroup.invalid) {
return;
}
this.currentStep = 2;
}
goToStep1(): void {
this.shouldValidate = false;
this.currentStep = 1;
}
openConfirmDialog(): void {
this.confirmDialogOpen = true;
}
closeConfirmDialogAndProceed(confirmDialogAnswer: ConfirmDialog, genomforandeReferens: number): void {
if (confirmDialogAnswer === ConfirmDialog.ACCEPTED) {
const distance = this.gpFormGroup.get('distance').value as boolean;
const activityIds = this.gpFormGroup.get('activityIds').value as number[];
void this.postGemensamPlanering({ distance, activityIds, genomforandeReferens });
} else {
this.confirmDialogOpen = false;
}
}
sendRequest(val: boolean): boolean {
return val;
async postGemensamPlanering(postRequest: GemensamPlanering): Promise<void> {
return this.gemensamPlaneringService
.postGemensamPlanering(mapGemensamPlaneringToGemensamPlaneringPostRequest(postRequest))
.then(() => {
this._lastSubmittedGP$.next(new Date());
});
}
onFormSubmitted(): void {}
}

View File

@@ -1,13 +1,17 @@
import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { DeltagareGemensamPlaneringComponent } from './deltagare-gemensam-planering.component';
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { ConfirmDialogModule } from '@msfa-shared/components/confirm-dialog/confirm-dialog.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 { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
import { DeltagareGemensamPlaneringComponent } from './deltagare-gemensam-planering.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -20,6 +24,10 @@ import { DigiNgFormCheckboxModule } from '@af/digi-ng/_form/form-checkbox';
DigiNgFormRadiobuttonGroupModule,
ReactiveFormsModule,
ReportLayoutModule,
ConfirmDialogModule,
BackLinkModule,
LoaderModule,
DigiNgSkeletonBaseModule,
DigiNgFormCheckboxModule,
],
exports: [DeltagareGemensamPlaneringComponent],

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { Activity } from '@msfa-models/activity.model';
import { GemensamPlaneringPostRequest } from '@msfa-models/api/gemensam-planering.request.model';
import { Avrop } from '@msfa-models/avrop.model';
import { GemensamPlaneringApiService } from '@msfa-services/api/gemensam-planering-api.service';
import { BehaviorSubject, Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class GemensamPlaneringService {
public activities$: Observable<Activity[]> = this.gemensamPlaneringApiService.fetchActivities$();
private _error$ = new BehaviorSubject<string>(null);
public error$: Observable<string> = this._error$.asObservable();
public fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
return this.gemensamPlaneringApiService.fetchAvropInformation$(genomforandeReferens);
}
public async postGemensamPlanering(requestData: GemensamPlaneringPostRequest): Promise<void> {
// TODO: When API has been updated we can activate the real post
return of(undefined as void).toPromise();
return this.gemensamPlaneringApiService.postGemensamPlanering(requestData);
}
constructor(private gemensamPlaneringApiService: GemensamPlaneringApiService) {}
}

View File

@@ -0,0 +1,37 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export class GemensamPlaneringValidator {
static isGemensamPlaneringValid(obligatoryActivityIds: number[] = []): ValidatorFn {
return (c: AbstractControl): { [key: string]: string } => {
let errors: { [key: string]: string } = null;
const activityIds = (c.get('activityIds')?.value as number[]) || [];
const obligatoryActivityIdsMissing = !obligatoryActivityIds.every(id => activityIds.includes(id));
const activityIdsWithoutObligatoryActivityIds = activityIds.filter(
id => obligatoryActivityIds.indexOf(id) === -1
);
if (obligatoryActivityIdsMissing && !activityIdsWithoutObligatoryActivityIds.length) {
errors = {
...errors,
activityIds: 'Obligatoriska aktiviteter och minst en frivillig aktivitet måste väljas',
};
} else {
if (obligatoryActivityIdsMissing) {
errors = {
...errors,
activityIds: 'Obligatoriska aktiviteter måste väljas',
};
}
if (!activityIdsWithoutObligatoryActivityIds.length) {
errors = {
...errors,
activityIds: 'Minst en frivillig aktivitet måste väljas',
};
}
}
return errors;
};
}
}

View File

@@ -1,8 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeltagarePeriodiskRedovisningComponent } from './deltagare-periodisk-redovisning.component';
describe('PeriodiskRedovisningComponent', () => {
describe('DeltagarePeriodiskRedovisningComponent', () => {
let component: DeltagarePeriodiskRedovisningComponent;
let fixture: ComponentFixture<DeltagarePeriodiskRedovisningComponent>;

View File

@@ -1,8 +1,7 @@
import { RadiobuttonGroupDirection } from '@af/digi-ng/_form/form-radiobutton-group';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
import { Activity, SubActivity } from '@msfa-models/activity.model';
import { ActivitiesApiService } from '@msfa-services/api/activities-api.service';
@Component({
selector: 'msfa-deltagare-periodisk-redovisning',
@@ -10,7 +9,7 @@ import { ActivitiesApiService } from '@msfa-services/api/activities-api.service'
styleUrls: ['./deltagare-periodisk-redovisning.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagarePeriodiskRedovisningComponent implements OnInit {
export class DeltagarePeriodiskRedovisningComponent {
radiobuttonGroupDirection = RadiobuttonGroupDirection;
totalAmountOfSteps = 3;
currentStep = 1;
@@ -25,8 +24,6 @@ export class DeltagarePeriodiskRedovisningComponent implements OnInit {
readonly datesForActivitiesFormArrayName = 'datesForActivities';
readonly subActivitiesFormArrayName = 'subActivities';
constructor(private activitiesApiService: ActivitiesApiService) {}
get lamnatJobbForslagFormControl(): AbstractControl {
return this.periodiskRedovisningFormGroup.get(this.lamnatJobbForslagFormControlName);
}
@@ -39,12 +36,6 @@ export class DeltagarePeriodiskRedovisningComponent implements OnInit {
return this.periodiskRedovisningFormGroup.get(this.activitiesFormArrayName) as FormArray;
}
ngOnInit(): void {
this.activitiesApiService
.getActivities$()
.subscribe(activities => this.initializePeriodiskRedovisningFormGroup(activities));
}
initializePeriodiskRedovisningFormGroup(activitiesList: Activity[]): void {
this.periodiskRedovisningFormGroup = new FormGroup({
lamnatJobbForslag: new FormControl(null, [Validators.required]),
@@ -70,9 +61,8 @@ export class DeltagarePeriodiskRedovisningComponent implements OnInit {
getActivtiesFormField(formCtrlName: string, activity: Activity): FormArray | FormControl {
if (formCtrlName === 'datesForActivities') {
return new FormArray([]);
} else if (formCtrlName === 'subActivities') {
return this.addSubActivitesFormArray(activity?.subActivities, activity.id);
}
return new FormControl(null, []);
}

View File

@@ -1,12 +1,12 @@
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { ReportLayoutModule } from '../components/report-layout/report-layout.module';
import { DeltagarePeriodiskRedovisningComponent } from './deltagare-periodisk-redovisning.component';
import { PeriodiskRedovisningFormModule } from './components/periodisk-redovisning-form/periodisk-redovisning-form.module';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DeltagarePeriodiskRedovisningComponent } from './deltagare-periodisk-redovisning.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],

View File

@@ -1,31 +1,33 @@
<msfa-layout>
<ng-container *ngIf="(showUnauthorizedError$ | async) === false; else roleError">
<section *ngIf="allDeltagareData$ | async as allDeltagareData; else loadingRef" class="deltagare">
<header class="deltagare__header">
<h1>Deltagarlista</h1>
<p>
Här ser du en lista på de deltagare som tillhör din organisation. Klicka på deltagarens namn för att öppna
och se mer information om deltagarna.
</p>
</header>
<div class="deltagare__filter">
<digi-form-checkbox
(afOnChange)="setOnlyMyDeltagare($event.detail.target.checked)"
[afChecked]="onlyMyDeltagare$ | async"
af-label="Visa endast mina tilldelade deltagare"
class="deltagare__only-my-deltagare"
></digi-form-checkbox>
</div>
<msfa-deltagare-list-table
(paginated)="setNewPage($event)"
(sorted)="handleDeltagareSort($event)"
*ngIf="allDeltagareData.data.length; else noDeltagare"
[deltagareLoading]="deltagareLoading$ | async"
[deltagare]="allDeltagareData.data"
[paginationMeta]="allDeltagareData.meta"
[sort]="sort$ | async"
></msfa-deltagare-list-table>
</section>
<digi-typography>
<section *ngIf="allDeltagareData$ | async as allDeltagareData; else loadingRef" class="deltagare">
<header class="deltagare__header">
<h1>Deltagarlista</h1>
<p>
Här ser du en lista på de deltagare som tillhör din organisation. Klicka på deltagarens namn för att öppna
och se mer information om deltagarna.
</p>
</header>
<div class="deltagare__filter">
<digi-form-checkbox
(afOnChange)="setOnlyMyDeltagare($event.detail.target.checked)"
[afChecked]="onlyMyDeltagare$ | async"
af-label="Visa endast mina tilldelade deltagare"
class="deltagare__only-my-deltagare"
></digi-form-checkbox>
</div>
<msfa-deltagare-list-table
(paginated)="setNewPage($event)"
(sorted)="handleDeltagareSort($event)"
*ngIf="allDeltagareData.data.length; else noDeltagare"
[deltagareLoading]="deltagareLoading$ | async"
[deltagare]="allDeltagareData.data"
[paginationMeta]="allDeltagareData.meta"
[sort]="sort$ | async"
></msfa-deltagare-list-table>
</section>
</digi-typography>
</ng-container>
</msfa-layout>

View File

@@ -5,7 +5,7 @@ import { ConfirmDialog } from '@msfa-enums/confirm-dialog.enum';
selector: 'msfa-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmDialogComponent {
@Input() openConfirmDialog: boolean;
@@ -21,5 +21,4 @@ export class ConfirmDialogComponent {
this.openConfirmDialog = false;
this.confirmDialogChanged.emit(ConfirmDialog.DISMISSED);
}
}

View File

@@ -1,8 +1,6 @@
export interface Activity {
id: number;
name: string;
description?: string;
subActivities: SubActivity[];
}
export interface SubActivity {
id: number;
@@ -12,15 +10,12 @@ export interface SubActivity {
export interface ActivityResponse {
id: number;
name: string;
description?: string;
subActivities: SubActivity[];
}
export function mapResponseToActivity(data: ActivityResponse): Activity {
const { id, name, description, subActivities } = data;
const { id, name } = data;
return {
id,
name,
description,
subActivities,
};
}

View File

@@ -0,0 +1,5 @@
export interface GemensamPlaneringPostRequest {
genomforandeReferens: string;
distans: boolean;
aktivitetsIds: number[];
}

View File

@@ -1,4 +1,5 @@
import { TrackName } from '@msfa-enums/track-name.enum';
import { mapStringToSsn } from '@msfa-utils/map-string-to-ssn.util';
import { AvropResponse } from './api/avrop.response.model';
import { PaginationMeta } from './pagination-meta.model';
@@ -17,6 +18,7 @@ export interface AvropCompact {
}
export interface Avrop extends AvropCompact {
ssn: string; // personnummer
genomforandeReferens: number; // genomforandeReferens
participationFrequency: number; // deltagandeGrad
utforandeVerksamhet: string; // utforandeverksamhet
@@ -41,6 +43,7 @@ export function mapAvropResponseToAvrop(data: AvropResponse): Avrop {
sprakstod,
adress,
sparkod,
personnummer,
genomforandeReferens,
deltagandeGrad,
utforandeverksamhet,
@@ -60,6 +63,7 @@ export function mapAvropResponseToAvrop(data: AvropResponse): Avrop {
utforandeAdress: adress,
trackCode: sparkod,
trackName: (TrackName[sparkod] || TrackName.UNKNOWN) as TrackName,
ssn: mapStringToSsn(personnummer),
genomforandeReferens,
participationFrequency: deltagandeGrad,
utforandeVerksamhet: utforandeverksamhet,

View File

@@ -1,3 +1,11 @@
import { GemensamPlaneringPostRequest } from './api/gemensam-planering.request.model';
export interface GemensamPlanering {
genomforandeReferens: number;
distance: boolean;
activityIds: number[];
}
export interface Activity {
activityId: string;
name: string;
@@ -9,3 +17,15 @@ export interface SubActivity {
name: string;
description: string;
}
export function mapGemensamPlaneringToGemensamPlaneringPostRequest(
gemensamPlanering: GemensamPlanering
): GemensamPlaneringPostRequest {
const { genomforandeReferens, distance, activityIds } = gemensamPlanering;
return {
genomforandeReferens: genomforandeReferens.toString(),
distans: distance,
aktivitetsIds: activityIds,
};
}

View File

@@ -2,21 +2,48 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { Activity, ActivityResponse, mapResponseToActivity } from '@msfa-models/activity.model';
import { GemensamPlaneringPostRequest } from '@msfa-models/api/gemensam-planering.request.model';
import { Avrop } from '@msfa-models/avrop.model';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { catchError, filter, map, shareReplay } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class GemensamPlaneringApiService {
private _apiBaseUrl = `${environment.api.url}`;
private _apiBaseUrl = `${environment.api.url}/rapporter/gemensam-planering`;
constructor(private httpClient: HttpClient) {}
public getActivities$(): Observable<Activity[]> {
return this.httpClient.get<{ data: ActivityResponse[] }>(`${this._apiBaseUrl}/activities`).pipe(
public fetchActivities$(): Observable<Activity[]> {
return this.httpClient.get<{ data: ActivityResponse[] }>(`${this._apiBaseUrl}/aktiviteter`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(aktivitet => mapResponseToActivity(aktivitet)))
map(({ data }) => data.map(activity => mapResponseToActivity(activity))),
catchError((error: Error & { status: number }) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta aktiviteter.\n\n${error.message}` })
);
}),
shareReplay(1)
);
}
public fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
}
public async postGemensamPlanering(requestData: GemensamPlaneringPostRequest): Promise<void> {
return this.httpClient
.post<void>(`${this._apiBaseUrl}`, requestData)
.pipe(
catchError((error: Error & { status: number }) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte spara gemensam planering.\n\n${error.message}` })
);
})
)
.toPromise();
}
constructor(private httpClient: HttpClient, private deltagareApiService: DeltagareApiService) {}
}

View File

@@ -1,4 +1,9 @@
{
"/api/rapporter/gemensam-planering": {
"target": "https://mina-sidor-fa-utv.tocp.arbetsformedlingen.se",
"secure": false,
"changeOrigin": true
},
"/api": {
"target": "https://mina-sidor-fa-test.tocp.arbetsformedlingen.se",
"secure": false,