feat(report): Added reporting for frånvaro and avvikelser. (TV-731)

Squashed commit of the following:

commit 3df2d57cc2afdd3a64bea03b7d6e1e6520b18a0c
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 14:41:39 2021 +0200

    wip

commit ce4acffd4d4919a0dd38d83226ba6917ee9ecf32
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 14:33:58 2021 +0200

    recievedTimestamp

commit a8aa0494c39d9e0218bdd3edefa6f6c063d60189
Merge: 0bbe98e2 6c6e37ed
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 14:31:34 2021 +0200

    Merge pull request #188 in TEA/mina-sidor-fa-web from feature/TV-731-new-avvikelserapport to feature/TV-731-refaktorisera-avvikelserapport-split-rapporter

    * commit '6c6e37edeed74b6a8ea6dec24f68765027c3b50b':
      Avvikelserapport (avvikelse)
      Delete avvikelse-orsak-kod.enum.ts
      Update deltagare-avvikelserapport.component.html
      cleanup and rearrange
      unsubscribe, add types and error handling
      Delete avvikelse-form-validator.ts
      remove uneccessary component
      fix validation
      form and dialog done
      wip
      wip
      make fragor dynamic with formarray
      form arrays working
      first

commit 6c6e37edeed74b6a8ea6dec24f68765027c3b50b
Merge: a621fc7f 0bbe98e2
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 14:00:47 2021 +0200

    Merge branch 'feature/TV-731-refaktorisera-avvikelserapport-split-rapporter' into feature/TV-731-new-avvikelserapport

    # Conflicts:
    #	apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/deltagare-details.module.ts
    #	apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-card/components/deltagare-tab-reports/deltagare-tab-reports.component.html
    #	apps/mina-sidor-fa/src/app/pages/deltagare/pages/deltagare-details/pages/deltagare-reports/components/report-layout/report-layout.component.html
    #	apps/mina-sidor-fa/src/app/shared/constants/navigation.ts
    #	apps/mina-sidor-fa/src/app/shared/enums/report-type.enum.ts

commit a621fc7f2425699b6f8720d9cc53776174b65d42
Merge: 3a34d434 a1b81ba3
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 13:55:45 2021 +0200

    Merge branch 'feature/TV-731-refaktorisera-avvikelserapport-split-rapporter' into feature/TV-731-new-avvikelserapport

commit 0bbe98e2165ec412d61f1314a1021191c3b77042
Merge: a1b81ba3 d2041c10
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 13:55:16 2021 +0200

    Merge pull request #189 in TEA/mina-sidor-fa-web from feature/TV-731-franvaro-erik to feature/TV-731-refaktorisera-avvikelserapport-split-rapporter

    * commit 'd2041c10fe02a6c197571149093f734f3bf026f9':
      Updated constant
      Minor change after PR
      Minor changes after PR
      Minor changes after demo
      Removed console.log
      Implemented confirm dialog
      Fixed breadcrumbs
      Removed steps and fixed error message
      Added more validation and styling
      Added validation
      Started validation
      possible to post
      WIP
      implemented some form elements
      Added frånvaro-report component

commit d2041c10fe02a6c197571149093f734f3bf026f9
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Oct 8 13:53:09 2021 +0200

    Updated constant

commit 59820cef664c467d8a85fcfde79de7f1930cd9ed
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Oct 8 13:52:00 2021 +0200

    Minor change after PR

commit b45d975135d1de351661f42d6cb90fd9cd5e9aa3
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Oct 8 13:43:13 2021 +0200

    Minor changes after PR

commit a1b81ba3a7318c5ad7183194e1959058fbc151df
Merge: dad332c3 132aba21
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 13:39:03 2021 +0200

    Merge branch 'develop' into feature/TV-731-refaktorisera-avvikelserapport-split-rapporter

commit 3a34d434c0504cb9e4317e9fa5bfa786750d7ae4
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 13:38:49 2021 +0200

    Avvikelserapport (avvikelse)

commit b122196d3edf32b8535f01a2bf8159a032409b8f
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Oct 8 13:16:05 2021 +0200

    Minor changes after demo

commit 582e5f4b73fab2bcb86096cdda74c58b9320b3b2
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 13:04:49 2021 +0200

    Delete avvikelse-orsak-kod.enum.ts

commit 70a20b7232289669915ae452a4c06db2a20f4f5a
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Oct 8 12:17:11 2021 +0200

    Removed console.log

commit d63e20f087efdf664f05f90b27ff52ad1d912561
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Oct 8 12:15:51 2021 +0200

    Implemented confirm dialog

commit ccbb7709451e6af43bb5463bc5847bda9d5fe097
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 12:04:47 2021 +0200

    Update deltagare-avvikelserapport.component.html

commit 9bb05d9e8d66a412e1435ff8fb6b9f14ae442f2c
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 12:04:23 2021 +0200

    cleanup and rearrange

commit c8b7496bdc3258efa7169bafd17284899baba862
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 12:00:55 2021 +0200

    unsubscribe, add types and error handling

commit bfedf8c5c1d282cd049aef1ae0775aaa2a1bf74f
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 11:52:36 2021 +0200

    Delete avvikelse-form-validator.ts

commit 9f060e2f1271822a3f18be58d6f6ad4faa6c5ecc
Author: Daniel Appelgren <daniel.appelgren@arbetsformedlingen.se>
Date:   Fri Oct 8 11:51:11 2021 +0200

    remove uneccessary component

... and 29 more commits
This commit is contained in:
Erik Tiekstra
2021-10-08 14:44:43 +02:00
parent 4222e3b682
commit 438241db49
72 changed files with 1438 additions and 1523 deletions

View File

@@ -9,11 +9,17 @@ const routes: Routes = [
},
{
path: 'avvikelserapport',
data: { title: 'Skapa avvikelserapport' },
loadChildren: () =>
import('./pages/deltagare-reports/deltagare-avvikelserapport/deltagare-avvikelserapport.module').then(
m => m.DeltagareAvvikelserapportModule
),
},
{
path: 'franvarorapport',
data: { title: 'Skapa rapport' },
loadChildren: () =>
import('./pages/deltagare-reports/deltagare-avvikelse/deltagare-avvikelse.module').then(
m => m.DeltagareAvvikelseModule
),
import('./pages/deltagare-reports/franvaro-report/franvaro-report.module').then(m => m.FranvaroReportModule),
},
{
path: 'gemensam-planering',

View File

@@ -1,27 +1,25 @@
<div class="deltagare-tab-reports">
<form [formGroup]="reportPickerFormGroup" class="deltagare-tab-reports__form">
<h3>Skapa ny rapport</h3>
<p>Här kan du skicka rapporter om deltagaren till arbetsförmedlingen.</p>
<digi-ng-form-select
[afDisableValidStyle]="true"
[afInvalid]="reportsFormControl.invalid && reportsFormControl.touched"
[afRequired]="true"
[afSelectItems]="selectableReportTypes"
[formControlName]="reportsFormControlName"
afLabel="Välj rapporttyp"
afPlaceholder="Välj rapporttyp"
>
</digi-ng-form-select>
<digi-form-validation-message *ngIf="reportsFormControl.invalid && reportsFormControl.touched" af-variation="error">
Du måste välja en rapporttyp
</digi-form-validation-message>
</form>
<div class="deltagare-tab-reports__cta-wrapper">
<!-- <form [formGroup]="reportPickerFormGroup" class="deltagare-tab-reports__form">-->
<h3>Skapa ny rapport</h3>
<p>Här kan du skicka rapporter om deltagaren till arbetsförmedlingen.</p>
<!-- TODO put these in a styled list-->
<p>
<digi-ng-link-button [afRoute]="'./gemensam-planering'" afText="Skapa ny Gemensam planering"></digi-ng-link-button>
</p>
<p>
<digi-ng-link-button
(click)="onFormSubmitted($event, reportsFormControl.value)"
afText="Skapa ny rapport"
[afRoute]="'./franvarorapport'"
afText="Skapa ny Avvikelserapport (frånvaro)"
></digi-ng-link-button>
</div>
</p>
<p>
<digi-ng-link-button
[afRoute]="'./avvikelserapport'"
afText="Skapa ny Avvikelserapport (avvikelse)"
></digi-ng-link-button>
</p>
<ng-container *ngIf="reportsData; else loadingRef">
<h3>Inskickade rapporter</h3>
<msfa-reports-list

View File

@@ -32,7 +32,7 @@
</dl>
</div>
<div class="report-layout__progress-bar">
<div class="report-layout__progress-bar" *ngIf="!!totalAmountOfSteps && !!currentStep">
<digi-progressbar
[afTotalSteps]="totalAmountOfSteps"
[afCompletedSteps]="currentStep - 1"

View File

@@ -16,9 +16,8 @@ export class ReportLayoutComponent {
@Input() service: string;
@Input() isPeriodDate = false;
@Input() avrop: Avrop;
@Input() totalAmountOfSteps = 3;
@Input() currentStep = 1;
@Input() totalAmountOfSteps: number;
@Input() currentStep: number;
@Input() showSuccessNotification = false;
@Input() showDangerNotification = false;
@Input() submitted = false;
}

View File

@@ -1,68 +0,0 @@
<section class="deltagare-confirm">
<h3 class="deltagare-confirm__header">Vad är det du vill rapportera?</h3>
<p>{{ formGroup?.get('alternative').value === 'franvaro' ? 'Frånvaro' : 'Avvikelse'}}</p>
<ng-container *ngIf="formGroup?.get('alternative').value === 'franvaro'">
<h3 class="deltagare-confirm__header">Orsak till frånvaro</h3>
<ng-container *ngFor="let orsak of orsakskoderfranvaro">
<p *ngIf="orsak.value == formGroup?.get('orsakerFormGroup').get('orsaker').value">{{orsak.name}}</p>
</ng-container>
<ng-container *ngIf="formGroup?.get('orsakerFormGroup').get('andraKandaOrsaker').value">
<h3 class="deltagare-confirm__header">Annan känd orsak</h3>
<ng-container *ngFor="let annanKandOrsak of andraKandaOrsaker">
<p *ngIf="annanKandOrsak.value == formGroup?.get('orsakerFormGroup').get('andraKandaOrsaker').value">
{{annanKandOrsak.name}}
</p>
</ng-container>
</ng-container>
<ng-container *ngIf="formGroup?.get('orsakerFormGroup').get('andraKandaOrsaker').value == 5">
<h3 class="deltagare-confirm__header">Beskrivning</h3>
<p>{{formGroup?.get('description').value}}</p>
</ng-container>
<h3 class="deltagare-confirm__header">Datum</h3>
<p>{{formGroup?.get('date').value}}</p>
<h3 class="deltagare-confirm__header">Hel eller del av dag</h3>
<p>{{formGroup.get('dayOrPartOfDay').value === 'HELDAG' ? 'Heldag' : 'Del av dag'}}</p>
<ng-container *ngIf="formGroup.get('dayOrPartOfDay').value === 'DEL_AV_DAG'">
<h3 class="deltagare-confirm__header">Starttid</h3>
<p>{{formGroup?.get('timepickerFormGroup').get('startTime').value}}</p>
<h3 class="deltagare-confirm__header">Sluttid</h3>
<p>{{formGroup?.get('timepickerFormGroup').get('endTime').value}}</p>
</ng-container>
</ng-container>
<ng-container *ngIf="formGroup?.get('alternative').value === 'avvikelse'">
<h3 class="deltagare-confirm__header">Orsak till avvikelse</h3>
<ng-container *ngFor="let orsak of avvikelseOrsaker">
<p *ngIf="orsak.value == formGroup?.get('orsakerFormGroup').get('orsaker').value">{{orsak.name}}</p>
</ng-container>
<ng-container *ngFor="let fraga of fragor1">
<h3
*ngIf="fraga.id.includes(formGroup?.get('orsakerFormGroup').get('orsaker').value)"
class="deltagare-confirm__header"
>
{{fraga.name}}
</h3>
</ng-container>
<p>{{formGroup?.get('fragorFormGroup').get('fraga1').value}}</p>
<ng-container *ngFor="let fraga of fragor2">
<h3
*ngIf="formGroup?.get('fragorFormGroup').get('fraga2').value && fraga.id.includes(formGroup?.get('orsakerFormGroup').get('orsaker').value)"
class="deltagare-confirm__header"
>
{{fraga.name}}
</h3>
</ng-container>
<p *ngIf="selectedOrsak !== '28'">{{formGroup?.get('fragorFormGroup').get('fraga2').value}}</p>
<h3 class="deltagare-confirm__header">Datum</h3>
<p>{{formGroup?.get('date').value}}</p>
</ng-container>
</section>

View File

@@ -1,10 +0,0 @@
@import 'apps/mina-sidor-fa/src/styles/variables/gutters';
.deltagare-confirm {
margin-bottom: $digi--layout--gutter--xl;
&__header {
font-size: var(--digi--typography--font-size--l);
margin: 0;
}
}

View File

@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeltagareConfirmFormComponent } from './deltagare-confirm-form.component';
describe('DeltagareConfirmFormComponent', () => {
let component: DeltagareConfirmFormComponent;
let fixture: ComponentFixture<DeltagareConfirmFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DeltagareConfirmFormComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareConfirmFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,28 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AvvikelseOrsaksKodEnum } from '@msfa-enums/avvikelse-orsak-kod.enum';
import { FragorForAvvikelser } from '@msfa-models/fragor-for-avvikelser.model';
import { OrsaksKoderAvvikelse } from '@msfa-models/orsaks-koder-avvikelse.model';
import { KandaAvvikelseKoder, OrsaksKoderFranvaro } from '@msfa-models/orsaks-koder-franvaro.model';
@Component({
selector: 'msfa-deltagare-confirm-form',
templateUrl: './deltagare-confirm-form.component.html',
styleUrls: ['./deltagare-confirm-form.component.scss'],
changeDetection: ChangeDetectionStrategy.Default
})
export class DeltagareConfirmFormComponent implements OnChanges {
@Input() formGroup: FormGroup | null = null;
@Input() orsakskoderfranvaro: OrsaksKoderFranvaro[];
@Input() andraKandaOrsaker: KandaAvvikelseKoder[];
@Input() avvikelseOrsaker: OrsaksKoderAvvikelse[];
@Input() fragor1: FragorForAvvikelser[];
@Input() fragor2: FragorForAvvikelser[];
@Input() selectedOrsak: string;
ngOnChanges(changes: SimpleChanges): void {
if (Number(changes.selectedOrsak?.currentValue) === AvvikelseOrsaksKodEnum.SerTillAttErbjudetArbeteInteKommerTillStand) {
this.formGroup?.get('fragorFormGroup').get('fraga2').reset('');
}
}
}

View File

@@ -1,15 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DeltagareConfirmFormComponent } from './deltagare-confirm-form.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareConfirmFormComponent],
imports: [
CommonModule
],
exports: [DeltagareConfirmFormComponent]
})
export class DeltagareConfirmFormModule { }

View File

@@ -1,28 +0,0 @@
<form class="fragor-form" [formGroup]="fragorFormGroup" *ngIf="fragorFormGroup">
<ng-container *ngFor="let fraga of fragor1 | filterFragor: selectedOrsaksKod">
<div class="fragor-form__content">
<digi-ng-form-textarea
[formControlName]="'fraga1'"
[afLabel]="fraga.name"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalidMessage]="fragorFormGroup.get('fraga1').errors?.message"
[afInvalid]="fragorFormGroup.get('fraga1').invalid && fragorFormGroup.get('fraga1').touched"
[afMaxLength]="2000"
></digi-ng-form-textarea>
</div>
</ng-container>
<ng-container *ngFor="let fraga of fragor2 | filterFragor: selectedOrsaksKod">
<div class="fragor-form__content">
<digi-ng-form-textarea
[formControlName]="'fraga2'"
[afLabel]="fraga.name"
[afDisableValidStyle]="true"
[afRequired]="selectedOrsaksKod !== '19' && selectedOrsaksKod !== '20' && selectedOrsaksKod !== '28'"
[afInvalidMessage]="fragorFormGroup.get('fraga2').errors?.message"
[afInvalid]="fragorFormGroup.get('fraga2').invalid && fragorFormGroup.get('fraga2').touched"
[afMaxLength]="2000"
></digi-ng-form-textarea>
</div>
</ng-container>
</form>

View File

@@ -1,8 +0,0 @@
@import 'apps/mina-sidor-fa/src/styles/variables/gutters';
.fragor-form {
&__content {
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
}

View File

@@ -1,30 +0,0 @@
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { DeltagareFragorFormComponent } from './deltagare-fragor-form.component';
describe('DeltagareAvvikelseFormComponent', () => {
let component: DeltagareFragorFormComponent;
let fixture: ComponentFixture<DeltagareFragorFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [DigiNgFormRadiobuttonGroupModule, ReactiveFormsModule, DigiNgFormTextareaModule],
declarations: [DeltagareFragorFormComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareFragorFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,19 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FragorForAvvikelser } from '@msfa-models/fragor-for-avvikelser.model';
@Component({
selector: 'msfa-deltagare-fragor-form',
templateUrl: './deltagare-fragor-form.component.html',
styleUrls: ['./deltagare-fragor-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DeltagareFragorFormComponent {
@Input() fragor1: FragorForAvvikelser[] | null = null;
@Input() fragor2: FragorForAvvikelser[] | null = null;
@Input() fragorFormGroup: FormGroup | null = null;
@Input() avvikelseFormGroup: FormGroup | null = null;
@Input() selectedOrsaksKod: string | null = null;
}

View File

@@ -1,20 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DeltagareFragorFormComponent } from './deltagare-fragor-form.component';
import { ReactiveFormsModule } from '@angular/forms';
import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea';
import { FilterFragorPipe } from './filter-fragor.pipe';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareFragorFormComponent, FilterFragorPipe],
imports: [
CommonModule,
ReactiveFormsModule,
DigiNgFormTextareaModule
],
exports: [DeltagareFragorFormComponent]
})
export class DeltagareFragorFormModule { }

View File

@@ -1,19 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FragorForAvvikelser } from '@msfa-models/fragor-for-avvikelser.model';
@Pipe({
name: 'filterFragor',
})
export class FilterFragorPipe implements PipeTransform {
transform(fragor: FragorForAvvikelser[], id: string): FragorForAvvikelser[] {
if (fragor?.length === 0) {
return fragor;
}
return fragor?.filter(fraga => fraga?.id.substring(0, 2) === id);
}
}

View File

@@ -1,43 +0,0 @@
<form class="orsaks-form" [formGroup]="orsakerFormGroup" *ngIf="orsakerFormGroup">
<div *ngIf="selectedAlternative" class="orsaks-form__content">
<digi-ng-form-select
*ngIf="franvaroOrsaker || avvikelseOrsaker; else loadingRef"
[formControlName]="'orsaker'"
[afLabel]="selectedAlternative === 'franvaro' ? 'Orsak till frånvaro' : 'Orsak till avvikelse'"
[afPlaceholder]="'Välj orsak till ' + (selectedAlternative === 'franvaro' ? 'frånvaro' : 'avvikelse')"
[afSelectItems]="selectedAlternative === 'franvaro' ? franvaroOrsaker : avvikelseOrsaker"
[afDisableValidStyle]="true"
[afInvalid]="avvikelseFormGroup.errors?.orsakerIsRequired && orsakerFormGroup.touched"
></digi-ng-form-select>
<digi-form-validation-message
af-variation="error"
*ngIf="avvikelseFormGroup.errors?.orsakerIsRequired && orsakerFormGroup.touched"
>
Orsak är obligatoriskt
</digi-form-validation-message>
</div>
<ng-template #loadingRef>
<msfa-loader type="padded"></msfa-loader>
</ng-template>
<div *ngIf="showAndraKandaOrsaker" class="orsaks-form__content">
<digi-ng-form-select
*ngIf="andraKandaOrsaker"
[formControlName]="'andraKandaOrsaker'"
[afLabel]="'Välj känd orsak'"
[afPlaceholder]="'Känd orsak'"
[afSelectItems]="andraKandaOrsaker"
[afDisableValidStyle]="true"
[afInvalid]="avvikelseFormGroup.errors?.annanKandorsakIsRequired && orsakerFormGroup.touched"
></digi-ng-form-select>
<digi-form-validation-message
af-variation="error"
*ngIf="avvikelseFormGroup.errors?.annanKandorsakIsRequired && orsakerFormGroup.touched"
>
Orsak är obligatoriskt
</digi-form-validation-message>
</div>
<ng-template #loadingRef>
<msfa-loader type="padded"></msfa-loader>
</ng-template>
</form>

View File

@@ -1,8 +0,0 @@
@import 'apps/mina-sidor-fa/src/styles/variables/gutters';
.orsaks-form {
&__content {
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
}

View File

@@ -1,30 +0,0 @@
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { DeltagareOrsaksFormComponent } from './deltagare-orsaks-form.component';
describe('DeltagareAvvikelseFormComponent', () => {
let component: DeltagareOrsaksFormComponent;
let fixture: ComponentFixture<DeltagareOrsaksFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [DigiNgFormRadiobuttonGroupModule, ReactiveFormsModule, DigiNgFormSelectModule],
declarations: [DeltagareOrsaksFormComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareOrsaksFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,34 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FranvaroOrsaksKodEnum } from '@msfa-enums/franvaro-orsak-kod.enum';
import { ReportType } from '@msfa-enums/report-type.enum';
import { OrsaksKoderAvvikelse } from '@msfa-models/orsaks-koder-avvikelse.model';
import { OrsaksKoderFranvaro } from '@msfa-models/orsaks-koder-franvaro.model';
@Component({
selector: 'msfa-deltagare-orsaks-form',
templateUrl: './deltagare-orsaks-form.component.html',
styleUrls: ['./deltagare-orsaks-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareOrsaksFormComponent implements OnChanges {
@Input() franvaroOrsaker: OrsaksKoderFranvaro[] | null = null;
@Input() avvikelseOrsaker: OrsaksKoderAvvikelse[] | null = null;
@Input() andraKandaOrsaker: OrsaksKoderFranvaro[] | null = null;
@Input() orsakerFormGroup: FormGroup | null = null;
@Input() avvikelseFormGroup: FormGroup | null = null;
@Input() selectedAlternative: string | null = null;
ngOnChanges(changes: SimpleChanges): void {
if (changes) {
this.orsakerFormGroup.reset();
}
}
get showAndraKandaOrsaker(): boolean {
return (
this.selectedAlternative === ReportType.FRANVARO &&
+this.orsakerFormGroup.get('orsaker')?.value === FranvaroOrsaksKodEnum.AnnanKandOrsak
);
}
}

View File

@@ -1,21 +0,0 @@
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { LoaderModule } from '@msfa-shared/components/loader/loader.module';
import { DeltagareOrsaksFormComponent } from './deltagare-orsaks-form.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareOrsaksFormComponent],
imports: [
CommonModule,
ReactiveFormsModule,
DigiNgFormSelectModule,
LoaderModule
],
exports: [DeltagareOrsaksFormComponent]
})
export class DeltagareOrsaksFormModule { }

View File

@@ -1,28 +0,0 @@
<div class="deltagare-timepicker">
<form [formGroup]="timepickerFormGroup" *ngIf="timepickerFormGroup">
<digi-typography>
<label class="deltagare-timepicker__heading">Välj tid för frånvaro</label>
</digi-typography>
<div class="deltagare-timepicker__input deltagare-timepicker__start-time">
<digi-ng-form-input
afLabel
[afDescription]="'Ange starttid'"
[formControl]="timepickerFormGroup.get('startTime')"
[afDisableValidStyle]="true"
[afInvalidMessage]="timepickerFormGroup.get('startTime').errors?.message"
afType="time"
></digi-ng-form-input>
</div>
<div class="deltagare-timepicker__input">
<digi-ng-form-input
afLabel
[afDescription]="'Ange sluttid'"
[formControl]="timepickerFormGroup.get('endTime')"
[afDisableValidStyle]="true"
[afInvalidMessage]="timepickerFormGroup.get('endTime').errors?.message"
afType="time"
></digi-ng-form-input>
</div>
</form>
</div>

View File

@@ -1,17 +0,0 @@
@import 'apps/mina-sidor-fa/src/styles/variables/gutters';
.deltagare-timepicker {
margin-bottom: $digi--layout--gutter--xl;
&__heading {
font-weight: var(--digi--typography--font-weight--semibold);
}
&__start-time {
margin-bottom: $digi--layout--gutter--l;
}
&__input {
width: 127px;
}
}

View File

@@ -1,30 +0,0 @@
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { DeltagareTimePickerComponent } from './deltagare-time-picker.component';
describe('DeltagareTimePickerComponent', () => {
let component: DeltagareTimePickerComponent;
let fixture: ComponentFixture<DeltagareTimePickerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareTimePickerComponent],
imports: [ReactiveFormsModule, DigiNgFormSelectModule, DigiNgFormInputModule]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareTimePickerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,13 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'msfa-deltagare-time-picker',
templateUrl: './deltagare-time-picker.component.html',
styleUrls: ['./deltagare-time-picker.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DeltagareTimePickerComponent {
@Input() timepickerFormGroup: FormGroup | null = null;
@Input() avvikelseFormGroup: FormGroup | null = null;
}

View File

@@ -1,21 +0,0 @@
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { DeltagareTimePickerComponent } from './deltagare-time-picker.component';
import {DigiNgFormInputModule} from '@af/digi-ng/_form/form-input'
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareTimePickerComponent],
imports: [
CommonModule,
ReactiveFormsModule,
DigiNgFormSelectModule,
DigiNgFormInputModule
],
exports: [DeltagareTimePickerComponent]
})
export class DeltagareTimePickerModule { }

View File

@@ -1,166 +0,0 @@
<msfa-layout *ngIf="avrop$ | async as avrop; else skeletonRef">
<msfa-report-layout
[currentStep]="currentStep"
[totalAmountOfSteps]="totalAmountOfSteps"
description="Här rapporterar du deltagarens frånvaro och eventuella misskötsel i tjänsten. Rapportering via avvikelserapport ska också ske om tjänsten inte fungerar för deltagaren."
reportSubTitle="Skapa rapport"
reportTitle="Avvikelserapport"
>
<div *ngIf="submittedDate$ | async as submittedDate; " class="deltagare-avvikelse__confirmation">
<digi-notification-alert
af-heading="Allt gick bra"
af-heading-level="h3"
af-variation="success"
class="deltagare-avvikelse__alert"
>
<p>
Avvikelserapport för deltagare {{avrop.fullName}} är nu inskickad till
Arbetsförmedlingen och inväntar
godkännande.
</p>
<dl>
<dt>Datum</dt>
<dd>{{submittedDate | date:'longDate'}} kl {{submittedDate | date:'shortTime'}}</dd>
</dl>
</digi-notification-alert>
<msfa-back-link [route]="['../']">Tillbaka till deltagaren</msfa-back-link>
</div>
<ng-container *ngIf="currentStep === 1">
<h3 class="deltagare-avvikelse__alternative-heading">Vad är det du vill rapportera?</h3>
<form [formGroup]="avvikelseFormGroup">
<div class="deltagare-avvikelse__alternative">
<digi-ng-form-radiobutton-group
(change)="setAlternative()"
[afRadiobuttons]="avvikelseAlternatives"
[formControlName]="alternativeFormControlName"
></digi-ng-form-radiobutton-group>
<digi-form-validation-message
*ngIf="alternativeFormControl.invalid && alternativeFormControl.touched"
af-variation="error"
>
Alternativ är obligatoriskt
</digi-form-validation-message>
</div>
<msfa-deltagare-orsaks-form
(change)="setOrsakerChanged()"
[andraKandaOrsaker]="andraKandaOrsaker$ | async"
[avvikelseFormGroup]="avvikelseFormGroup"
[avvikelseOrsaker]="avvikelseOrsaker$ | async "
[franvaroOrsaker]="franvaroOrsaker$ | async"
[orsakerFormGroup]="avvikelseFormGroup.get('orsakerFormGroup')"
[selectedAlternative]="alternativeFormControl.value"
></msfa-deltagare-orsaks-form>
<div *ngIf="showDescription" class="deltagare-avvikelse__description">
<digi-ng-form-textarea
[afDisableValidStyle]="true"
[afInvalidMessage]="descriptionFormControl.errors?.message"
[afInvalid]="descriptionFormControl?.invalid && descriptionFormControl.touched"
[afMaxLength]="2000"
[afRequired]="true"
[afSize]="sizeTextArea"
[formControlName]="descriptionFormControlName"
afLabel="Beskriv frånvaro"
></digi-ng-form-textarea>
</div>
<msfa-deltagare-fragor-form
*ngIf="showFragor"
[avvikelseFormGroup]="avvikelseFormGroup"
[fragor1]="fragor1$ | async"
[fragor2]="fragor2$ | async"
[fragorFormGroup]="avvikelseFormGroup.get('fragorFormGroup')"
[selectedOrsaksKod]="selectedOrsaksKod"
></msfa-deltagare-fragor-form>
<div *ngIf="showDatePicker" class="deltagare-avvikelse__datepicker">
<digi-ng-form-datepicker
[afDisableValidStyle]="true"
[afMinDate]="setMinDate(avrop.startDate)"
[afMaxDate]="setMaxDate"
[afInvalid]="avvikelseFormGroup.errors?.dateIsRequired"
[afLabel]="'Välj dag för ' + (alternativeFormControl.value === 'franvaro' ? 'frånvaro' : 'avvikelse')"
[formControlName]="dateFormControlName"
></digi-ng-form-datepicker>
<digi-form-validation-message *ngIf="avvikelseFormGroup.errors?.dateIsRequired" af-variation="error">
{{avvikelseFormGroup.errors?.dateIsRequired}}
</digi-form-validation-message>
</div>
<div *ngIf="showDayOrPartOfDayPicker" class="deltagare-avvikelse__dayOrPartOfDay">
<digi-ng-form-radiobutton-group
(change)="setDayOrPartOfDayChanged()"
[afRadiobuttons]="dayOrPartOfDay"
[afRequired]="dayOrPartOfDayFormControl.invalid && dayOrPartOfDayFormControl.touched"
[formControlName]="dayOrPartOfDayFormControlName"
></digi-ng-form-radiobutton-group>
<digi-form-validation-message
*ngIf="dayOrPartOfDayFormControl.invalid && dayOrPartOfDayFormControl.touched"
af-variation="error"
>
{{dayOrPartOfDayFormControl.errors?.message}}
</digi-form-validation-message>
</div>
<msfa-deltagare-time-picker
*ngIf="showTimePicker"
[avvikelseFormGroup]="avvikelseFormGroup"
[timepickerFormGroup]="avvikelseFormGroup.get('timepickerFormGroup')"
></msfa-deltagare-time-picker>
</form>
</ng-container>
<msfa-deltagare-confirm-form
*ngIf="currentStep === totalAmountOfSteps"
[andraKandaOrsaker]="andraKandaOrsaker$ | async"
[avvikelseOrsaker]="avvikelseOrsaker$ | async"
[formGroup]="avvikelseFormGroup"
[fragor1]="fragor1$ | async"
[fragor2]="fragor2$ | async"
[orsakskoderfranvaro]="franvaroOrsaker$ | async"
[selectedOrsak]="selectedOrsaksKod"
></msfa-deltagare-confirm-form>
<digi-notification-alert
*ngIf="error$ | async as error"
af-heading="Någonting gick fel"
af-variation="danger"
class="deltagare-avvikelse__alert"
>
<p>Kunde inte spara avvikelserapporten. Ladda om sidan och försök igen.</p>
<p *ngIf="error.message" class="msfa__small-text">{{error.message}}</p>
</digi-notification-alert>
<div class="deltagare-avvikelse__step-buttons-wrapper">
<ng-container *ngIf="(submittedDate$ | async) === null">
<digi-button
(afOnClick)="previousStep()"
*ngIf="currentStep > 1"
af-size="m"
af-variation="secondary"
class="deltagare-avvikelse__step-buttons-wrapper--space-right"
>
Tillbaka
</digi-button>
<digi-button (afOnClick)="openConfirmDialog = true" *ngIf="currentStep === totalAmountOfSteps" af-size="m">
Skicka in
</digi-button>
</ng-container>
<digi-button (afOnClick)="nextStep" *ngIf="currentStep === (totalAmountOfSteps -1)" af-size="m">
Förhandsgranska
</digi-button>
</div>
</msfa-report-layout>
<msfa-confirm-dialog
(confirmDialogChanged)="setConfirmDialogChanged($event)"
[openConfirmDialog]="openConfirmDialog"
reportToConfirm="avvikelserapport"
>
</msfa-confirm-dialog>
</msfa-layout>
<ng-template #skeletonRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar data för avvikelserapport"></digi-ng-skeleton-base>
</ng-template>

View File

@@ -1,360 +0,0 @@
import { RadiobuttonModel } from '@af/digi-ng/_form/form-radiobutton-group';
import { FormTextareaSize } from '@af/digi-ng/_form/form-textarea';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { ConfirmDialog } from '@msfa-enums/confirm-dialog.enum';
import { DayOrPartOfDay } from '@msfa-enums/day-or-part-of-day.enum';
import { FranvaroOrsaksKodEnum } from '@msfa-enums/franvaro-orsak-kod.enum';
import { KandaOrsakerEnum } from '@msfa-enums/kanda-orsaker-kod.enum';
import { ReportType } from '@msfa-enums/report-type.enum';
import { AvvikelseAlternativ } from '@msfa-models/avvikelse-alternativ.model';
import { Avvikelse } from '@msfa-models/avvikelse.model';
import { FragorForAvvikelser } from '@msfa-models/fragor-for-avvikelser.model';
import { FranvaroAlternativ } from '@msfa-models/franvaro-alternativ.model';
import { OrsaksKoderAvvikelse } from '@msfa-models/orsaks-koder-avvikelse.model';
import { KandaAvvikelseKoder, OrsaksKoderFranvaro } from '@msfa-models/orsaks-koder-franvaro.model';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
import {
requiredAnnanKandOrsakValidator,
RequiredDateValidator,
requiredDayOrPartOfDayValidator,
requiredDescriptionValidator,
requiredEndTimeValidator,
requiredFraga1Validator,
requiredfraga2Validator,
requiredOrsakerValidator,
requiredStartTimeValidator,
} from '@msfa-validators/avvikelse-form-validator';
import { RequiredValidator } from '@msfa-validators/required.validator';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, shareReplay, switchMap } from 'rxjs/operators';
import { DeltagareAvvikelseService } from './deltagare-avvikelse.service';
import { avvikelseAlternatives, dayOrPartOfDay } from './report-alternatives';
import { CustomError } from '@msfa-models/error/custom-error';
import { ErrorType } from '@msfa-enums/error-type.enum';
import { Avrop } from '@msfa-models/avrop.model';
interface Params {
genomforandeReferens: string;
}
@Component({
selector: 'msfa-deltagare-avvikelse',
templateUrl: './deltagare-avvikelse.component.html',
styleUrls: ['./deltagare-avvikelse.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareAvvikelseComponent {
readonly alternativeFormControlName = 'alternative';
readonly descriptionFormControlName = 'description';
readonly dateFormControlName = 'date';
readonly dayOrPartOfDayFormControlName = 'dayOrPartOfDay';
readonly orsakerFormControlName = 'orsaker';
readonly andraKandaOrsakerFormControlName = 'andraKandaOrsaker';
readonly fraga1FormControlName = 'fraga1';
readonly fraga2FormControlName = 'fraga2';
readonly startTimeFormControlName = 'startTime';
readonly endTimeFormControlName = 'endTime';
genomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
map((params: Params) => +params.genomforandeReferens)
);
avrop$: Observable<Avrop> = this.genomforandeReferens$.pipe(
switchMap(genomforandeReferens => this.deltagareAvvikelseService.fetchAvropInformation$(genomforandeReferens)),
shareReplay(1)
);
franvaroOrsaker$: Observable<OrsaksKoderFranvaro[]>;
avvikelseOrsaker$: Observable<OrsaksKoderAvvikelse[]>;
andraKandaOrsaker$: Observable<KandaAvvikelseKoder[]>;
fragor1$: Observable<FragorForAvvikelser[]>;
fragor2$: Observable<FragorForAvvikelser[]>;
sizeTextArea: FormTextareaSize.S;
todayDate = new Date().toISOString().slice(0, 10);
avvikelseAlternatives: RadiobuttonModel[] = avvikelseAlternatives;
dayOrPartOfDay: RadiobuttonModel[] = dayOrPartOfDay;
selectedOrsaksKod: string;
avvikelseFormGroup = new FormGroup(
{
alternative: new FormControl(null, [RequiredValidator()]),
description: new FormControl('', [requiredDescriptionValidator()]),
date: new FormControl(this.todayDate),
dayOrPartOfDay: new FormControl(null, [requiredDayOrPartOfDayValidator()]),
orsakerFormGroup: new FormGroup({
orsaker: new FormControl([], [requiredOrsakerValidator()]),
andraKandaOrsaker: new FormControl([], [requiredAnnanKandOrsakValidator()]),
}),
fragorFormGroup: new FormGroup({
fraga1: new FormControl('', [requiredFraga1Validator()]),
fraga2: new FormControl('', [requiredfraga2Validator()]),
}),
timepickerFormGroup: new FormGroup({
startTime: new FormControl('', [requiredStartTimeValidator()]),
endTime: new FormControl('', [requiredEndTimeValidator()]),
}),
},
{
validators: [RequiredDateValidator.CheckIfRequired()],
}
);
totalAmountOfSteps = 2;
currentStep = 1;
openConfirmDialog = false;
private _submittedDate$ = new BehaviorSubject<Date | null>(null);
submittedDate$: Observable<Date | null> = this._submittedDate$.asObservable();
private _error$ = new BehaviorSubject<CustomError>(null);
error$: Observable<CustomError> = this._error$.asObservable();
private _showDangerNotification$ = new BehaviorSubject<boolean>(false);
constructor(
private deltagareAvvikelseService: DeltagareAvvikelseService,
private deltagareApiService: DeltagareApiService,
private activatedRoute: ActivatedRoute
) {}
get alternativeFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup?.get(this.alternativeFormControlName);
}
get descriptionFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup?.get(this.descriptionFormControlName);
}
get dateFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup?.get(this.dateFormControlName);
}
get dayOrPartOfDayFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup?.get(this.dayOrPartOfDayFormControlName);
}
get orsakerFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup?.get('orsakerFormGroup').get(this.orsakerFormControlName);
}
get andraKandaOrsakerFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup?.get('orsakerFormGroup').get(this.andraKandaOrsakerFormControlName);
}
get fraga1FormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup.get('fragorFormGroup').get(this.fraga1FormControlName);
}
get fraga2FormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup.get('fragorFormGroup').get(this.fraga2FormControlName);
}
get startTimeFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup.get('timepickerFormGroup').get(this.startTimeFormControlName);
}
get endTimeFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup.get('timepickerFormGroup').get(this.endTimeFormControlName);
}
get franvaro(): FranvaroAlternativ {
return {
avvikelseOrsaksKod: this.orsakerFormControl.value as string,
datum: this.dateFormControl.value as string,
heldag: this.dayOrPartOfDayFormControl.value === DayOrPartOfDay.HELDAG,
startTid: (this.startTimeFormControl.value as string) || '9:00',
slutTid: (this.endTimeFormControl.value as string) || '16:00',
forvantadNarvaro: {
startTid: '',
slutTid: '',
},
alternativForKandaOrsaker: {
typ: (this.andraKandaOrsakerFormControl.value as string) || '',
motivering: this.descriptionFormControl.value as string,
},
};
}
get avvikelse(): AvvikelseAlternativ {
return {
avvikelseorsakskod: this.orsakerFormControl.value as string,
frageformular: [
{
fraga: (this.avvikelseFormGroup.get('orsakerFormGroup').get('orsaker').value as string) + '_1',
svar: this.fraga1FormControl.value as string,
},
{
fraga:
(this.fraga2FormControl.value as string) !== ''
? (this.avvikelseFormGroup.get('orsakerFormGroup').get('orsaker').value as string) + '_2'
: '',
svar: this.fraga2FormControl.value as string,
},
],
rapporteringsdatum: this.dateFormControl.value as string,
};
}
get showDescription(): boolean {
return (
(this.alternativeFormControl.value as string) === ReportType.FRANVARO &&
+this.orsakerFormControl.value === FranvaroOrsaksKodEnum.AnnanKandOrsak &&
+this.andraKandaOrsakerFormControl.value === KandaOrsakerEnum.AnnanOrsak
);
}
get showFragor(): boolean {
return (
(this.alternativeFormControl.value as string) === ReportType.AVVIKELSE &&
(this.orsakerFormControl.value as boolean)
);
}
get showDatePicker(): boolean {
return this.orsakerFormControl.value as boolean;
}
get showDayOrPartOfDayPicker(): boolean {
return (
(this.alternativeFormControl.value as string) === ReportType.FRANVARO &&
(this.orsakerFormControl.value as boolean)
);
}
get showTimePicker(): boolean {
return (
(this.alternativeFormControl.value as string) === ReportType.FRANVARO &&
(this.dayOrPartOfDayFormControl.value as string) === DayOrPartOfDay.DEL_AV_DAG
);
}
get nextStep(): number {
this.avvikelseFormGroup.markAllAsTouched();
if (this.avvikelseFormGroup.valid && this.currentStep < this.totalAmountOfSteps) {
return this.currentStep++;
}
}
setConfirmDialogChanged(confirm: ConfirmDialog): void {
this.openConfirmDialog = false;
if (confirm === ConfirmDialog.ACCEPTED) {
const postAvvikelse: Avvikelse = {
genomforandeReferens: +this.activatedRoute.snapshot.params['genomforandeReferens'],
};
if ((this.alternativeFormControl.value as string) === ReportType.AVVIKELSE) {
this.postAvvikelse();
} else if ((this.alternativeFormControl.value as string) === ReportType.FRANVARO) {
this.postFranvaro();
}
}
}
get genomforandeReferensSnapshot() {
return +this.activatedRoute.snapshot.params['genomforandeReferens'];
}
postAvvikelse() {
const avvikelseData: Avvikelse = {
genomforandeReferens: this.genomforandeReferensSnapshot,
avvikelseAlternativ: this.avvikelse,
};
this.deltagareAvvikelseService
.createAvvikelse$(avvikelseData)
.then(() => {
this._submittedDate$.next(new Date());
this.avvikelseFormGroup.reset();
this.currentStep = 3;
})
.catch((error: Error) => {
this._error$.next(new CustomError({ error, message: error.message, type: ErrorType.API }));
});
}
postFranvaro() {
const avvikelseData: Avvikelse = {
genomforandeReferens: this.genomforandeReferensSnapshot,
franvaro: this.franvaro,
};
this.deltagareAvvikelseService
.createFranvaro$(avvikelseData)
.then(() => {
this._submittedDate$.next(new Date());
this.avvikelseFormGroup.reset();
this.currentStep = 3;
})
.catch((error: Error) => {
this._error$.next(new CustomError({ error, message: error.message, type: ErrorType.API }));
});
}
setAlternative(): void {
if ((this.alternativeFormControl.value as string) == ReportType.FRANVARO) {
this.franvaroOrsaker$ = this.deltagareAvvikelseService.getOrsaksKoderFranvaro$.pipe(shareReplay(1));
this.andraKandaOrsaker$ = this.deltagareAvvikelseService.getAndraKandaOrsaker$.pipe(shareReplay(1));
}
if ((this.alternativeFormControl.value as string) == ReportType.AVVIKELSE) {
this.avvikelseOrsaker$ = this.deltagareAvvikelseService.getOrsaksKoderAvvikelse$.pipe(shareReplay(1));
this.fragor1$ = this.deltagareAvvikelseService.fragorForAvvikelser$.pipe(
map((fragor: FragorForAvvikelser[]) => {
return fragor.filter((fraga: FragorForAvvikelser) => fraga.id.includes('_1'));
})
);
this.fragor2$ = this.deltagareAvvikelseService.fragorForAvvikelser$.pipe(
map((fragor: FragorForAvvikelser[]) => {
return fragor.filter((fraga: FragorForAvvikelser) => {
return fraga.id.includes('_2');
});
})
);
}
this.clearControlOnAlternativeChange();
}
setOrsakerChanged(): void {
this.avvikelseFormGroup.markAsUntouched();
if ((this.alternativeFormControl.value as string) === ReportType.AVVIKELSE) {
this.selectedOrsaksKod = this.avvikelseFormGroup.get('orsakerFormGroup').get('orsaker').value as string;
}
this.avvikelseFormGroup
.get('orsakerFormGroup')
.get('orsaker')
.valueChanges.subscribe(value => {
if (value !== null) {
this.avvikelseFormGroup.get('orsakerFormGroup').get('andraKandaOrsaker').reset();
}
});
}
setDayOrPartOfDayChanged(): void {
if (this.dayOrPartOfDayFormControl.value === DayOrPartOfDay.HELDAG) {
this.startTimeFormControl.reset();
this.endTimeFormControl.reset();
}
}
setMinDate(startdatumAvrop: Date): Date {
return new Date(startdatumAvrop);
}
get setMaxDate(): Date {
return new Date();
}
previousStep(): void {
if (this.currentStep > 1) {
this.currentStep--;
this._submittedDate$.next(null);
this._showDangerNotification$.next(false);
}
}
private clearControlOnAlternativeChange(): void {
this.descriptionFormControl.setValue('');
this.fraga1FormControl.setValue('');
this.fraga2FormControl.setValue('');
this.dayOrPartOfDayFormControl.reset();
this.avvikelseFormGroup?.get('timepickerFormGroup').reset();
}
}

View File

@@ -1,76 +0,0 @@
import { Injectable } from '@angular/core';
import { FranvaroOrsaksKodEnum } from '@msfa-enums/franvaro-orsak-kod.enum';
import { Avvikelse } from '@msfa-models/avvikelse.model';
import { FragorForAvvikelser } from '@msfa-models/fragor-for-avvikelser.model';
import { OrsaksKoderAvvikelse } from '@msfa-models/orsaks-koder-avvikelse.model';
import { KandaAvvikelseKoder, OrsaksKoderFranvaro } from '@msfa-models/orsaks-koder-franvaro.model';
import { AvvikelseApiService } from '@msfa-services/api/avvikelse-api.service';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
@Injectable()
export class DeltagareAvvikelseService {
public fragorForAvvikelser$: Observable<
FragorForAvvikelser[]
> = this.avvikelseApiService.getFragorForAvvikelser$().pipe(shareReplay(1));
public getOrsaksKoderFranvaro$: Observable<
OrsaksKoderFranvaro[]
> = this.avvikelseApiService.getOrsaksKoderFranvaro$().pipe(
map((orsaksKoder: OrsaksKoderFranvaro[]) => {
orsaksKoder.find(kod => {
if (kod.value === FranvaroOrsaksKodEnum.VAB) {
kod.name = 'Vård av barn';
}
});
return this.sortOrsaksKoder(orsaksKoder);
}),
shareReplay(1)
);
constructor(private avvikelseApiService: AvvikelseApiService, private deltagareApiService: DeltagareApiService) {}
public getOrsaksKoderAvvikelse$: Observable<
OrsaksKoderAvvikelse[]
> = this.avvikelseApiService.getOrsaksKoderAvvikelse$();
public getAndraKandaOrsaker$: Observable<KandaAvvikelseKoder[]> = this.avvikelseApiService.getAndraKandaOrsaker$();
public createAvvikelse$(avvikelse: Avvikelse): Promise<void> {
return this.avvikelseApiService.createAvvikelse$(avvikelse);
}
public createFranvaro$(avvikelse: Avvikelse): Promise<void> {
return this.avvikelseApiService.createFranvaro$(avvikelse);
}
private sortOrsaksKoder(orsaksKoder: OrsaksKoderFranvaro[]): OrsaksKoderFranvaro[] {
orsaksKoder.map(orsak => {
if (+orsak.value == FranvaroOrsaksKodEnum.Sjuk) {
orsak.index = 1;
}
if (+orsak.value == FranvaroOrsaksKodEnum.Arbete) {
orsak.index = 3;
}
if (+orsak.value == FranvaroOrsaksKodEnum.OkandOrsak) {
orsak.index = 6;
}
if (+orsak.value == FranvaroOrsaksKodEnum.AnnanKandOrsak) {
orsak.index = 5;
}
if (+orsak.value == FranvaroOrsaksKodEnum.VAB) {
orsak.index = 2;
}
if (+orsak.value == FranvaroOrsaksKodEnum.Utbildning) {
orsak.index = 4;
}
});
const sortedOrsaksKoder = orsaksKoder.sort((kodA, kodB) => (kodA.index < kodB.index ? -1 : 1));
return sortedOrsaksKoder;
}
fetchAvropInformation$(genomforandeReferens: number) {
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
}
}

View File

@@ -0,0 +1,124 @@
<msfa-layout>
<msfa-report-layout
[avrop]="avrop$ | async"
description="Här rapporterar du deltagarens frånvaro och eventuella misskötsel i tjänsten. Rapportering via avvikelserapport ska också ske om tjänsten inte fungerar för deltagaren."
reportSubTitle="Skapa rapport"
reportTitle="Avvikelserapport (avvikelse)"
*ngIf="avrop$ | async as avrop; else skeletonRef"
>
<div *ngIf="submittedDate$ | async as submittedDate; else formRef" class="deltagare-avvikelse__confirmation">
<digi-notification-alert
af-heading="Allt gick bra"
af-heading-level="h3"
af-variation="success"
class="deltagare-avvikelse__alert"
>
<p>
Avvikelserapport för deltagare {{avrop.fullName}} är nu inskickad till Arbetsförmedlingen och inväntar
godkännande.
</p>
<dl>
<dt>Datum</dt>
<dd>{{submittedDate | date:'longDate'}} kl {{submittedDate | date:'shortTime'}}</dd>
</dl>
</digi-notification-alert>
<msfa-back-link [route]="['../']">Tillbaka till deltagaren</msfa-back-link>
</div>
<ng-template #formRef>
<form [formGroup]="avvikelseFormGroup" (ngSubmit)="openConfirmDialog()">
<div class="orsaks-form__content">
<digi-ng-form-select
*ngIf="reasonsAsNgDigiFormSelectItems$ | async; let reason; else loadingRef"
[formControlName]="reasonFormName"
[afLabel]=" 'Orsak till avvikelse'"
[afPlaceholder]="'Välj orsak till avvikelse'"
[afSelectItems]="reason"
[afDisableValidStyle]="true"
[afInvalid]="formControlIsInvalid(reasonFormControl)"
></digi-ng-form-select>
<digi-form-validation-message *ngIf="formControlIsInvalid(reasonFormControl)" af-variation="error"
>{{reasonFormControl.errors?.message}}
</digi-form-validation-message>
</div>
<div [formArrayName]="questionsFormName">
<ng-container *ngIf="questionsForChosenReason$ | async; let questions">
<div *ngFor="let question of questionsFormArray.controls; let i=index">
<div class="fragor-form__content">
<digi-ng-form-textarea
[formControlName]="i"
[afLabel]="questions[i]?.name"
[afDisableValidStyle]="true"
[afRequired]="questionIsRequired(questions[i])"
[afMaxLength]="2000"
[afInvalid]="formControlIsInvalid(question)"
></digi-ng-form-textarea>
<digi-form-validation-message *ngIf="formControlIsInvalid(question)" af-variation="error"
>{{question.errors?.message}}
</digi-form-validation-message>
</div>
</div>
</ng-container>
</div>
<ng-container *ngIf="chosenReasonId$ | async">
<digi-ng-form-datepicker
[afDisableValidStyle]="true"
[afMinDate]="minDate(avrop)"
[afMaxDate]="maxDate"
[afInvalid]="formControlIsInvalid(avvikelseDateFormControl)"
[afLabel]="'Välj dag för avvikelse'"
[formControlName]="reportingDateFormName"
></digi-ng-form-datepicker>
<!-- NOTE: Other errors (such as formatting) are captured and displayed within digi-ng-form-datepicker -->
<digi-form-validation-message
*ngIf="avvikelseDateFormControl?.errors?.type === 'required'"
af-variation="error"
>{{avvikelseDateFormControl.errors?.message}}
</digi-form-validation-message>
</ng-container>
<div class="deltagare-avvikelse__cta-wrapper">
<digi-button af-type="submit" af-size="m">Förhandsgranska</digi-button>
<msfa-back-link [showIcon]="false" [asButton]="true" [route]="['../']">Avbryt</msfa-back-link>
</div>
</form>
</ng-template>
</msfa-report-layout>
<digi-ng-dialog
[afActive]="confirmDialogIsOpen$ | async"
(afOnPrimaryClick)="submitAndCloseConfirmDialog()"
(afOnInactive)="cancelConfirmDialog()"
afHeadingLevel="h2"
[afPrimaryButtonText]="'Skicka in Avvikelserapport (avvikelse)'"
[afSecondaryButtonText]="'Avbryt'"
(afOnSecondaryClick)="cancelConfirmDialog()"
[afHeading]="'Bekräfta och skicka in'"
id="confirmAvvikelserapport"
>
<dl>
<dt>Orsak till avvikelse:</dt>
<dd>{{(chosenReason$ | async)?.name }}</dd>
<ng-container *ngIf="avvikelseSubmitData$ | async; let avvikelseSubmitData; else loadingRef">
<ng-container *ngFor="let question of avvikelseSubmitData.avvikelseAlternativ.frageformular">
<dt>{{getCurrentQuestionFromId(question.fraga).name}}</dt>
<dd>{{question.svar.length === 0 ? 'Inget svar' : question.svar }}</dd>
</ng-container>
<dt>Dag för avvikelse:</dt>
<dd>{{avvikelseSubmitData.avvikelseAlternativ.rapporteringsdatum }}</dd>
</ng-container>
</dl>
<msfa-loader *ngIf="submitIsLoading$ | async"></msfa-loader>
</digi-ng-dialog>
</msfa-layout>
<ng-template #skeletonRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar data för avvikelserapport"></digi-ng-skeleton-base>
</ng-template>
<ng-template #loadingRef>
<msfa-loader type="padded"></msfa-loader>
</ng-template>

View File

@@ -36,4 +36,26 @@
&__alert {
max-width: var(--digi--typography--text--max-width);
}
&__cta-wrapper {
display: flex;
gap: var(--digi--layout--gutter);
}
}
.fragor-form {
&__content {
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
}
.orsaks-form {
&__content {
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}
}

View File

@@ -6,17 +6,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { DeltagareAvvikelseComponent } from './deltagare-avvikelse.component';
import { DeltagareAvvikelseService } from './deltagare-avvikelse.service';
import { DeltagareAvvikelserapportComponent } from './deltagare-avvikelserapport.component';
import { DeltagareAvvikelserapportService } from './deltagare-avvikelserapport.service';
describe('DeltagareAvvikelseComponent', () => {
let component: DeltagareAvvikelseComponent;
let fixture: ComponentFixture<DeltagareAvvikelseComponent>;
let component: DeltagareAvvikelserapportComponent;
let fixture: ComponentFixture<DeltagareAvvikelserapportComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareAvvikelseComponent, LayoutComponent],
declarations: [DeltagareAvvikelserapportComponent, LayoutComponent],
imports: [
RouterTestingModule,
HttpClientTestingModule,
@@ -24,12 +24,12 @@ describe('DeltagareAvvikelseComponent', () => {
DigiNgFormRadiobuttonGroupModule,
DigiNgFormDatepickerModule,
],
providers: [DeltagareAvvikelseService],
providers: [DeltagareAvvikelserapportService],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeltagareAvvikelseComponent);
fixture = TestBed.createComponent(DeltagareAvvikelserapportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,205 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { AvvikelseAlternativ, AvvikelseRequestData } from '@msfa-models/avvikelse.model';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
import { DeltagareAvvikelserapportService } from './deltagare-avvikelserapport.service';
import { Avrop } from '@msfa-models/avrop.model';
import { FragorForAvvikelser } from '@msfa-models/fragor-for-avvikelser.model';
import { RequiredValidator } from '@msfa-validators/required.validator';
import { FormSelectItem } from '@af/digi-ng/_form/form-select';
import { OrsaksKoderAvvikelse } from '@msfa-models/orsaks-koder-avvikelse.model';
import { CustomError } from '@msfa-models/error/custom-error';
interface Params {
genomforandeReferens: string;
}
interface AvvikelseFormData {
reason: string;
questions: string[];
reportingDate: string;
}
type AvvikelseFormKeys = keyof AvvikelseFormData;
@Component({
selector: 'msfa-deltagare-avvikelse',
templateUrl: './deltagare-avvikelserapport.component.html',
styleUrls: ['./deltagare-avvikelserapport.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareAvvikelserapportComponent implements OnInit, OnDestroy {
shouldValidate$ = new BehaviorSubject<boolean>(false);
reasonFormName: AvvikelseFormKeys = 'reason';
questionsFormName: AvvikelseFormKeys = 'questions';
reportingDateFormName: AvvikelseFormKeys = 'reportingDate';
submitIsLoading$ = new BehaviorSubject<boolean>(false);
genomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
map((params: Params) => +params.genomforandeReferens)
);
avrop$: Observable<Avrop> = this.genomforandeReferens$.pipe(
switchMap(genomforandeReferens => this.deltagareAvvikelseService.fetchAvropInformation$(genomforandeReferens)),
shareReplay(1)
);
reasons$ = this.deltagareAvvikelseService.getAvvikelseOrsaker$;
reasonsAsNgDigiFormSelectItems$: Observable<FormSelectItem[]> = this.reasons$.pipe(
map(reasons => reasons.map(reason => ({ name: reason.name, value: reason.id })))
);
allAvvikelseQuestions$ = this.deltagareAvvikelseService.fragorForAvvikelser$;
chosenReasonId$: Observable<string>;
chosenReason$: Observable<OrsaksKoderAvvikelse>;
questionsForChosenReason$: Observable<FragorForAvvikelser[]>;
avvikelseSubmitData$: Observable<AvvikelseRequestData>;
confirmDialogIsOpen$ = new BehaviorSubject<boolean>(false);
submittedDate$ = new BehaviorSubject<Date | null>(null);
private subscriptions: Subscription[] = [];
private todayDateISO = new Date().toISOString().slice(0, 10);
avvikelseFormGroup = new FormGroup({
[this.reasonFormName]: new FormControl(null, RequiredValidator('En orsak')), //[this.orsakFormControlName]:
[this.reportingDateFormName]: new FormControl(this.todayDateISO, RequiredValidator('Datum')),
[this.questionsFormName]: new FormArray([]),
});
private formData$: Observable<AvvikelseFormData> = this.avvikelseFormGroup
.valueChanges as Observable<AvvikelseFormData>;
private currentQuestions: FragorForAvvikelser[];
constructor(
private deltagareAvvikelseService: DeltagareAvvikelserapportService,
private deltagareApiService: DeltagareApiService,
private activatedRoute: ActivatedRoute
) {}
get reasonFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup.get(this.reasonFormName);
}
get avvikelseDateFormControl(): AbstractControl | undefined {
return this.avvikelseFormGroup.get(this.reportingDateFormName);
}
get questionsFormArray(): FormArray {
return this.avvikelseFormGroup.get('questions') as FormArray;
}
get maxDate(): Date {
return new Date();
}
getCurrentQuestionFromId(id: string): FragorForAvvikelser {
return this.currentQuestions.find(currentQuestions => currentQuestions.id === id);
}
ngOnInit(): void {
this.chosenReasonId$ = this.reasonFormControl.valueChanges as Observable<string>;
this.chosenReason$ = combineLatest([this.chosenReasonId$, this.reasons$]).pipe(
map(([chosenReasonId, reasons]) => reasons.find(reason => reason.id === chosenReasonId))
);
this.questionsForChosenReason$ = combineLatest([this.chosenReasonId$, this.allAvvikelseQuestions$]).pipe(
map(([chosenOrsak, allAvvikelseQuestions]) => {
return allAvvikelseQuestions.filter(question => question.id.startsWith(chosenOrsak.toString() + '_'));
})
);
this.subscriptions.push(
this.chosenReason$.subscribe(() => {
this.shouldValidate$.next(false);
}),
this.questionsForChosenReason$.subscribe(questions => {
this.clearQuestions();
questions.forEach(question => this.addQuestionToForm(question));
})
);
this.avvikelseSubmitData$ = combineLatest([this.genomforandeReferens$, this.chosenReasonId$, this.formData$]).pipe(
map(([genomforandeReferens, chosenReason, formData]) =>
this.makeAvvikelseSubmitData(genomforandeReferens, chosenReason, formData)
),
shareReplay(1)
);
}
questionIsRequired(question: FragorForAvvikelser): boolean {
return !(question.id === '19_2' || question.id === '20_2');
}
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate$.value);
}
minDate(avrop: Avrop): Date {
return new Date(avrop.recievedTimestamp);
}
openConfirmDialog(): void {
this.shouldValidate$.next(true);
if (this.avvikelseFormGroup.valid) {
this.confirmDialogIsOpen$.next(true);
}
}
submitAndCloseConfirmDialog(): void {
this.submitIsLoading$.next(true);
this.avvikelseSubmitData$.pipe(take(1)).subscribe(avvikelseSubmitData =>
this.deltagareAvvikelseService.createAvvikelse$(avvikelseSubmitData).subscribe({
next: () => {
this.submitIsLoading$.next(false);
this.submittedDate$.next(new Date());
this.confirmDialogIsOpen$.next(false);
},
error: error => {
this.submitIsLoading$.next(false);
throw new CustomError(error);
},
})
);
}
cancelConfirmDialog(): void {
this.confirmDialogIsOpen$.next(false);
}
ngOnDestroy(): void {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
}
private makeAvvikelseSubmitData(
genomforandeReferens: number,
chosenReason: string,
formData: AvvikelseFormData
): AvvikelseRequestData {
const avvikelseAlternativ: AvvikelseAlternativ = {
avvikelseorsakskod: chosenReason,
frageformular: formData.questions.map((question, index) => ({
fraga: this.currentQuestions[index].id,
svar: question,
})),
rapporteringsdatum: formData.reportingDate,
};
return { genomforandeReferens, avvikelseAlternativ };
}
private clearQuestions(): void {
this.questionsFormArray.clear();
this.currentQuestions = [];
}
private addQuestionToForm(question: FragorForAvvikelser): void {
// FormArray doesnt hold any IDs so we need to store these seperately and rebuild structure at submit
this.currentQuestions.push(question);
this.questionsFormArray.push(
new FormControl('', this.questionIsRequired(question) ? RequiredValidator('Frågan') : null)
);
}
}

View File

@@ -9,37 +9,37 @@ import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { ConfirmDialogModule } from '@msfa-shared/components/confirm-dialog/confirm-dialog.module';
import { ReportLayoutModule } from '../components/report-layout/report-layout.module';
import { DeltagareConfirmFormModule } from './components/deltagare-confirm-form/deltagare-confirm-form.module';
import { DeltagareFragorFormModule } from './components/deltagare-fragor-form/deltagare-fragor-form.module';
import { DeltagareOrsaksFormModule } from './components/deltagare-orsaks-form/deltagare-orsaks-form.module';
import { DeltagareTimePickerModule } from './components/deltagare-time-picker/deltagare-time-picker.module';
import { DeltagareAvvikelseComponent } from './deltagare-avvikelse.component';
import { DeltagareAvvikelseService } from './deltagare-avvikelse.service';
import { DeltagareAvvikelserapportComponent } from './deltagare-avvikelserapport.component';
import { DeltagareAvvikelserapportService } from './deltagare-avvikelserapport.service';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { LoaderModule } from '@msfa-shared/components/loader/loader.module';
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [DeltagareAvvikelseComponent],
declarations: [DeltagareAvvikelserapportComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: DeltagareAvvikelseComponent }]),
RouterModule.forChild([{ path: '', component: DeltagareAvvikelserapportComponent }]),
LayoutModule,
ReactiveFormsModule,
DigiNgFormRadiobuttonGroupModule,
DigiNgFormDatepickerModule,
DigiNgFormTextareaModule,
DigiNgProgressProgressbarModule,
DeltagareOrsaksFormModule,
DeltagareFragorFormModule,
DeltagareTimePickerModule,
DeltagareConfirmFormModule,
ReportLayoutModule,
ConfirmDialogModule,
BackLinkModule,
DigiNgSkeletonBaseModule,
DigiNgFormSelectModule,
LoaderModule,
DigiNgFormInputModule,
DigiNgDialogModule,
],
providers: [DeltagareAvvikelseService],
exports: [DeltagareAvvikelseComponent],
providers: [DeltagareAvvikelserapportService],
exports: [DeltagareAvvikelserapportComponent],
})
export class DeltagareAvvikelseModule {}
export class DeltagareAvvikelserapportModule {}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { Avvikelse } from '@msfa-models/avvikelse.model';
import { FragorForAvvikelser } from '@msfa-models/fragor-for-avvikelser.model';
import { OrsaksKoderAvvikelse } from '@msfa-models/orsaks-koder-avvikelse.model';
import { AvvikelseApiService } from '@msfa-services/api/avvikelse-api.service';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
@Injectable()
export class DeltagareAvvikelserapportService {
fragorForAvvikelser$: Observable<FragorForAvvikelser[]> = this.avvikelseApiService
.getFragorForAvvikelser$()
.pipe(shareReplay(1));
getAvvikelseOrsaker$: Observable<OrsaksKoderAvvikelse[]> = this.avvikelseApiService.getOrsaksKoderAvvikelse$();
constructor(private avvikelseApiService: AvvikelseApiService, private deltagareApiService: DeltagareApiService) {}
createAvvikelse$(avvikelse: Avvikelse): Observable<unknown> {
return this.avvikelseApiService.createAvvikelse$(avvikelse);
}
fetchAvropInformation$(genomforandeReferens: number) {
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
}
}

View File

@@ -1,18 +1,6 @@
import { ReportType } from '@msfa-enums/report-type.enum';
import { DayOrPartOfDay } from '@msfa-enums/day-or-part-of-day.enum';
import { RadiobuttonModel } from '@af/digi-ng/_form/form-radiobutton-group';
export const avvikelseAlternatives: RadiobuttonModel[] = [
{
label: 'Frånvaro',
value: ReportType.FRANVARO,
},
{
label: 'Avvikelse',
value: ReportType.AVVIKELSE,
},
];
export const dayOrPartOfDay: RadiobuttonModel[] = [
{
label: 'Heldag',

View File

@@ -2,10 +2,7 @@
<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">
@@ -35,66 +32,44 @@
id="gemensam-planering-form"
>
<msfa-loader *ngIf="submitLoading$ | async" type="absolute"></msfa-loader>
<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>
<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 *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">
<digi-notification-alert
*ngIf="error$ | async as error"
@@ -106,29 +81,37 @@
<p class="msfa__small-text" *ngIf="error.message">{{error.message}}</p>
</digi-notification-alert>
<div class="gemensam-planering__cta-wrapper">
<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>
<digi-button af-type="submit" af-size="m">Bekräfta och skicka in</digi-button>
<msfa-back-link [showIcon]="false" [asButton]="true" [route]="['/deltagare/'+ genomforandeReferens]"
>Avbryt</msfa-back-link
>
</div>
</footer>
</form>
<msfa-confirm-dialog
[openConfirmDialog]="confirmDialogOpen"
reportToConfirm="gemensam planering"
[dialogOpen]="confirmDialogOpen"
dialogTitle="Förhandsgranska och bekräfta"
ariaLabel="Förhandsgranska och bekräfta Avvikelserapport (frånvaro)"
primaryButtonText="Bekräfta och skicka in"
(confirmDialogChanged)="closeConfirmDialogAndProceed($event, genomforandeReferens)"
></msfa-confirm-dialog>
>
<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>
</msfa-confirm-dialog>
</ng-template>
</div>
</msfa-report-layout>

View File

@@ -2,6 +2,8 @@
@import 'variables/gutters';
.gemensam-planering {
max-width: var(--digi--typography--text--max-width);
&__confirmation,
&__form {
position: relative;
@@ -26,10 +28,6 @@
color: var(--digi--ui--color--border--success);
}
&__alert {
max-width: var(--digi--typography--text--max-width);
}
&__footer {
display: flex;
flex-direction: column;
@@ -40,15 +38,4 @@
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

@@ -23,8 +23,6 @@ import { GemensamPlaneringValidator } from './gemensam-planering.validator';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeltagareGemensamPlaneringComponent {
totalAmountOfSteps = 2;
currentStep = 1;
obligatoryActivityIds = [165, 188];
shouldValidate = false;
RadiobuttonGroupDirection = RadiobuttonGroupDirection;
@@ -109,22 +107,12 @@ export class DeltagareGemensamPlaneringComponent {
}
}
goToPreview(): void {
openConfirmDialog(): 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;
}
@@ -144,7 +132,6 @@ export class DeltagareGemensamPlaneringComponent {
.postGemensamPlanering(mapGemensamPlaneringToGemensamPlaneringPostRequest(postRequest))
.then(() => {
this._lastSubmittedGP$.next(new Date());
this.currentStep = 3;
})
.catch((error: Error) => {
this._error$.next(new CustomError({ error, message: error.message, type: ErrorType.API }));

View File

@@ -0,0 +1,313 @@
<msfa-layout>
<msfa-report-layout
*ngIf="avrop$ | async as avrop; else skeletonRef"
[avrop]="avrop"
description="Här rapporterar du deltagarens frånvaro i tjänsten."
reportTitle="Avvikelserapport (frånvaro)"
>
<div class="franvaro-report" *ngIf="currentGenomforandeReferens$ | async as genomforandeReferens">
<digi-notification-alert
*ngIf="maxDate < avrop.startDate; else reportRef"
af-variation="warning"
af-heading="Kan inte skapa Avvikelserapport (frånvaro)"
>
<p>Det går inte att rapportera frånvaro eftersom tjänsten inte har startad ännu.</p>
</digi-notification-alert>
<ng-template #reportRef>
<div
class="franvaro-report__confirmation"
*ngIf="lastSubmittedFranvaroReport$ | async as lastSubmittedFranvaroReport; else formRef"
>
<digi-notification-alert
class="franvaro-report__alert"
af-variation="success"
af-heading="Allt gick bra"
af-heading-level="h3"
>
<p>Avvikelserapport (frånvaro) för deltagare {{avrop.fullName}} är nu inskickad till Arbetsförmedlingen.</p>
<dl>
<dt>Datum</dt>
<dd>
{{lastSubmittedFranvaroReport | date:'longDate'}} kl {{lastSubmittedFranvaroReport | date:'shortTime'}}
</dd>
</dl>
</digi-notification-alert>
<msfa-back-link [route]="['/deltagare/'+ genomforandeReferens]">Tillbaka till deltagaren</msfa-back-link>
</div>
<ng-template #formRef>
<form
class="franvaro-report__form"
[formGroup]="franvaroFormGroup"
(ngSubmit)="openConfirmDialog()"
id="franvaro-report-form"
>
<msfa-loader *ngIf="submitLoading$ | async" type="absolute"></msfa-loader>
<div class="franvaro-report__form-item">
<digi-ng-form-select
*ngIf="reasons$ | async as reasons; else loadingRef"
[formControl]="reasonFormControl"
afLabel="Orsak till frånvaro"
afPlaceholder="Välj orsak till frånvaro"
[afSelectItems]="reasons"
[afRequired]="true"
[afDisableValidStyle]="true"
[afInvalid]="formControlIsInvalid(['reason'])"
></digi-ng-form-select>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['reason'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.reason"
></digi-ng-form-validation-message>
</div>
</div>
<ng-container *ngIf="reasonFormControl.value">
<ng-container *ngIf="showOtherKnownReasonsSelect">
<div
class="franvaro-report__form-item"
*ngIf="otherKnownReasons$ | async as otherKnownReasons; else loadingRef"
>
<digi-ng-form-select
[formControl]="otherKnownReasonFormControl"
afLabel="Känd orsak"
afPlaceholder="Välj känd orsak"
[afSelectItems]="otherKnownReasons"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="formControlIsInvalid(['otherKnownReason'])"
></digi-ng-form-select>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['otherKnownReason'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.otherKnownReason"
></digi-ng-form-validation-message>
</div>
</div>
<div class="franvaro-report__form-item" *ngIf="showKnownReasonTextArea">
<digi-ng-form-textarea
[afDisableValidStyle]="true"
[afInvalid]="formControlIsInvalid(['knownReasonComment'])"
[afMaxLength]="2000"
[afRequired]="true"
afLabel="Beskriv frånvaro"
afSize="s"
[formControl]="knownReasonCommentFormControl"
></digi-ng-form-textarea>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['knownReasonComment'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.knownReasonComment"
></digi-ng-form-validation-message>
</div>
</div>
</ng-container>
<div class="franvaro-report__form-item">
<digi-ng-form-datepicker
[afDisableValidStyle]="true"
[afMinDate]="avrop.startDate"
[afMaxDate]="maxDate"
[afInvalid]="formControlIsInvalid(['date'])"
[afRequired]="true"
afLabel="Välj dag för frånvaro"
[formControl]="dateFormControl"
></digi-ng-form-datepicker>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['date'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.date"
></digi-ng-form-validation-message>
</div>
</div>
<digi-form-fieldset af-legend="Hel eller del av dag" af-name="wholeDay" af-form="franvaro-report-form">
<digi-ng-form-radiobutton-group
[afRadiobuttons]="wholeDayOrPartOfDayRadiobuttons"
[afRequired]="true"
[formControl]="wholeDayFormControl"
></digi-ng-form-radiobutton-group>
</digi-form-fieldset>
<digi-form-fieldset
*ngIf="showTimePickers"
af-legend="Välj tid för frånvaro"
af-name="time"
af-form="franvaro-report-form"
>
<div class="franvaro-report__time-pickers">
<div class="franvaro-report__time-picker">
<digi-ng-form-input
afLabel="Ange starttid"
[formControl]="startTimeFormControl"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="formControlIsInvalid(['startTime', 'expectedEndTimeIsBeforeStartTime'])"
afType="time"
></digi-ng-form-input>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['startTime'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.startTime"
></digi-ng-form-validation-message>
</div>
</div>
<div class="franvaro-report__time-picker">
<digi-ng-form-input
afLabel="Ange sluttid"
[formControl]="endTimeFormControl"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="formControlIsInvalid(['endTime', 'expectedEndTimeIsBeforeStartTime'])"
afType="time"
></digi-ng-form-input>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['endTime'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.endTime"
></digi-ng-form-validation-message>
</div>
</div>
</div>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['expectedEndTimeIsBeforeStartTime'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.expectedEndTimeIsBeforeStartTime"
></digi-ng-form-validation-message>
</div>
</digi-form-fieldset>
<digi-form-fieldset
af-legend="Välj tid förväntad närvaro"
af-name="expectedPresence"
af-form="franvaro-report-form"
>
<div class="franvaro-report__time-pickers">
<div class="franvaro-report__time-picker">
<digi-ng-form-input
afLabel="Ange starttid"
[formControl]="expectedPresenceStartTimeFormControl"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="formControlIsInvalid(['expectedPresenceStartTime', 'expectedPresenceEndTimeIsBeforeStartTime'])"
afType="time"
></digi-ng-form-input>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['expectedPresenceStartTime'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.expectedPresenceStartTime"
></digi-ng-form-validation-message>
</div>
</div>
<div class="franvaro-report__time-picker">
<digi-ng-form-input
afLabel="Ange sluttid"
[formControl]="expectedPresenceEndTimeFormControl"
[afDisableValidStyle]="true"
[afRequired]="true"
[afInvalid]="formControlIsInvalid(['expectedPresenceEndTime', 'expectedPresenceEndTimeIsBeforeStartTime'])"
afType="time"
></digi-ng-form-input>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['expectedPresenceEndTime'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.expectedPresenceEndTime"
></digi-ng-form-validation-message>
</div>
</div>
</div>
<div aria-atomic="true" role="alert">
<digi-ng-form-validation-message
*ngIf="formControlIsInvalid(['expectedPresenceEndTimeIsBeforeStartTime'])"
class="franvaro-report__validation-message"
[afPositive]="false"
[afValidationText]="formErrors.expectedPresenceEndTimeIsBeforeStartTime"
></digi-ng-form-validation-message>
</div>
</digi-form-fieldset>
</ng-container>
<footer class="franvaro-report__footer">
<digi-notification-alert
*ngIf="error$ | async as error"
class="franvaro-report__alert"
af-variation="danger"
af-heading="Någonting gick fel"
>
<p>Kunde inte spara Avvikelserapport (frånvaro). Ladda om sidan och försök igen.</p>
<p class="msfa__small-text" *ngIf="error.message">{{error.message}}</p>
</digi-notification-alert>
<div class="franvaro-report__cta-wrapper">
<digi-button af-type="submit" af-size="m">Förhandsgranska</digi-button>
<msfa-back-link [showIcon]="false" [asButton]="true" [route]="['/deltagare/'+ genomforandeReferens]"
>Avbryt</msfa-back-link
>
</div>
</footer>
</form>
<msfa-confirm-dialog
[dialogOpen]="confirmDialogOpen$ | async"
dialogTitle="Förhandsgranska och bekräfta"
ariaLabel="Förhandsgranska och bekräfta Avvikelserapport (frånvaro)"
primaryButtonText="Bekräfta och skicka in"
(confirmDialogChanged)="closeConfirmDialogAndProceed($event, genomforandeReferens)"
>
<dl *ngIf="reasons$ | async as reasons">
<ng-container *ngIf="reasons$ | async as reasons">
<dt>Orsak till frånvaro</dt>
<dd>{{getReasonNameFromValue(reasons, reasonFormControl.value)}}</dd>
</ng-container>
<ng-container *ngIf="showOtherKnownReasonsSelect">
<ng-container *ngIf="otherKnownReasons$ | async as otherKnownReasons">
<dt>Annan känd orsak</dt>
<dd>{{getReasonNameFromValue(otherKnownReasons, otherKnownReasonFormControl.value)}}</dd>
</ng-container>
<ng-container *ngIf="showKnownReasonTextArea">
<dt>Beskrivning för frånvaro</dt>
<dd>{{knownReasonCommentFormControl.value}}</dd>
</ng-container>
</ng-container>
<dt>Datum</dt>
<dd><digi-typography-time [afDateTime]="dateFormControl.value"></digi-typography-time></dd>
<dt>Hel eller del av dag</dt>
<dd>{{dayOrPartOfDayFromValue}}</dd>
<ng-container *ngIf="showTimePickers">
<dt>Tid för frånvaro</dt>
<dd>{{startTimeFormControl.value}} - {{endTimeFormControl.value}}</dd>
</ng-container>
<dt>Tid för förväntad närvaro</dt>
<dd>{{expectedPresenceStartTimeFormControl.value}} - {{expectedPresenceEndTimeFormControl.value}}</dd>
</dl>
</msfa-confirm-dialog>
</ng-template>
</ng-template>
</div>
</msfa-report-layout>
</msfa-layout>
<ng-template #skeletonRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar data för Avvikelserapport (frånvaro)"></digi-ng-skeleton-base>
</ng-template>
<ng-template #loadingRef>
<msfa-loader type="padded"></msfa-loader>
</ng-template>

View File

@@ -0,0 +1,39 @@
@import 'variables/gutters';
@import 'variables/z-index';
.franvaro-report {
max-width: var(--digi--typography--text--max-width);
&__confirmation,
&__form {
position: relative;
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
z-index: $msfa__z-index-default;
}
&__time-pickers {
display: flex;
gap: var(--digi--layout--gutter);
}
&__time-picker {
flex-grow: 1;
}
&__footer {
display: flex;
flex-direction: column;
gap: var(--digi--layout--gutter);
}
&__cta-wrapper {
display: flex;
gap: var(--digi--layout--gutter);
}
&__validation-message {
margin-top: var(--digi--layout--gutter--s);
}
}

View File

@@ -0,0 +1,29 @@
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 { FranvaroReportComponent } from './franvaro-report.component';
describe('FranvaroReportComponent', () => {
let component: FranvaroReportComponent;
let fixture: ComponentFixture<FranvaroReportComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [FranvaroReportComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FranvaroReportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,187 @@
import { RadiobuttonModel } from '@af/digi-ng/_form/form-radiobutton-group';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { ANNAN_KAND_ORSAK_ID, ANNAN_ORSAK_ID } from '@msfa-constants/franvaro-reasons';
import { ConfirmDialog } from '@msfa-enums/confirm-dialog.enum';
import { ErrorType } from '@msfa-enums/error-type.enum';
import { Avrop } from '@msfa-models/avrop.model';
import { FranvaroRequestData } from '@msfa-models/avvikelse.model';
import { CustomError } from '@msfa-models/error/custom-error';
import { Franvaro } from '@msfa-models/franvaro.model';
import { OrsaksKoderFranvaro } from '@msfa-models/orsaks-koder-franvaro.model';
import { dateToIsoString } from '@msfa-utils/format-to-date.util';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, shareReplay, switchMap } from 'rxjs/operators';
import { FranvaroReportService } from './franvaro-report.service';
import { FranvaroReportValidator } from './franvaro-report.validator';
@Component({
selector: 'msfa-franvaro-report',
templateUrl: './franvaro-report.component.html',
styleUrls: ['./franvaro-report.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FranvaroReportComponent {
maxDate = new Date();
shouldValidate$ = new BehaviorSubject<boolean>(false);
confirmDialogOpen$ = new BehaviorSubject<boolean>(false);
franvaroFormGroup = new FormGroup(
{
reason: new FormControl(null),
otherKnownReason: new FormControl(null),
knownReasonComment: new FormControl(''),
date: new FormControl(new Date()),
wholeDay: new FormControl(true),
startTime: new FormControl(null),
endTime: new FormControl(null),
expectedPresenceStartTime: new FormControl(null),
expectedPresenceEndTime: new FormControl(null),
},
[FranvaroReportValidator.isFranvaroReportValid()]
);
error$ = new BehaviorSubject<CustomError>(null);
submitLoading$ = new BehaviorSubject<boolean>(false);
lastSubmittedFranvaroReport$ = new BehaviorSubject<Date>(null);
currentGenomforandeReferens$: Observable<string> = this.activatedRoute.params.pipe(
map(params => params.genomforandeReferens as string)
);
avrop$: Observable<Avrop> = this.currentGenomforandeReferens$.pipe(
switchMap(genomforandeReferens => this.franvaroReportService.fetchAvropInformation$(+genomforandeReferens)),
shareReplay(1)
);
reasons$: Observable<OrsaksKoderFranvaro[]> = this.franvaroReportService.reasons$;
otherKnownReasons$: Observable<OrsaksKoderFranvaro[]> = this.franvaroReportService.otherKnownReasons$;
wholeDayOrPartOfDayRadiobuttons: RadiobuttonModel[] = [
{ label: 'Heldag', value: true },
{ label: 'Del av dag', value: false },
];
constructor(private franvaroReportService: FranvaroReportService, private activatedRoute: ActivatedRoute) {}
get showOtherKnownReasonsSelect(): boolean {
return this.reasonFormControl.value === ANNAN_KAND_ORSAK_ID;
}
get showKnownReasonTextArea(): boolean {
return this.otherKnownReasonFormControl.value === ANNAN_ORSAK_ID;
}
get showTimePickers(): boolean {
return !this.wholeDayFormControl.value;
}
get formErrors(): { [key: string]: string } {
return this.franvaroFormGroup.errors || {};
}
get reasonFormControl(): FormControl {
return this.franvaroFormGroup.get('reason') as FormControl;
}
get dateFormControl(): FormControl {
return this.franvaroFormGroup.get('date') as FormControl;
}
get wholeDayFormControl(): FormControl {
return this.franvaroFormGroup.get('wholeDay') as FormControl;
}
get startTimeFormControl(): FormControl {
return this.franvaroFormGroup.get('startTime') as FormControl;
}
get endTimeFormControl(): FormControl {
return this.franvaroFormGroup.get('endTime') as FormControl;
}
get expectedPresenceStartTimeFormControl(): FormControl {
return this.franvaroFormGroup.get('expectedPresenceStartTime') as FormControl;
}
get expectedPresenceEndTimeFormControl(): FormControl {
return this.franvaroFormGroup.get('expectedPresenceEndTime') as FormControl;
}
get otherKnownReasonFormControl(): FormControl {
return this.franvaroFormGroup.get('otherKnownReason') as FormControl;
}
get knownReasonCommentFormControl(): FormControl {
return this.franvaroFormGroup.get('knownReasonComment') as FormControl;
}
getReasonNameFromValue(reasons: OrsaksKoderFranvaro[], value: string): string {
return reasons.find(reason => reason.value.toString() === value)?.name;
}
get dayOrPartOfDayFromValue(): string {
return this.wholeDayFormControl.value ? 'Heldag' : 'Del av dag';
}
formControlIsInvalid(formControlNames: string[]): boolean {
return (
formControlNames.some(formControlName => this.formErrors[formControlName]) && this.shouldValidate$.getValue()
);
}
openConfirmDialog(): void {
this.shouldValidate$.next(true);
if (this.franvaroFormGroup.invalid) {
return;
}
this.confirmDialogOpen$.next(true);
}
closeConfirmDialogAndProceed(confirmDialogAnswer: ConfirmDialog, genomforandeReferens: number): void {
this.confirmDialogOpen$.next(true);
if (confirmDialogAnswer === ConfirmDialog.ACCEPTED) {
void this.postFranvaroReport(genomforandeReferens);
}
}
async postFranvaroReport(genomforandeReferens: number): Promise<void> {
this.submitLoading$.next(true);
const {
reason,
date,
wholeDay,
startTime,
endTime,
otherKnownReason,
knownReasonComment,
expectedPresenceStartTime,
expectedPresenceEndTime,
} = this.franvaroFormGroup.value as Franvaro;
const postRequest: FranvaroRequestData = {
genomforandeReferens,
franvaro: {
avvikelseOrsaksKod: reason,
datum: dateToIsoString(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
alternativForKandaOrsaker: this.showOtherKnownReasonsSelect
? {
typ: otherKnownReason,
motivering: this.showKnownReasonTextArea ? knownReasonComment : '',
}
: null,
forvantadNarvaro: {
startTid: expectedPresenceStartTime,
slutTid: expectedPresenceEndTime,
},
},
};
return this.franvaroReportService
.postFranvaroReport(postRequest)
.then(() => {
this.lastSubmittedFranvaroReport$.next(new Date());
})
.catch((error: Error) => {
this.error$.next(new CustomError({ error, message: error.message, type: ErrorType.API }));
})
.finally(() => {
this.submitLoading$.next(false);
});
}
}

View File

@@ -0,0 +1,43 @@
import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea';
import { DigiNgFormValidationMessageModule } from '@af/digi-ng/_form/form-validation-message';
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 { 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 { FranvaroReportComponent } from './franvaro-report.component';
import { FranvaroReportService } from './franvaro-report.service';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [FranvaroReportComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: FranvaroReportComponent }]),
ReactiveFormsModule,
LayoutModule,
ReportLayoutModule,
LoaderModule,
BackLinkModule,
ConfirmDialogModule,
DigiNgFormSelectModule,
DigiNgFormDatepickerModule,
DigiNgFormRadiobuttonGroupModule,
DigiNgSkeletonBaseModule,
DigiNgFormTextareaModule,
DigiNgFormInputModule,
DigiNgFormValidationMessageModule,
],
providers: [FranvaroReportService],
exports: [FranvaroReportComponent],
})
export class FranvaroReportModule {}

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { FranvaroRequestData } from '@msfa-models/avvikelse.model';
import { OrsaksKoderFranvaro } from '@msfa-models/orsaks-koder-franvaro.model';
import { FranvaroReportApiService } from '@msfa-services/api/franvaro-report.api.service';
import { Observable } from 'rxjs';
@Injectable()
export class FranvaroReportService {
public reasons$: Observable<OrsaksKoderFranvaro[]> = this.franvaroReportApiService.fetchReasons$();
public otherKnownReasons$: Observable<
OrsaksKoderFranvaro[]
> = this.franvaroReportApiService.fetchOtherKnownReasons$();
constructor(private franvaroReportApiService: FranvaroReportApiService) {}
public fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
return this.franvaroReportApiService.fetchAvropInformation$(genomforandeReferens);
}
public async postFranvaroReport(requestData: FranvaroRequestData): Promise<void> {
return this.franvaroReportApiService.postFranvaroReport$(requestData);
}
}

View File

@@ -0,0 +1,114 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { ANNAN_KAND_ORSAK_ID, ANNAN_ORSAK_ID } from '@msfa-constants/franvaro-reasons';
import { Franvaro } from '@msfa-models/franvaro.model';
const TIME_REGEX = /^([0-1]?[0-9]|2[0-4]):([0-5][0-9])(:[0-5][0-9])?$/;
function isTimeValid(value: string): boolean {
return TIME_REGEX.test(value);
}
export class FranvaroReportValidator {
static isFranvaroReportValid(): ValidatorFn {
return (c: AbstractControl): { [key: string]: string } => {
let errors: { [key: string]: string } = null;
const {
reason,
date,
wholeDay,
startTime,
endTime,
otherKnownReason,
knownReasonComment,
expectedPresenceStartTime,
expectedPresenceEndTime,
} = c.value as Franvaro;
if (!reason) {
errors = {
...errors,
reason: 'Orsak till frånvaro måste väljas',
};
}
if (reason === ANNAN_KAND_ORSAK_ID) {
if (!otherKnownReason) {
errors = {
...errors,
otherKnownReason: 'Känd orsak måste väljas',
};
} else if (otherKnownReason === ANNAN_ORSAK_ID && !knownReasonComment) {
errors = {
...errors,
knownReasonComment: 'Beskrivning av frånvaro är obligatorisk',
};
}
}
if (!date) {
errors = {
...errors,
date: 'Dag för frånvaro måste väljas',
};
}
if (!wholeDay) {
if (!startTime) {
errors = {
...errors,
startTime: 'Starttid för frånvaro måste väljas',
};
} else if (!isTimeValid(startTime)) {
errors = {
...errors,
startTime: 'Felaktig tid för starttid (HH:MM)',
};
}
if (!endTime) {
errors = {
...errors,
endTime: 'Sluttid för frånvaro måste väljas',
};
} else if (!isTimeValid(endTime)) {
errors = {
...errors,
endTime: 'Felaktig tid för sluttid (HH:MM)',
};
}
if (endTime && startTime && endTime < startTime) {
errors = {
...errors,
expectedEndTimeIsBeforeStartTime: 'Sluttid för frånvaro får inte vara före starttid',
};
}
}
if (!expectedPresenceStartTime) {
errors = {
...errors,
expectedPresenceStartTime: 'Starttid för förväntad närvaro måste väljas',
};
} else if (!isTimeValid(expectedPresenceStartTime)) {
errors = {
...errors,
expectedPresenceStartTime: 'Felaktig tid för starttid för förväntad närvaro (HH:MM)',
};
}
if (!expectedPresenceEndTime) {
errors = {
...errors,
expectedPresenceEndTime: 'Sluttid för förväntad närvaro måste väljas',
};
} else if (!isTimeValid(expectedPresenceEndTime)) {
errors = {
...errors,
expectedPresenceEndTime: 'Felaktig tid för sluttid för förväntad närvaro (HH:MM)',
};
}
if (expectedPresenceEndTime && expectedPresenceStartTime && expectedPresenceEndTime < expectedPresenceStartTime) {
errors = {
...errors,
expectedPresenceEndTimeIsBeforeStartTime: 'Sluttid för förväntad närvaro får inte vara före starttid',
};
}
return errors;
};
}
}

View File

@@ -1,4 +1,4 @@
<a class="back-link" [routerLink]="route">
<msfa-icon [icon]="iconType.ARROW_LEFT"></msfa-icon>
<a [ngClass]="backLinkClass" [routerLink]="route">
<msfa-icon *ngIf="showIcon" [icon]="iconType.ARROW_LEFT"></msfa-icon>
<ng-content></ng-content>
</a>

View File

@@ -1,5 +1,12 @@
@import 'mixins/buttons';
@import 'mixins/link';
.back-link {
@include msfa__link(true);
&--link {
@include msfa__link(true);
}
&--button {
@include msfa__button('secondary');
}
}

View File

@@ -8,6 +8,17 @@ import { IconType } from '@msfa-enums/icon-type.enum';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BackLinkComponent {
private readonly _defaultClass = 'back-link';
@Input() route: string[];
@Input() showIcon = true;
@Input() asButton = false;
iconType = IconType;
get backLinkClass(): string {
if (this.asButton) {
return `${this._defaultClass} ${this._defaultClass}--button`;
}
return `${this._defaultClass} ${this._defaultClass}--link`;
}
}

View File

@@ -1,14 +1,14 @@
<digi-ng-dialog
*ngIf="openConfirmDialog"
[afActive]="openConfirmDialog"
(afOnPrimaryClick)="sendRequest()"
(afOnSecondaryClick)="closeConfirmDialog()"
(afOnInactive)="closeConfirmDialog()"
afHeading="Bekräfta"
*ngIf="dialogOpen"
[afActive]="dialogOpen"
(afOnPrimaryClick)="closeDialog(true)"
(afOnSecondaryClick)="closeDialog(false)"
(afOnInactive)="closeDialog(false)"
[afHeading]="dialogTitle"
afHeadingLevel="h2"
[afAriaLabel]="'Bekräfta att skicka in en ' + reportToConfirm"
afPrimaryButtonText="Skicka"
afSecondaryButtonText="Avbryt"
[afAriaLabel]="ariaLabel"
[afPrimaryButtonText]="primaryButtonText"
[afSecondaryButtonText]="secondaryButtonText"
>
<p>Är du säker på att du vill skicka in en {{reportToConfirm}}?</p>
<ng-content></ng-content>
</digi-ng-dialog>

View File

@@ -8,17 +8,15 @@ import { ConfirmDialog } from '@msfa-enums/confirm-dialog.enum';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmDialogComponent {
@Input() openConfirmDialog: boolean;
@Input() reportToConfirm: string;
@Input() dialogOpen: boolean;
@Input() dialogTitle = 'Bekräfta';
@Input() ariaLabel = 'Bekräfta för att gå vidare';
@Input() primaryButtonText = 'Skicka';
@Input() secondaryButtonText = 'Avbryt';
@Output() confirmDialogChanged = new EventEmitter<ConfirmDialog>();
sendRequest(): void {
this.openConfirmDialog = false;
this.confirmDialogChanged.emit(ConfirmDialog.ACCEPTED);
}
closeConfirmDialog(): void {
this.openConfirmDialog = false;
this.confirmDialogChanged.emit(ConfirmDialog.DISMISSED);
closeDialog(confirmed: boolean): void {
this.dialogOpen = false;
this.confirmDialogChanged.emit(confirmed ? ConfirmDialog.ACCEPTED : ConfirmDialog.DISMISSED);
}
}

View File

@@ -0,0 +1,2 @@
export const ANNAN_KAND_ORSAK_ID = '18';
export const ANNAN_ORSAK_ID = '5';

View File

@@ -1,7 +1,8 @@
export const DELTAGARE_REPORTING_ROUTES = {
'gemensam-planering': 'Gemensam planering',
'periodisk-redovisning': 'Periodisk redovisning',
avvikelserapport: 'Avvikelserapport',
franvarorapport: 'Avvikelserapport (frånvaro)',
avvikelserapport: 'Avvikelserapport (avvikelse)',
};
export const NAVIGATION = {

View File

@@ -1 +1,2 @@
export const EMAIL_REGEX = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/;
export const ISO_DATE_NO_TIME = /^\d{4}[\-\/\s]?((((0[13578])|(1[02]))[\-\/\s]?(([0-2][0-9])|(3[01])))|(((0[469])|(11))[\-\/\s]?(([0-2][0-9])|(30)))|(02[\-\/\s]?[0-2][0-9]))$/;

View File

@@ -1,7 +0,0 @@
export enum AvvikelseOrsaksKodEnum {
TackatNejTillInsatsEllerAktivitet = 19,
TackatNejTillErbjudetArbete = 20,
KanInteTillGodoGoraSigProgrammet = 21,
MisskottSigEllerStortVerksamheten = 22,
SerTillAttErbjudetArbeteInteKommerTillStand = 28
}

View File

@@ -2,6 +2,6 @@ export enum ReportType {
FRANVARO = 'franvaro',
AVVIKELSE = 'avvikelse',
GemensamPlanering = 'Gemensam planering',
Franvaro = 'Frånvaro',
Avvikelse = 'Avvikelse',
Avvikelse = 'Avvikelserapport (avvikelse)',
Franvaro = 'Avvikelserapport (frånvaro)',
}

View File

@@ -29,7 +29,7 @@ export interface AvropResponse {
sparNamn: string;
handledareCiamUserId: string;
handledare: string;
recievedTimestamp: string;
recievedTimestamp: Date;
}
export interface AvropApiResponse {

View File

@@ -1,5 +1,4 @@
export interface OrsaksKoderFranvaroResponse {
id: number;
name: string;
state: number;
}

View File

@@ -24,6 +24,7 @@ export interface Avrop extends AvropCompact {
utforandeVerksamhet: string; // utforandeverksamhet
handledareCiamUserId: string;
handledare: string;
recievedTimestamp: Date;
}
export interface AvropCompactData {
@@ -49,6 +50,7 @@ export function mapAvropResponseToAvrop(data: AvropResponse): Avrop {
utforandeverksamhet,
handledareCiamUserId,
handledare,
recievedTimestamp,
} = data;
return {
@@ -69,5 +71,6 @@ export function mapAvropResponseToAvrop(data: AvropResponse): Avrop {
utforandeVerksamhet: utforandeverksamhet,
handledareCiamUserId: handledareCiamUserId,
handledare,
recievedTimestamp,
};
}

View File

@@ -1,7 +0,0 @@
import { Fraga } from './fraga.model';
export interface AvvikelseAlternativ {
avvikelseorsakskod: string,
frageformular: Array<Fraga>,
rapporteringsdatum: string
}

View File

@@ -1,5 +1,11 @@
import { AvvikelseAlternativ } from './avvikelse-alternativ.model';
import { FranvaroAlternativ } from './franvaro-alternativ.model';
import { Fraga } from '@msfa-models/fraga.model';
export interface AvvikelseAlternativ {
avvikelseorsakskod: string;
frageformular: Fraga[];
rapporteringsdatum: string;
}
export interface Avvikelse {
genomforandeReferens: number;

View File

@@ -1,6 +1,6 @@
export interface DateFormatOptions {
year?: 'short' | 'long' | 'numeric';
year?: 'numeric' | '2-digit';
month?: 'short' | 'long' | 'numeric';
day?: 'short' | 'long' | 'numeric';
weekday?: 'short' | 'long' | 'numeric';
day?: 'numeric' | '2-digit';
weekday?: 'short' | 'long' | 'narrow';
}

View File

@@ -2,8 +2,8 @@ export interface FranvaroAlternativ {
avvikelseOrsaksKod: string;
datum: string;
heldag: boolean;
startTid: string;
slutTid: string;
startTid: string | null;
slutTid: string | null;
forvantadNarvaro: {
startTid: string;
slutTid: string;

View File

@@ -0,0 +1,11 @@
export interface Franvaro {
reason: string;
date: Date;
wholeDay: boolean;
startTime: string;
endTime: string;
expectedPresenceStartTime: string;
expectedPresenceEndTime: string;
otherKnownReason: string;
knownReasonComment: string;
}

View File

@@ -1,9 +1,9 @@
import { AvvikelseOrsaksKodEnum } from '@msfa-enums/avvikelse-orsak-kod.enum';
import { OrsaksKoderAvvikelseResponse } from './api/orsaks-koder-avvikelse.response.model';
// TODO rename to AvvikelseOrsaker
export interface OrsaksKoderAvvikelse {
name: string;
value: AvvikelseOrsaksKodEnum;
id: string; //AvvikelseOrsaksKodEnum
state: number;
}
@@ -12,7 +12,7 @@ export function mapResponseToOrsaksKoderAvvikelse(data: OrsaksKoderAvvikelseResp
return {
name,
value: id,
state
}
id: id.toString(),
state,
};
}

View File

@@ -5,7 +5,6 @@ import { OrsaksKoderFranvaroResponse } from './api/orsaks-koder-franvaro.respons
export interface OrsaksKoderFranvaro {
name: string;
value: FranvaroOrsaksKodEnum;
state: number;
index?: number;
}
@@ -15,13 +14,12 @@ export interface KandaAvvikelseKoder {
}
export function mapResponseToOrsaksKoderFranvaro(data: OrsaksKoderFranvaroResponse): OrsaksKoderFranvaro {
const { name, id, state } = data;
const { name, id } = data;
return {
name,
value: id,
state
}
};
}
export function mapResponseToAndraKandaOrsaker(data: KandaAvvikelseKoderResponse): KandaAvvikelseKoder {
@@ -29,6 +27,6 @@ export function mapResponseToAndraKandaOrsaker(data: KandaAvvikelseKoderResponse
return {
name,
value: id
}
value: id,
};
}

View File

@@ -14,7 +14,9 @@ import {
OrsaksKoderFranvaro,
} from '@msfa-models/orsaks-koder-franvaro.model';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { catchError, filter, map } from 'rxjs/operators';
import { CustomError } from '@msfa-models/error/custom-error';
import { ErrorType } from '@msfa-enums/error-type.enum';
@Injectable({
providedIn: 'root',
@@ -56,8 +58,16 @@ export class AvvikelseApiService {
);
}
public createAvvikelse$(avvikelse: Avvikelse): Promise<void> {
return this.httpClient.post<void>(`${this._apiBaseUrl}/avvikelse`, avvikelse).toPromise();
public createAvvikelse$(avvikelse: Avvikelse): Observable<unknown> {
return this.httpClient.post<void>(`${this._apiBaseUrl}/avvikelse`, avvikelse).pipe(
catchError((error: Error) => {
throw new CustomError({
error,
message: 'Det gick inte att skicka avvikelse \n\n' + error.message,
type: ErrorType.API,
});
})
);
}
public createFranvaro$(avvikelse: Avvikelse): Promise<void> {

View File

@@ -0,0 +1,53 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '@msfa-environment';
import { OrsaksKoderFranvaroResponse } from '@msfa-models/api/orsaks-koder-franvaro.response.model';
import { Avrop } from '@msfa-models/avrop.model';
import { FranvaroRequestData } from '@msfa-models/avvikelse.model';
import { CustomError, errorToCustomError } from '@msfa-models/error/custom-error';
import { mapResponseToOrsaksKoderFranvaro, OrsaksKoderFranvaro } from '@msfa-models/orsaks-koder-franvaro.model';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
import { Observable } from 'rxjs';
import { catchError, filter, map, shareReplay } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class FranvaroReportApiService {
private _apiBaseUrl = `${environment.api.url}/rapporter`;
constructor(private httpClient: HttpClient, private deltagareApiService: DeltagareApiService) {}
public fetchReasons$(): Observable<OrsaksKoderFranvaro[]> {
return this.httpClient.get<{ data: OrsaksKoderFranvaroResponse[] }>(`${this._apiBaseUrl}/orsakskoderfranvaro`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(reason => mapResponseToOrsaksKoderFranvaro(reason))),
catchError((error: Error & { status: number }) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta orsaker till frånvaro.\n\n${error.message}` })
);
}),
shareReplay(1)
);
}
public fetchOtherKnownReasons$(): Observable<OrsaksKoderFranvaro[]> {
return this.httpClient.get<{ data: OrsaksKoderFranvaroResponse[] }>(`${this._apiBaseUrl}/kandaavvikelsekoder`).pipe(
filter(response => !!response?.data),
map(({ data }) => data.map(reason => mapResponseToOrsaksKoderFranvaro(reason))),
catchError((error: Error & { status: number }) => {
throw new CustomError(
errorToCustomError({ ...error, message: `Kunde inte hämta kända orsaker till frånvaro.\n\n${error.message}` })
);
}),
shareReplay(1)
);
}
public fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
}
public async postFranvaroReport$(requestData: FranvaroRequestData): Promise<void> {
return this.httpClient.post<void>(`${this._apiBaseUrl}/franvaro`, requestData).toPromise();
}
}

View File

@@ -1,3 +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) {
@@ -16,3 +18,13 @@ 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 = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
};
return new Date(date).toLocaleDateString(locale, formatOptions);
}

View File

@@ -1,172 +0,0 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { ReportType } from '@msfa-enums/report-type.enum';
import { DayOrPartOfDay } from '@msfa-enums/day-or-part-of-day.enum';
import { FranvaroOrsaksKodEnum } from '@msfa-enums/franvaro-orsak-kod.enum';
import { KandaOrsakerEnum } from '@msfa-enums/kanda-orsaker-kod.enum';
import { ValidationError } from '@msfa-models/validation-error.model';
export interface Controls {
[key: string]: AbstractControl;
}
export function requiredDescriptionValidator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.controls as Controls;
if (ctrls) {
const valueOfNestedFormControl = ctrls['orsakerFormGroup'].get('andraKandaOrsaker').value as string;
const valueOfFormControl = control.value as string;
const isRequired = !valueOfFormControl && +valueOfNestedFormControl === KandaOrsakerEnum.AnnanOrsak;
if (isRequired) {
return { type: 'required', message: 'Beskrivning är obligatoriskt' };
}
return null;
}
};
}
export function requiredOrsakerValidator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.controls as Controls;
if (ctrls) {
const valueOfNestedFormControl = ctrls['orsaker'].value as string;
const isRequired = !valueOfNestedFormControl && !ctrls['andraKandaOrsaker'].value;
if (isRequired) {
return { type: 'required', message: `Orsak är obligatoriskt` };
}
return null;
}
};
}
export function requiredAnnanKandOrsakValidator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.controls as Controls;
if (ctrls) {
const isAnnanKandOrsak = +ctrls['orsaker'].value === FranvaroOrsaksKodEnum.AnnanKandOrsak;
const valueOfNestedFormControl = ctrls['andraKandaOrsaker'].value as string;
const isRequired = isAnnanKandOrsak && !valueOfNestedFormControl;
if (isRequired) {
return { type: 'required', message: `Annan orsak är obligatoriskt` };
}
return null;
}
};
}
export class RequiredDateValidator {
static CheckIfRequired(): ValidatorFn {
return (fg: AbstractControl): { [key: string]: string } => {
const valueOfFormControl = fg?.get('date')?.value as string;
const isRequired = !valueOfFormControl;
return isRequired ? { dateIsRequired: 'Datum är obligatoriskt' } : null;
};
}
}
export function requiredDayOrPartOfDayValidator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.controls as Controls;
if (ctrls) {
const isFranvaro = ctrls['alternative'].value === ReportType.FRANVARO;
const valueOfFormControl = control.value as string;
const isRequired = isFranvaro && !valueOfFormControl;
if (isRequired) {
return { type: 'required', message: `Hel- eller del av dag är obligatoriskt` };
}
return null;
}
};
}
export function requiredStartTimeValidator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.parent?.controls as Controls;
if (ctrls) {
const isFranvaro = (ctrls['alternative']?.value as string) === ReportType.FRANVARO;
const isPartOfDay = ctrls['dayOrPartOfDay']?.value === DayOrPartOfDay.DEL_AV_DAG;
const valueOfFormControl = control?.value as string;
const isRequired = isFranvaro && isPartOfDay && (valueOfFormControl === '' || valueOfFormControl === null);
if (isRequired) {
return { type: 'required', message: `Starttid är obligatoriskt` };
}
return null;
}
};
}
export function requiredEndTimeValidator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.parent?.controls as Controls;
if (ctrls) {
const isFranvaro = (ctrls['alternative']?.value as string) === ReportType.FRANVARO;
const isPartOfDay = ctrls['dayOrPartOfDay']?.value === DayOrPartOfDay.DEL_AV_DAG;
const valueOfFormControl = control?.value as string;
const isRequired = isFranvaro && isPartOfDay && (valueOfFormControl === '' || valueOfFormControl === null);
if (isRequired) {
return { type: 'required', message: `Sluttid är obligatoriskt` };
}
return null;
}
};
}
export function requiredFraga1Validator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.parent?.controls as Controls;
if (ctrls) {
const isAvvikelse = ctrls['alternative']?.value === 'avvikelse';
const valueOfFormControl = control.value as string;
const isRequired = isAvvikelse && !valueOfFormControl;
if (isRequired) {
return { type: 'required', message: `Beskrivning är obligatoriskt` };
}
return null;
}
};
}
export function requiredfraga2Validator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
const ctrls = control?.parent?.parent?.controls as Controls;
if (ctrls) {
const isAvvikelse = ctrls['alternative']?.value === 'avvikelse';
const valueOfFormControl = control.value as string;
const orsaksKodToValidate = ctrls['orsakerFormGroup']?.get('orsaker')?.value as string;
const isRequired =
isAvvikelse &&
!valueOfFormControl &&
orsaksKodToValidate !== '19' &&
orsaksKodToValidate !== '20' &&
orsaksKodToValidate !== '28';
if (isRequired) {
return { type: 'required', message: `Beskrivning är obligatoriskt` };
}
return null;
}
};
}

View File

@@ -0,0 +1,21 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { ISO_DATE_NO_TIME } from '@msfa-constants/regex';
import { ValidationError } from '@msfa-models/validation-error.model';
export function isoDateIsValid(date: string): boolean {
return ISO_DATE_NO_TIME.test(date);
}
export function isoDateWithoutTimeValidator(): ValidatorFn {
return (control: AbstractControl): ValidationError => {
if (control && control.value) {
const value: string = control.value as string;
if (!isoDateIsValid(value)) {
return { type: 'invalid', message: `Ogiltigt datum, vänligen ange YYYY-MM-DD` };
}
}
return null;
};
}

View File

@@ -35,7 +35,7 @@
@if $type == 'secondary' {
background-color: var(--digi-button--background--secondary--hover);
color: var(--digi-button--color--secondary--hover);
color: var(--digi-button--color--secondary);
} @else if $type == 'tertiary' {
color: var(--digi-button--color--tertiary--hover);
} @else {

View File

@@ -66,6 +66,16 @@ dl {
left: 0;
}
// Removing margins from digi fieldset component.
.digi-form-fieldset {
margin: 0;
&__legend {
margin-bottom: var(--digi--layout--gutter--s);
padding: 0;
}
}
.msfa {
&__a11y-sr-only {
@include msfa__a11y-sr-only;