Merge branch 'develop' of ssh://bitbucket.arbetsformedlingen.se:7999/tea/mina-sidor-fa-web into develop

This commit is contained in:
Erik Tiekstra
2021-11-12 09:31:05 +01:00
105 changed files with 5624 additions and 44 deletions

View File

@@ -2,7 +2,7 @@
<digi-typography>
<h1>{{ reportTitle }}</h1>
<p class="report-layout__description" *ngIf="description">{{description}}</p>
<msfa-report-description-list [avrop]="avrop"></msfa-report-description-list>
<msfa-report-description-list *ngIf="showAvropDetails" [avrop]="avrop"></msfa-report-description-list>
<div class="report-layout__main-content">
<ng-content></ng-content>
</div>

View File

@@ -14,4 +14,5 @@ export class ReportLayoutComponent {
@Input() startDate: string;
@Input() endDate: string;
@Input() avrop: Avrop;
@Input() showAvropDetails = true;
}

View File

@@ -0,0 +1,79 @@
<h3>Huvudsysselsättning</h3>
<dl>
<dt>Vilken är deltagarens huvudsakliga sysselsättning just nu?</dt>
<dd>{{mainOccupationName}}</dd>
<ng-container *ngIf="education">
<dt>Utbildningsnivå</dt>
<dd>{{educationLevel }}</dd>
<ng-container *ngIf="education.otherExplanation">
<dt>Beskrivning av Annat</dt>
<dd>{{education.otherExplanation }}</dd>
</ng-container>
<dt>Utbildningens längd</dt>
<dd>{{educationLength }}</dd>
<dt>Inriktning på utbildningen</dt>
<dd>{{education.educationSpecification }}</dd>
</ng-container>
<ng-container *ngIf="work">
<ng-container *ngFor="let workItem of work">
<dt>Yrkesområde</dt>
<dd>{{workItem.yrkesomradeName }}</dd>
<dt>Yrkesgrupp</dt>
<dd>{{workItem.yrkesgruppName }}</dd>
<dt>Anställningsform</dt>
<dd>{{workItem.anstallningsform }}</dd>
<ng-container *ngIf="workItem.otherExplanation">
<dt>Beskrivning av Annat</dt>
<dd>{{workItem.otherExplanation }}</dd>
</ng-container>
<dt>Omfattning</dt>
<dd>{{omfattningToString(workItem.omfattning) }}</dd>
<dt>Omfattning i procent</dt>
<dd>{{workItem.omfattningPercent }}</dd>
</ng-container>
</ng-container>
<ng-container *ngIf="stillUnemployed">
<dt>Anledning till att arbetssökanden under tjänstens gång inte nått målet:</dt>
<ul>
<ng-container *ngFor="let reason of stillUnemployed.reasonsGoalNotReached">
<li>{{capitalizeSentence(reason) }}</li>
</ng-container>
</ul>
<ng-container *ngIf="stillUnemployed.otherExplanation">
<dt>Beskrivning av Annat</dt>
<dd>{{stillUnemployed.otherExplanation }}</dd>
</ng-container>
</ng-container>
<ng-container *ngIf="other">
<dt>Förtydling av Annan huvudsysselsättning:</dt>
<dd>{{other.otherExplanation }}</dd>
</ng-container>
</dl>
<h3>Aktiviteter</h3>
<dl>
<ng-container *ngFor="let activity of slutredovisning.activities">
<dt>{{activity.name}}</dt>
<dd>
<pre>{{activity.whatHasBeenDone}}</pre>
</dd>
</ng-container>
</dl>
<h3>Deltagarens framsteg och utveckling</h3>
<dl>
<dt>Beskriv deltagarens framsteg och utveckling under perioden</dt>
<dd><pre>{{slutredovisning.progressDescription}}</pre></dd>
<dt>Information om lämpligt nästa steg för deltagaren</dt>
<dd><pre>{{slutredovisning.nextStepDescription}}</pre></dd>
<dt>Övrig information</dt>
<dd><pre>{{slutredovisning.otherInformation}}</pre></dd>
</dl>

View File

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

View File

@@ -0,0 +1,65 @@
import { Component, Input } from '@angular/core';
import {
educationLengthToString,
educationLevelToString,
MainOccupation,
mainOccupationToString,
Omfattning,
omfattningToString,
Slutredovisning,
} from '@msfa-models/slutredovisning.model';
import { capitalizeSentence } from '@msfa-utils/capitalize-sentence.util';
import {
SlutredovisningResponseMainOccupationEducationDetails,
SlutredovisningResponseMainOccupationOtherDetails,
SlutredovisningResponseMainOccupationStillUnemployedDetails,
SlutredovisningResponseMainOccupationWorkDetails,
} from '@msfa-models/api/slutredovisning.response.model';
@Component({
selector: 'msfa-slutredovisning-view-description-list',
templateUrl: './slutredovisning-view-description-list.component.html',
styleUrls: ['./slutredovisning-view-description-list.component.css'],
})
export class SlutredovisningViewDescriptionListComponent {
capitalizeSentence = capitalizeSentence;
@Input() slutredovisning: Slutredovisning;
get mainOccupationName(): string | null {
return mainOccupationToString(this.slutredovisning?.mainOccupation?.type);
}
get education(): SlutredovisningResponseMainOccupationEducationDetails {
return this.slutredovisning?.mainOccupation?.type === MainOccupation.Education
? this.slutredovisning.mainOccupation.education
: undefined;
}
get work(): SlutredovisningResponseMainOccupationWorkDetails[] {
return this.slutredovisning?.mainOccupation?.type === MainOccupation.Work
? this.slutredovisning.mainOccupation.work
: undefined;
}
get stillUnemployed(): SlutredovisningResponseMainOccupationStillUnemployedDetails {
return this.slutredovisning?.mainOccupation?.type === MainOccupation.StillUnemployed
? this.slutredovisning.mainOccupation.stillUnemployed
: undefined;
}
get other(): SlutredovisningResponseMainOccupationOtherDetails {
return this.slutredovisning?.mainOccupation?.type === MainOccupation.Other
? this.slutredovisning.mainOccupation.other
: undefined;
}
get educationLevel(): string {
return educationLevelToString(this.education.educationLevel);
}
get educationLength(): string {
return educationLengthToString(this.education.educationLength);
}
omfattningToString(omfattning: Omfattning): string {
return omfattningToString(omfattning);
}
}

View File

@@ -0,0 +1,15 @@
import { SlutredovisningViewService } from '../../pages/report-views/slutredovisning-view/slutredovisning-view.service';
import { UiLoaderModule } from '@ui/loader/loader.module';
import { CommonModule } from '@angular/common';
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { SlutredovisningViewDescriptionListComponent } from './slutredovisning-view-description-list.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [SlutredovisningViewDescriptionListComponent],
imports: [CommonModule, UiLoaderModule, UiSkeletonModule],
providers: [SlutredovisningViewService],
exports: [SlutredovisningViewDescriptionListComponent],
})
export class SlutredovisningViewDescriptionListModule {}

View File

@@ -110,6 +110,26 @@ activeFeatures.forEach(feature => {
}
);
break;
case Feature.SLUTREDOVISNING:
routes.push(
{
path: 'slutredovisning',
data: { title: 'Skapa slutredovisning' },
loadChildren: () =>
import('./pages/report-forms/slutredovisning-form/slutredovisning-form.module').then(
m => m.SlutredovisningFormModule
),
},
{
path: 'slutredovisning/:handlingId',
data: { title: 'Slutredovisning' },
loadChildren: () =>
import('./pages/report-views/slutredovisning-view/slutredovisning-view.module').then(
m => m.SlutredovisningViewModule
),
}
);
break;
default:
break;
}

View File

@@ -60,6 +60,8 @@ export class ReportsListComponent {
return `./avvikelserapport/${report.id}`;
case ReportType.PeriodiskRedovisning:
return `./periodisk-redovisning/${report.id}`;
case ReportType.Slutredovisning:
return `./slutredovisning/${report.id}`;
case ReportType.InformativRapport:
case ReportType.InformativRedovisning:
return `./informativ-rapport/${report.id}`;

View File

@@ -45,6 +45,13 @@
afText="Informativ rapport"
></digi-ng-link-button>
</li>
<li *ngIf="slutredovisningButtonVisible">
<digi-ng-link-button
class="deltagare-tab-reports__button"
afRoute="./slutredovisning"
afText="Slutredovisning"
></digi-ng-link-button>
</li>
</ul>
<ng-container *ngIf="reportsData$ | async as reportsData; else loadingRef">

View File

@@ -36,6 +36,9 @@ export class DeltagareTabReportsComponent {
get informativRapportButtonVisible(): boolean {
return this._activeFeatures.includes(Feature.REPORTING_INFORMATIV_RAPPORT);
}
get slutredovisningButtonVisible(): boolean {
return this._activeFeatures.includes(Feature.SLUTREDOVISNING);
}
constructor(private activatedRoute: ActivatedRoute, private deltagareApiService: DeltagareApiService) {}

View File

@@ -0,0 +1,68 @@
import {
Anstallningsform,
EducationLength,
EducationLevel,
MainOccupation,
Omfattning,
StillUnemployedReason,
} from '@msfa-models/slutredovisning.model';
export interface SlutredovisningFormDataMainOccupationWorkDetails {
yrkesomrade: string;
yrkesgrupp: string;
anstallningsform: Anstallningsform;
otherExplanation: string;
omfattning: Omfattning;
omfattningPercent: number;
}
export interface SlutredovisningFormDataMainOccupationWork {
type: MainOccupation.Work;
work: SlutredovisningFormDataMainOccupationWorkDetails[];
}
export interface SlutredovisningFormDataMainOccupationEducationDetails {
educationLevel: EducationLevel;
otherExplanation: string;
educationLength: EducationLength;
educationSpecification: string;
}
export interface SlutredovisningFormDataMainOccupationEducation {
type: MainOccupation.Education;
education: SlutredovisningFormDataMainOccupationEducationDetails;
}
export interface SlutredovisningFormDataMainOccupationOtherDetails {
otherExplanation: string;
}
export interface SlutredovisningFormDataMainOccupationOther {
type: MainOccupation.Other;
other: SlutredovisningFormDataMainOccupationOtherDetails;
}
export interface SlutredovisningFormDataMainOccupationStillUnemployedDetails {
reasonsGoalNotReached: StillUnemployedReason[];
otherExplanation: string;
}
export interface SlutredovisningFormDataMainOccupationStillUnemployed {
type: MainOccupation.StillUnemployed;
stillUnemployed: SlutredovisningFormDataMainOccupationStillUnemployedDetails;
}
export type SlutredovisningFormDataMainOccupationDetails =
| SlutredovisningFormDataMainOccupationWork
| SlutredovisningFormDataMainOccupationEducation
| SlutredovisningFormDataMainOccupationOther
| SlutredovisningFormDataMainOccupationStillUnemployed;
export interface SlutredovisningFormData {
genomforandereferens: number;
mainOccupation: SlutredovisningFormDataMainOccupationDetails;
activities: { id: string; whatHasBeenDone: string; name: string }[];
progressDescription: string;
nextStepDescription: string;
otherInformation: string;
}

View File

@@ -0,0 +1,46 @@
<div class="slutredovisning-form-step0-education">
<div class="slutredovisning-form-step0-education__level">
<digi-form-fieldset af-legend="Utbildningsnivå" af-name="mainOccupationx" af-form="report-form">
<ui-radiobutton-group
[uiRadiobuttons]="educationLevelOptions"
[formControl]="educationLevelFormControl"
uiName="educationLevel"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid(educationLevelFormControl)"
[uiValidationMessage]="educationLevelFormControl.errors?.required"
></ui-radiobutton-group>
</digi-form-fieldset>
<ui-textarea
*ngIf="educationLevelFormControl.value === EducationLevel.Annat"
[formControl]="otherExplanationFormControl"
uiLabel="Beskrivning av Annat"
[uiValidationMessage]="formGroup?.errors?.needDescriptionOfOther"
[uiAnnounceIfOptional]="false"
[uiMaxLength]="200"
[uiInvalid]="(otherExplanationFormControl?.touched || this.shouldValidate) && formGroup.errors?.needDescriptionOfOther"
></ui-textarea>
</div>
<digi-form-fieldset af-legend="Utbildningens längd" af-name="mainOccupationx" af-form="report-form">
<ui-radiobutton-group
[uiRadiobuttons]="educationLengthOptions"
[formControl]="educationLengthFormControl"
uiName="educationLength"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid(educationLengthFormControl)"
[uiValidationMessage]="educationLengthFormControl.errors?.required"
></ui-radiobutton-group>
</digi-form-fieldset>
<ui-textarea
[formControl]="educationSpecificationFormControl"
uiLabel="Inriktning på utbildningen"
[uiValidationMessage]="educationSpecificationFormControl?.errors?.required"
[uiAnnounceIfOptional]="false"
[uiMaxLength]="200"
[uiInvalid]="(educationSpecificationFormControl?.touched || this.shouldValidate) && educationSpecificationFormControl.errors?.required"
></ui-textarea>
</div>

View File

@@ -0,0 +1,14 @@
@import 'mixins/list';
@import 'variables/gutters';
.slutredovisning-form-step0-education {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
&__level {
display: flex;
flex-direction: column;
gap: var(--digi--layout--gutter);
}
}

View File

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

View File

@@ -0,0 +1,102 @@
import { RadiobuttonModel } from '@af/digi-ng/_form/form-radiobutton-group';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { RequiredValidator } from '@msfa-validators/required.validator';
import { SlutredovisningFormStep0Education } from './slutredovisning-form-step0-education.validator';
import { EducationLength, EducationLevel, MainOccupation } from '@msfa-models/slutredovisning.model';
export interface SlutredovisningFormStep0EducationFormData {
educationLevel: EducationLevel;
educationLength: EducationLength;
otherExplanation?: string;
educationSpecification: string;
}
type FormKeys = keyof SlutredovisningFormStep0EducationFormData;
@Component({
selector: 'msfa-slutredovisning-form-step0-education',
templateUrl: './slutredovisning-form-step0-education.component.html',
styleUrls: ['./slutredovisning-form-step0-education.component.scss'],
})
export class SlutredovisningFormStep0EducationComponent implements OnInit {
readonly formGroupKey: MainOccupation = MainOccupation.Education;
@Input() formGroupRef: FormGroup;
@Input() formData: SlutredovisningFormStep0EducationFormData;
EducationLevel = EducationLevel;
@Input() shouldValidate: boolean;
@Output() nextClick = new EventEmitter<SlutredovisningFormStep0EducationFormData>();
@Output() backClick = new EventEmitter<void>();
private educationLevelFormControlName: FormKeys = 'educationLevel';
private educationLengthFormControlName: FormKeys = 'educationLength';
private otherExplanationFormControlName: FormKeys = 'otherExplanation';
private educationSpecificationFormControlName: FormKeys = 'educationSpecification';
// utbildningsniva
educationLevelOptions: RadiobuttonModel[] = [
{ label: 'Högskola eller universitet', value: EducationLevel.HogskolaEllerUniversitet },
{ label: 'Yrkeshögskola', value: EducationLevel.Yrkeshogskola },
{ label: 'Komvux, gymnasium eller folkhögskola', value: EducationLevel.KomvuxGymnasiumFolkhogskola },
{
label: 'Vet ej',
value: EducationLevel.VetEj,
},
{
label: 'Annat',
value: EducationLevel.Annat,
},
];
// langd_utbildning
educationLengthOptions: RadiobuttonModel[] = [
{ label: 'Upp till ett år', value: EducationLength.UpToOneYear },
{ label: 'Ett år till två år', value: EducationLength.OneToTwoYears },
{ label: 'Två år eller längre', value: EducationLength.OverTwoYears },
];
formGroup: FormGroup | AbstractControl;
get educationLevelFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.educationLevelFormControlName);
}
get educationLengthFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.educationLengthFormControlName);
}
get otherExplanationFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.otherExplanationFormControlName);
}
get educationSpecificationFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.educationSpecificationFormControlName);
}
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate);
}
ngOnInit(): void {
if (this.formGroupRef.get(this.formGroupKey)) {
this.formGroup = this.formGroupRef.get(this.formGroupKey);
} else {
this.formGroup = new FormGroup(
{
[this.educationLevelFormControlName]: new FormControl(null, [
RequiredValidator('Utbildningsnivå är obligatoriskt'),
]),
[this.otherExplanationFormControlName]: new FormControl(null),
[this.educationLengthFormControlName]: new FormControl(null, [
RequiredValidator('Utbildningens längd är obligatorisk'),
]),
[this.educationSpecificationFormControlName]: new FormControl(null, [
RequiredValidator('Utbildningens inriktning är obligatorisk'),
]),
},
[SlutredovisningFormStep0Education.slutredovisningFormStep1EducationIsValid()]
);
this.formGroupRef.addControl(this.formGroupKey, this.formGroup);
}
}
}

View File

@@ -0,0 +1,26 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { SlutredovisningFormStep0EducationFormData } from './slutredovisning-form-step0-education.component';
import { EducationLevel } from '@msfa-models/slutredovisning.model';
export interface SlutredovisningFormErrors {
needDescriptionOfOther?: string;
}
export class SlutredovisningFormStep0Education {
static slutredovisningFormStep1EducationIsValid(): ValidatorFn {
return (c: AbstractControl): SlutredovisningFormErrors => {
let errors: SlutredovisningFormErrors = {};
const { educationLevel, otherExplanation } = c.value as SlutredovisningFormStep0EducationFormData;
if (educationLevel === EducationLevel.Annat && (!otherExplanation || otherExplanation?.length <= 0)) {
errors = {
...errors,
needDescriptionOfOther: 'Ifall Annat är valt som utbildningsnivå krävs en beskrivning.',
};
}
return errors;
};
}
}

View File

@@ -0,0 +1,13 @@
<div class="slutredovisning-report-form-step0-still-unemployed">
<div class="slutredovisning-report-form-step0-still-unemployed__form">
<ui-textarea
[formControl]="otherExplanationFormControl"
uiLabel="Beskrivning av Annat"
[uiValidationMessage]="otherExplanationFormControl?.errors?.required"
[uiAnnounceIfOptional]="false"
[uiMaxLength]="200"
[uiRequired]="true"
[uiInvalid]="(otherExplanationFormControl?.touched || this.shouldValidate) && otherExplanationFormControl.errors?.required"
></ui-textarea>
</div>
</div>

View File

@@ -0,0 +1,26 @@
@import 'mixins/list';
@import 'variables/gutters';
.slutredovisning-report-form-step0-still-unemployed {
max-width: var(--digi--typography--text--max-width);
&__confirmation,
&__warning,
&__form {
position: relative;
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
}
&__footer {
display: flex;
flex-direction: column;
gap: var(--digi--layout--gutter);
}
&__cta-wrapper {
display: flex;
gap: var(--digi--layout--gutter);
}
}

View File

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

View File

@@ -0,0 +1,51 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { RequiredValidator } from '@msfa-validators/required.validator';
import { MainOccupation } from '@msfa-models/slutredovisning.model';
export interface SlutredovisningFormStep0OtherFormData {
otherExplanation: string;
}
type FormKeys = keyof SlutredovisningFormStep0OtherFormData;
@Component({
selector: 'msfa-slutredovisning-form-step0-other',
templateUrl: './slutredovisning-form-step0-other.component.html',
styleUrls: ['./slutredovisning-form-step0-other.component.scss'],
})
export class SlutredovisningFormStep0OtherComponent implements OnInit {
readonly formGroupKey: MainOccupation = MainOccupation.Other;
@Input() formGroupRef: FormGroup;
@Input() formData: SlutredovisningFormStep0OtherFormData;
@Input() shouldValidate: boolean;
@Output() nextClick = new EventEmitter<SlutredovisningFormStep0OtherFormData>();
@Output() backClick = new EventEmitter<void>();
private otherExplanationFormControlName: FormKeys = 'otherExplanation';
formGroup: FormGroup | AbstractControl;
get otherExplanationFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.otherExplanationFormControlName);
}
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate);
}
ngOnInit(): void {
if (this.formGroupRef.get(this.formGroupKey)) {
this.formGroup = this.formGroupRef.get(this.formGroupKey);
} else {
this.formGroup = new FormGroup({
[this.otherExplanationFormControlName]: new FormControl(
null,
RequiredValidator('En beskrivning av Annat är obligatoriskt.')
),
});
this.formGroupRef.addControl(this.formGroupKey, this.formGroup);
}
}
}

View File

@@ -0,0 +1,36 @@
<div class="slutredovisning-report-form-step0-still-unemployed">
<div class="slutredovisning-report-form-step0-still-unemployed__form">
<div>
<digi-form-fieldset
af-legend="Anledning till att arbetssökanden under tjänstens gång inte har nått målet"
af-name="stillUnemployed"
af-form="report-form"
[formGroup]="stillUnemployedReasonFormControl"
>
<ui-checkbox
*ngFor="let stillUnemployedReasonOption of stillUnemployedReasonOptions"
[formControl]="stillUnemployedReasonFormControl.get(stillUnemployedReasonOption.formControlName)"
[uiLabel]="stillUnemployedReasonOption.label"
[uiInvalid]="(stillUnemployedReasonFormControl?.touched || this.shouldValidate) && formGroup?.errors?.atLeastOneSelected"
></ui-checkbox>
</digi-form-fieldset>
<div aria-atomic="true" role="alert">
<digi-form-validation-message
*ngIf="(stillUnemployedReasonFormControl?.touched || this.shouldValidate) && formGroup?.errors?.atLeastOneSelected"
af-variation="error"
>{{formGroup?.errors?.atLeastOneSelected}}</digi-form-validation-message
>
</div>
</div>
<ui-textarea
*ngIf="showExplanationTextArea"
[formControl]="stillUnemployedReasonDescriptionFormControl"
uiLabel="Beskrivning av Annat"
[uiValidationMessage]="formGroup?.errors?.needDescriptionOfOther"
[uiAnnounceIfOptional]="false"
[uiMaxLength]="200"
[uiInvalid]="(stillUnemployedReasonDescriptionFormControl?.touched || this.shouldValidate) && formGroup.errors?.needDescriptionOfOther"
></ui-textarea>
</div>
</div>

View File

@@ -0,0 +1,26 @@
@import 'mixins/list';
@import 'variables/gutters';
.slutredovisning-report-form-step0-still-unemployed {
max-width: var(--digi--typography--text--max-width);
&__confirmation,
&__warning,
&__form {
position: relative;
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
}
&__footer {
display: flex;
flex-direction: column;
gap: var(--digi--layout--gutter);
}
&__cta-wrapper {
display: flex;
gap: var(--digi--layout--gutter);
}
}

View File

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

View File

@@ -0,0 +1,97 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { SlutredovisningFormStep0StillUnemployed } from './slutredovisning-form-step0-still-unemployed.validator';
import { MainOccupation, StillUnemployedReason } from '@msfa-models/slutredovisning.model';
type ReasonCheckboxValues = { [key: string]: boolean };
export interface SlutredovisningFormStep0StillUnemployedFormData {
reasonsGoalNotReached: ReasonCheckboxValues;
stillUnemployedExplanation?: string;
}
type FormKeys = keyof SlutredovisningFormStep0StillUnemployedFormData;
interface CheckboxModel {
label: string;
formControlName: string;
}
@Component({
selector: 'msfa-slutredovisning-form-step0-still-unemployed',
templateUrl: './slutredovisning-form-step0-still-unemployed.component.html',
styleUrls: ['./slutredovisning-form-step0-still-unemployed.component.scss'],
})
export class SlutredovisningFormStep0StillUnemployedComponent implements OnInit {
readonly formGroupKey: MainOccupation = MainOccupation.StillUnemployed;
@Input() formGroupRef: FormGroup;
@Input() formData: SlutredovisningFormStep0StillUnemployedFormData;
StillUnemployedReason = StillUnemployedReason;
@Input() shouldValidate: boolean;
@Output() nextClick = new EventEmitter<SlutredovisningFormStep0StillUnemployedFormData>();
@Output() backClick = new EventEmitter<void>();
private stillUnemployedReasonFormControlName: FormKeys = 'reasonsGoalNotReached';
private stillUnemployedExplanationFormControlName: FormKeys = 'stillUnemployedExplanation';
//anledning_mal_ej_uppnatt
stillUnemployedReasonOptions: CheckboxModel[] = [
{ label: 'Saknar relevant utbildning', formControlName: StillUnemployedReason.SaknarRelevantUtbildning },
{ label: 'Saknar arbetslivserfarenhet', formControlName: StillUnemployedReason.SaknarArbetslivserfarenhet },
{ label: 'Saknar nätverk och kontakter', formControlName: StillUnemployedReason.SaknarNatverkOchKontakter },
{
label: 'Bristande språkkunskaper i svenska',
formControlName: StillUnemployedReason.BristandeSprakkunskaperISvenska,
},
{
label: 'Konjunkturläget',
formControlName: StillUnemployedReason.KonjukturLaget,
},
{
label: 'Annat',
formControlName: StillUnemployedReason.Annat,
},
];
formGroup: FormGroup | AbstractControl;
get showExplanationTextArea(): boolean {
const reasonsValue: ReasonCheckboxValues | undefined = this.stillUnemployedReasonFormControl
?.value as ReasonCheckboxValues;
return reasonsValue[StillUnemployedReason.Annat] === true;
}
get stillUnemployedReasonFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.stillUnemployedReasonFormControlName);
}
get stillUnemployedReasonDescriptionFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.stillUnemployedExplanationFormControlName);
}
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate);
}
ngOnInit(): void {
if (this.formGroupRef.get(this.formGroupKey)) {
this.formGroup = this.formGroupRef.get(this.formGroupKey);
} else {
const reasonsFormControls: { [key: string]: AbstractControl } = {};
this.stillUnemployedReasonOptions.forEach(reason => {
reasonsFormControls[reason.formControlName] = new FormControl(false);
});
this.formGroup = new FormGroup(
{
[this.stillUnemployedReasonFormControlName]: new FormGroup(reasonsFormControls),
[this.stillUnemployedExplanationFormControlName]: new FormControl(null),
},
[SlutredovisningFormStep0StillUnemployed.slutredovisningFormStep0StillUnemployedIsValid()]
);
this.formGroupRef.addControl(this.formGroupKey, this.formGroup);
}
}
}

View File

@@ -0,0 +1,41 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { SlutredovisningFormStep0StillUnemployedFormData } from './slutredovisning-form-step0-still-unemployed.component';
import { StillUnemployedReason } from '@msfa-models/slutredovisning.model';
export interface SlutredovisningStillUnemployedFormErrors {
needDescriptionOfOther?: string;
atLeastOneSelected?: string;
}
export class SlutredovisningFormStep0StillUnemployed {
static slutredovisningFormStep0StillUnemployedIsValid(): ValidatorFn {
return (c: AbstractControl): SlutredovisningStillUnemployedFormErrors => {
let errors: SlutredovisningStillUnemployedFormErrors = {};
const {
reasonsGoalNotReached,
stillUnemployedExplanation,
} = c.value as SlutredovisningFormStep0StillUnemployedFormData;
if (Object.values(reasonsGoalNotReached).every(reasonIsChecked => !reasonIsChecked)) {
errors = {
...errors,
atLeastOneSelected: 'Minst en anledning måste väljas.',
};
}
// TODO: Is it required that at least one reason is chosen? Check with Bea and Kia
if (
reasonsGoalNotReached[StillUnemployedReason.Annat] === true &&
(!stillUnemployedExplanation || stillUnemployedExplanation?.length <= 0)
) {
errors = {
...errors,
needDescriptionOfOther: 'Ifall Annat är valt som utbildningsnivå krävs en beskrivning.',
};
}
return errors;
};
}
}

View File

@@ -0,0 +1,8 @@
export interface SlutredovisningFormStep0WorkErrors {
yrkesomrade?: string;
yrkesgrupp?: string;
anstallningsform?: string;
annatAnstallningComment?: string;
omfattning?: string;
omfattningPercent?: string;
}

View File

@@ -0,0 +1,111 @@
<div class="slutredovisning-form-step0-work" [formGroup]="formGroupRef">
<ng-container [formArrayName]="MainOccupation.Work">
<!-- <ui-error-list uiHeadingText="Something is wrong" [uiErrorLinks]="errorListErrors"></ui-error-list> -->
<h2 class="slutredovisning-form-step0-work__heading">Inom vilket/vilka yrken</h2>
<div
class="slutredovisning-form-step0-work__group"
*ngFor="let formGroup of formArray.controls; let index = index"
[formGroupName]="index"
>
<div class="slutredovisning-form-step0-work__group-header">
<h3 class="slutredovisning-form-step0-work__heading">Yrke {{index + 1}}</h3>
<digi-button
*ngIf="formArray.controls.length < 2"
af-size="s"
af-variation="tertiary"
(afOnClick)="addNewFormGroupToFormArray()"
>
<msfa-icon icon="plus" size="s" slot="icon"></msfa-icon>
Lägg till yrke
</digi-button>
<digi-button
*ngIf="formArray.controls.length === 2"
af-size="s"
af-variation="tertiary"
(afOnClick)="removeGroup(index)"
>
<msfa-icon icon="x" size="s" slot="icon"></msfa-icon>
Ta bort yrket
</digi-button>
</div>
<ui-select
*ngIf="yrkesomradeSelectOptions$ | async as yrkesomradeSelectOptions"
formControlName="yrkesomrade"
uiLabel="Yrkesområde"
uiPlaceholder="Välj yrkesområde"
[uiId]="'slutredovisning-yrkesomrade-control-'+index"
[uiOptions]="yrkesomradeSelectOptions"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid('yrkesomrade', index)"
[uiValidationMessage]="formControlError('yrkesomrade', index)"
></ui-select>
<ui-select
*ngIf="getYrkesgruppSelectOptions$(index) | async as yrkesgruppSelectOptions"
formControlName="yrkesgrupp"
uiLabel="Yrkesgrupp"
uiPlaceholder="Välj yrkesgrupp"
[uiId]="'slutredovisning-yrkesgrupp-control-'+index"
[uiOptions]="yrkesgruppSelectOptions"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid('yrkesgrupp', index)"
[uiValidationMessage]="formControlError('yrkesgrupp', index)"
></ui-select>
<digi-form-fieldset af-legend="Anställningsform">
<ui-radiobutton-group
[uiRadiobuttons]="anstallningsformOptions"
formControlName="anstallningsform"
[uiName]="'anstallningsform-'+index"
[uiId]="'slutredovisning-anstallningsform-control-'+index"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid('anstallningsform', index)"
[uiValidationMessage]="formControlError('anstallningsform', index)"
></ui-radiobutton-group>
</digi-form-fieldset>
<ui-textarea
*ngIf="formArray.controls[index].get('anstallningsform').value === Anstallningsform.Annat"
formControlName="annatAnstallningComment"
uiLabel="Beskrivning av Annat"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiId]="'slutredovisning-annatAnstallningComment-control-'+index"
[uiMaxLength]="200"
[uiInvalid]="formControlIsInvalid('annatAnstallningComment', index)"
[uiValidationMessage]="formControlError('annatAnstallningComment', index)"
></ui-textarea>
<digi-form-fieldset af-legend="Omfattning">
<div class="slutredovisning-form-step0-work__fieldset-content">
<ui-radiobutton-group
#omfattningRadioGroup
[uiRadiobuttons]="omfattningOptions"
formControlName="omfattning"
[uiName]="'omfattning-'+index"
[uiId]="'slutredovisning-omfattning-control-'+index"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid('omfattning', index)"
[uiValidationMessage]="formControlError('omfattning', index)"
></ui-radiobutton-group>
<ui-input
*ngIf="formArray.controls[index].get('omfattning').value === Omfattning.Deltid"
class="slutredovisning-form-step0-work__number-input"
formControlName="omfattningPercent"
uiType="number"
[uiMin]="1"
[uiMax]="99"
uiLabel="Anställningens omfattning i procent"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid('omfattningPercent', index)"
[uiValidationMessage]="formControlError('omfattningPercent', index)"
></ui-input>
</div>
</digi-form-fieldset>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,30 @@
@import 'variables/gutters';
.slutredovisning-form-step0-work {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
&__group,
&__fieldset-content {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter;
}
&__group-header {
display: flex;
gap: $digi--layout--gutter;
align-items: center;
}
&__heading {
margin: 0;
}
&__number-input {
::ng-deep input {
width: auto !important;
}
}
}

View File

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

View File

@@ -0,0 +1,116 @@
import { RadiobuttonModel } from '@af/digi-ng/_form/form-radiobutton-group';
import { Component, Input, OnInit } from '@angular/core';
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { Anstallningsform, MainOccupation, Omfattning } from '@msfa-models/slutredovisning.model';
import { Yrkesgrupp } from '@msfa-models/yrkesgrupp.model';
import { Yrkesomrade } from '@msfa-models/yrkesomrade.model';
import { capitalizeSentence } from '@msfa-utils/capitalize-sentence.util';
import { ErrorLink } from '@ui/error-list/error-link.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SlutredovisningFormService } from '../../slutredovisning-form.service';
import { SlutredovisningFormStep0WorkFormData } from './slutredovisning-form-step0-work.model';
import { SlutredovisningFormStep0WorkValidator } from './slutredovisning-form-step0-work.validator';
@Component({
selector: 'msfa-slutredovisning-form-step0-work',
templateUrl: './slutredovisning-form-step0-work.component.html',
styleUrls: ['./slutredovisning-form-step0-work.component.scss'],
})
export class SlutredovisningFormStep0WorkComponent implements OnInit {
readonly formGroupKey: MainOccupation = MainOccupation.Work;
@Input() formGroupRef: FormGroup;
@Input() formData: SlutredovisningFormStep0WorkFormData;
@Input() shouldValidate: boolean;
MainOccupation = MainOccupation;
Anstallningsform = Anstallningsform;
Omfattning = Omfattning;
yrkesomradeSelectOptions$: Observable<Yrkesomrade[]> = this.slutredovisningFormService.yrkesomraden$;
formArray: FormArray;
anstallningsformOptions: RadiobuttonModel[] = Object.values(Anstallningsform).map(value => ({
label: capitalizeSentence(value),
value,
}));
omfattningOptions: RadiobuttonModel[] = Object.values(Omfattning).map(value => ({
label: capitalizeSentence(value),
value,
}));
constructor(private slutredovisningFormService: SlutredovisningFormService) {}
getYrkesgruppSelectOptions$(index: number): Observable<Yrkesgrupp[]> {
const yrkesomradeId = this.formArray.controls[index].get('yrkesomrade').value as string;
return this.yrkesomradeSelectOptions$.pipe(
map(yrkesomraden =>
yrkesomradeId
? yrkesomraden
.filter(yo => yo.value === yrkesomradeId)
.reduce((yrkesgrupper: Yrkesgrupp[], yo: Yrkesomrade) => {
yrkesgrupper.push(...yo.items);
return yrkesgrupper;
}, [])
: null
)
);
}
get errorListErrors(): ErrorLink[] {
const formControlErrors: ErrorLink[] = [];
if (this.shouldValidate) {
this.formArray.controls.forEach(arrayControl => {
if (arrayControl.errors) {
Object.entries(arrayControl.errors).forEach(([key, value]) => {
formControlErrors.push({
elementId: `slutredovisning-${key}-control`,
text: value as string,
});
});
}
});
}
return formControlErrors;
}
formControlIsInvalid(formControlName: string, index: number): boolean {
const errors = this.formArray.controls[index].errors;
return errors && errors[formControlName] && this.shouldValidate;
}
formControlError(formControlName: string, index: number): string {
const errors = this.formArray.controls[index].errors;
return errors && (errors[formControlName] as string);
}
addNewFormGroupToFormArray(): void {
this.formArray.push(
new FormGroup(
{
yrkesomrade: new FormControl(null),
yrkesgrupp: new FormControl(null),
anstallningsform: new FormControl(null),
annatAnstallningComment: new FormControl(null),
omfattning: new FormControl(null),
omfattningPercent: new FormControl(null),
},
[SlutredovisningFormStep0WorkValidator.slutredovisningFormStep0WorkIsValid()]
)
);
}
removeGroup(index: number): void {
this.formArray.removeAt(index);
}
ngOnInit(): void {
if (this.formGroupRef.get(this.formGroupKey)) {
this.formArray = this.formGroupRef.get(this.formGroupKey) as FormArray;
} else {
this.formArray = new FormArray([]);
this.addNewFormGroupToFormArray();
this.formGroupRef.addControl(this.formGroupKey, this.formArray);
}
}
}

View File

@@ -0,0 +1,10 @@
export interface SlutredovisningFormStep0WorkFormData {
yrkesomrade: string;
yrkesgrupp: string;
anstallningsform: string;
otherExplanation: string;
omfattning: string;
omfattningPercent: number;
}
export type FormKeys = keyof SlutredovisningFormStep0WorkFormData;

View File

@@ -0,0 +1,69 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { Anstallningsform, Omfattning } from '@msfa-models/slutredovisning.model';
import { SlutredovisningFormStep0WorkErrors } from './slutredovisning-form-step0-work-errors.model';
import { SlutredovisningFormStep0WorkFormData } from './slutredovisning-form-step0-work.model';
export class SlutredovisningFormStep0WorkValidator {
static slutredovisningFormStep0WorkIsValid(): ValidatorFn {
return (c: AbstractControl): SlutredovisningFormStep0WorkErrors => {
let errors: SlutredovisningFormStep0WorkErrors = {};
const {
yrkesomrade,
yrkesgrupp,
anstallningsform,
otherExplanation,
omfattning,
omfattningPercent,
} = c.value as SlutredovisningFormStep0WorkFormData;
if (!yrkesomrade) {
errors = {
...errors,
yrkesomrade: 'Yrkesområde måste väljas',
};
}
if (yrkesomrade && !yrkesgrupp) {
errors = {
...errors,
yrkesgrupp: 'Yrkesgrupp måste väljas',
};
}
if (!anstallningsform) {
errors = {
...errors,
anstallningsform: 'Anställningsform måste väljas',
};
} else if (anstallningsform === Anstallningsform.Annat && !otherExplanation) {
errors = {
...errors,
annatAnstallningComment: 'Beskrivning av annat är obligatorisk om "annat" är vald under anställningsform',
};
}
if (!omfattning) {
errors = {
...errors,
omfattning: 'Omfattning måste väljas',
};
} else if (omfattning === Omfattning.Deltid) {
if (omfattningPercent === undefined || omfattningPercent === null) {
errors = {
...errors,
omfattningPercent: 'Anställningens omfattning i procent är obligatoriskt',
};
} else if (omfattningPercent < 1) {
errors = {
...errors,
omfattningPercent: 'Anställningens omfattning i procent får inte vara mindre än 1% om deltid har valts',
};
} else if (omfattningPercent > 99) {
errors = {
...errors,
omfattningPercent: 'Anställningens omfattning i procent får inte vara mer än 99% om deltid har valts',
};
}
}
return errors;
};
}
}

View File

@@ -0,0 +1,50 @@
<h2>Huvudsaklig sysselsättning</h2>
<form
class="slutredovisning-form-step0"
[formGroup]="formGroup"
name="slutredovisning-form-step0"
(ngSubmit)="verifyAndEmitData()"
>
<digi-form-fieldset
af-legend="Vilken är deltagarens huvudsakliga sysselsättning nu?"
af-name="mainOccupation"
af-form="slutredovisning-form-step0"
>
<ui-radiobutton-group
uiId="slutredovisning-mainOccupation"
[uiRadiobuttons]="mainOccupationRadioButtonGroup"
formControlName="mainOccupation"
[uiRequired]="true"
[uiAnnounceIfOptional]="true"
[uiInvalid]="formControlIsInvalid(mainOccupationFormControl)"
[uiValidationMessage]="mainOccupationFormControl.errors?.required"
></ui-radiobutton-group>
</digi-form-fieldset>
<msfa-slutredovisning-form-step0-education
*ngIf="mainOccupationFormControl.value === MainOccupation.Education"
[formGroupRef]="formGroup"
[shouldValidate]="shouldValidate$ | async"
></msfa-slutredovisning-form-step0-education>
<msfa-slutredovisning-form-step0-still-unemployed
*ngIf="mainOccupationFormControl.value === MainOccupation.StillUnemployed"
[formGroupRef]="formGroup"
[shouldValidate]="shouldValidate$ | async"
></msfa-slutredovisning-form-step0-still-unemployed>
<msfa-slutredovisning-form-step0-work
*ngIf="mainOccupationFormControl.value === MainOccupation.Work"
[formGroupRef]="formGroup"
[shouldValidate]="shouldValidate$ | async"
></msfa-slutredovisning-form-step0-work>
<msfa-slutredovisning-form-step0-other
*ngIf="mainOccupationFormControl.value === MainOccupation.Other"
[formGroupRef]="formGroup"
[shouldValidate]="shouldValidate$ | async"
></msfa-slutredovisning-form-step0-other>
<digi-button af-type="submit">Vidare till steg 2</digi-button>
</form>

View File

@@ -0,0 +1,7 @@
@import 'variables/gutters';
.slutredovisning-form-step0 {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
}

View File

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

View File

@@ -0,0 +1,87 @@
import { RadiobuttonModel } from '@af/digi-ng/_form/form-radiobutton-group';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { markControlsAsDirty } from '@msfa-utils/mark-controls-as-dirty.util';
import { RequiredValidator } from '@msfa-validators/required.validator';
import { BehaviorSubject } from 'rxjs';
import { SlutredovisningFormStep0EducationFormData } from './slutredovisning-form-step0-education/slutredovisning-form-step0-education.component';
import { SlutredovisningFormStep0WorkFormData } from './slutredovisning-form-step0-work/slutredovisning-form-step0-work.model';
import { SlutredovisningFormStep0StillUnemployedFormData } from './slutredovisning-form-step0-still-unemployed/slutredovisning-form-step0-still-unemployed.component';
import { SlutredovisningFormStep0OtherFormData } from './slutredovisning-form-step0-other/slutredovisning-form-step0-other.component';
import { MainOccupation } from '@msfa-models/slutredovisning.model';
export interface SlutredovisningStep0FormData {
mainOccupation: MainOccupation;
education: SlutredovisningFormStep0EducationFormData;
work: SlutredovisningFormStep0WorkFormData[];
stillUnemployed: SlutredovisningFormStep0StillUnemployedFormData;
other: SlutredovisningFormStep0OtherFormData;
}
interface MainOccupationWithLabel {
label: string;
key: string;
}
type FormKeys = keyof SlutredovisningStep0FormData;
@Component({
selector: 'msfa-slutredovisning-form-step0',
templateUrl: './slutredovisning-form-step0.component.html',
styleUrls: ['./slutredovisning-form-step0.component.scss'],
})
export class SlutredovisningFormStep0Component implements OnInit {
shouldValidate$ = new BehaviorSubject<boolean>(false);
@Output() nextClick = new EventEmitter<SlutredovisningStep0FormData>();
@Input() formGroup: FormGroup;
MainOccupation = MainOccupation;
private mainOccupationFormControlName: FormKeys = 'mainOccupation';
mainOccupations: MainOccupationWithLabel[] = [
{ label: 'Arbete', key: MainOccupation.Work },
{ label: 'Utbildning', key: MainOccupation.Education },
{ label: 'Fortsatt arbetssökande', key: MainOccupation.StillUnemployed },
{
label: 'Byte till ny leverantör i rusta och matcha',
key: MainOccupation.ByteTillNyLeverantorIRustaOchMatcha,
},
{ label: 'Annat', key: MainOccupation.Other },
];
get mainOccupationRadioButtonGroup(): RadiobuttonModel[] {
return this.mainOccupations.map(mainOccupation => ({ label: mainOccupation.label, value: mainOccupation.key }));
}
get mainOccupationFormControl(): AbstractControl | undefined {
return this.formGroup.get(this.mainOccupationFormControlName);
}
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate$.value);
}
verifyAndEmitData(): void {
this.mainOccupations
.map(mainOccupations => mainOccupations.key)
.filter(formGroupKey => this.mainOccupationFormControl.value !== formGroupKey)
.forEach(notSelectedFormGroupKey => this.formGroup.removeControl(notSelectedFormGroupKey));
this.shouldValidate$.next(true);
markControlsAsDirty(Object.values(this.formGroup.controls));
this.formGroup.markAllAsTouched();
if (this.formGroup.valid) {
this.nextClick.emit(this.formGroup.value);
}
}
ngOnInit(): void {
if (!this.mainOccupationFormControl) {
this.formGroup.addControl(
this.mainOccupationFormControlName,
new FormControl(null, [RequiredValidator('Val av huvudsysselsättning är obligatoriskt')])
);
}
}
}

View File

@@ -0,0 +1,35 @@
<h2>Aktiviteter</h2>
<form [formGroup]="formGroup" name="report-form" (ngSubmit)="verifyAndEmitData()">
<ng-container *ngIf="activitiesFormArray.controls.length; else loadingRef" [formArrayName]="ACTIVITES_FORM_NAME">
<div
class="slutredovisning-form__activity"
*ngFor="let activityFormGroup of activitiesFormArray.controls; let i=index"
[formGroupName]="i"
>
<h3>{{activityFormGroup.get('name').value}}</h3>
<ui-textarea
formControlName="whatHasBeenDone"
uiLabel="Beskriv vad deltagaren har gjort inom aktiviteten"
[uiAnnounceIfOptional]="true"
[uiMaxLength]="2000"
[uiMinLength]="1"
[uiRequired]="true"
[uiValidationMessage]="activityFormGroup.get('description')?.errors?.required"
[uiInvalid]="activityFormGroup.get('description')?.touched && !!activityFormGroup.get('description')?.errors?.required"
></ui-textarea>
</div>
<!-- TODO: If something is required at the top of the form it will be hard to see when at the bottom and clicking next -->
<footer class="slutredovisning-report-form-step1__footer">
<digi-button af-type="button" af-variation="tertiary" (click)="goBack()">
<digi-icon-arrow-left slot="icon"></digi-icon-arrow-left>
Tillbaka
</digi-button>
<digi-button af-type="submit">Nästa</digi-button>
</footer>
</ng-container>
</form>
<ng-template #loadingRef>
<ui-loader uiType="padded"></ui-loader>
</ng-template>

View File

@@ -0,0 +1,6 @@
.slutredovisning-report-form-step1 {
&__footer {
display: flex;
justify-content: space-between;
}
}

View File

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

View File

@@ -0,0 +1,83 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
import { BehaviorSubject } from 'rxjs';
import { markControlsAsDirty } from '@msfa-utils/mark-controls-as-dirty.util';
import { Activity } from '@msfa-models/activity.model';
import { SlutredovisningFormService } from '../slutredovisning-form.service';
import { take } from 'rxjs/operators';
import { RequiredValidator } from '@msfa-validators/required.validator';
export interface SlutredovisningStep1FormData {
activities: { whatHasBeenDone: string; id: string; name: string }[];
}
type FormControlNames = keyof SlutredovisningStep1FormData;
@Component({
selector: 'msfa-slutredovisning-form-step1',
templateUrl: './slutredovisning-form-step1.component.html',
styleUrls: ['./slutredovisning-form-step1.component.scss'],
})
export class SlutredovisningFormStep1Component implements OnInit {
readonly ACTIVITES_FORM_NAME: FormControlNames = 'activities';
shouldValidate$ = new BehaviorSubject<boolean>(false);
@Output() nextClick = new EventEmitter<SlutredovisningStep1FormData>();
@Output() backClick = new EventEmitter<void>();
@Input() genomforandereferens: number;
@Input() formGroup: FormGroup;
constructor(
private slutredovisningFormService: SlutredovisningFormService,
private changeDetectionRef: ChangeDetectorRef
) {}
private clearActivities(): void {
this.activitiesFormArray.clear();
}
private addActivityToForm(activity: Activity): void {
this.activitiesFormArray.push(
new FormGroup({
name: new FormControl(activity.name),
id: new FormControl(activity.id),
whatHasBeenDone: new FormControl('temp', RequiredValidator('Beskrivning är obligatorisk')),
})
);
this.changeDetectionRef.detectChanges();
}
ngOnInit(): void {
if (!this.activitiesFormArray) {
this.formGroup.addControl(this.ACTIVITES_FORM_NAME, new FormArray([]));
this.slutredovisningFormService
.getAllChosenActivities(this.genomforandereferens)
.pipe(take(1))
.subscribe(activities => {
this.clearActivities();
activities.forEach(activity => this.addActivityToForm(activity));
});
}
}
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate$.value);
}
get activitiesFormArray(): FormArray {
return this.formGroup.get(this.ACTIVITES_FORM_NAME) as FormArray;
}
verifyAndEmitData(): void {
this.shouldValidate$.next(true);
markControlsAsDirty(Object.values(this.formGroup.controls));
this.formGroup.markAllAsTouched();
if (this.formGroup.valid) {
this.nextClick.emit(this.formGroup.value);
}
}
goBack(): void {
this.backClick.emit();
}
}

View File

@@ -0,0 +1,47 @@
<h2>Deltagarens framsteg och utveckling</h2>
<form [formGroup]="formGroup" name="slutredovisning-report-form-step1__form" (ngSubmit)="verifyAndEmitData()">
<ui-textarea
[formControl]="framstegFormControl"
uiLabel="Beskriv deltagarens framsteg och utveckling under perioden"
[uiAnnounceIfOptional]="true"
[uiMaxLength]="2000"
[uiMinLength]="1"
[uiRequired]="true"
[uiValidationMessage]="framstegFormControl.errors?.required"
[uiInvalid]="framstegFormControl?.touched && !!framstegFormControl?.errors?.required"
></ui-textarea>
<ui-textarea
[formControl]="nastaStegFormControl"
uiLabel="Information om lämpligt nästa steg för deltagaren"
[uiAnnounceIfOptional]="true"
[uiMaxLength]="2000"
[uiMinLength]="1"
[uiRequired]="true"
[uiValidationMessage]="nastaStegFormControl.errors?.required"
[uiInvalid]="nastaStegFormControl?.touched && !!nastaStegFormControl?.errors?.required"
></ui-textarea>
<ui-textarea
[formControl]="ovrigtFormControl"
uiLabel="Övrig information"
[uiAnnounceIfOptional]="true"
[uiMaxLength]="2000"
[uiMinLength]="1"
[uiRequired]="true"
[uiValidationMessage]="ovrigtFormControl.errors?.required"
[uiInvalid]="ovrigtFormControl?.touched && !!ovrigtFormControl?.errors?.required"
></ui-textarea>
<footer class="slutredovisning-report-form-step1__footer">
<digi-button af-type="button" af-variation="tertiary" (click)="goBack()">
<digi-icon-arrow-left slot="icon"></digi-icon-arrow-left>
Tillbaka
</digi-button>
<digi-button af-type="submit">Nästa</digi-button>
</footer>
</form>
<ng-template #loadingRef>
<ui-loader uiType="padded"></ui-loader>
</ng-template>

View File

@@ -0,0 +1,13 @@
@import 'variables/gutters';
.slutredovisning-report-form-step1 {
&__form {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
}
&__footer {
display: flex;
justify-content: space-between;
}
}

View File

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

View File

@@ -0,0 +1,65 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { BehaviorSubject } from 'rxjs';
import { markControlsAsDirty } from '@msfa-utils/mark-controls-as-dirty.util';
import { RequiredValidator } from '@msfa-validators/required.validator';
export interface SlutredovisningStep2FormData {
progressDescription: string;
nextStepDescription: string;
otherInformation: string;
}
type FormControlNames = keyof SlutredovisningStep2FormData;
@Component({
selector: 'msfa-slutredovisning-form-step2',
templateUrl: './slutredovisning-form-step2.component.html',
styleUrls: ['./slutredovisning-form-step2.component.scss'],
})
export class SlutredovisningFormStep2Component implements OnInit {
readonly framstegFormName: FormControlNames = 'progressDescription';
readonly nastaStegFormName: FormControlNames = 'nextStepDescription';
readonly ovrigtFormName: FormControlNames = 'otherInformation';
shouldValidate$ = new BehaviorSubject<boolean>(false);
@Output() nextClick = new EventEmitter<SlutredovisningStep2FormData>();
@Output() backClick = new EventEmitter<void>();
@Input() formGroup: FormGroup;
@Input() genomforandereferens: number;
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate$.value);
}
get framstegFormControl(): FormControl {
return this.formGroup.get(this.framstegFormName) as FormControl;
}
get nastaStegFormControl(): FormControl {
return this.formGroup.get(this.nastaStegFormName) as FormControl;
}
get ovrigtFormControl(): FormControl {
return this.formGroup.get(this.ovrigtFormName) as FormControl;
}
verifyAndEmitData(): void {
this.shouldValidate$.next(true);
markControlsAsDirty(Object.values(this.formGroup.controls));
this.formGroup.markAllAsTouched();
if (this.formGroup.valid) {
this.nextClick.emit(this.formGroup.value);
}
}
ngOnInit(): void {
if (!this.framstegFormControl) {
this.formGroup.addControl(this.framstegFormName, new FormControl(null, RequiredValidator()));
this.formGroup.addControl(this.nastaStegFormName, new FormControl(null, RequiredValidator()));
this.formGroup.addControl(this.ovrigtFormName, new FormControl(null, RequiredValidator()));
}
}
goBack(): void {
this.backClick.emit();
}
}

View File

@@ -0,0 +1,52 @@
<div class="slutredovisning-report-form-step3__view">
<h2>Förhandsgranskning</h2>
<div class="informativ-rapport-form__confirmation" *ngIf="submittedDate$ | async as submittedDate; else previewRef">
<digi-notification-alert af-variation="success" af-heading="Allt gick bra" af-heading-level="h3">
<p>Slutredovisning för deltagare {{avrop.fullName}} är nu inskickad till Arbetsförmedlingen.</p>
<dl>
<dt>Datum</dt>
<dd>{{submittedDate | date:'longDate'}} kl {{submittedDate | date:'shortTime'}}</dd>
</dl>
</digi-notification-alert>
<msfa-back-link route="../">Tillbaka till deltagaren</msfa-back-link>
</div>
<ng-template #previewRef>
<ui-loader *ngIf="submitIsLoading$ | async" uiType="absolute"></ui-loader>
<msfa-slutredovisning-view-description-list
[slutredovisning]="slutredovisning$ | async"
></msfa-slutredovisning-view-description-list>
<digi-notification-alert
*ngIf="submitError$ | async as error"
class="slutredovisnin-rapport-form__alert"
af-variation="danger"
af-heading="Någonting gick fel"
>
<p>Kunde inte skicka in Slutredovisning. Försök igen om en stund.</p>
<p *ngIf="error.message" class="msfa__small-text">{{error.message}}</p>
</digi-notification-alert>
<footer class="slutredovisning-report-form-step3__footer">
<digi-button af-type="button" af-variation="tertiary" (click)="goBack()">
<digi-icon-arrow-left slot="icon"></digi-icon-arrow-left>
Tillbaka
</digi-button>
<digi-button af-type="button" (click)="submitSlutredovisning()">Skicka in Slutredovisning</digi-button>
</footer>
</ng-template>
</div>
<!--<ui-loader *ngIf="submitIsLoading$ | async" uiType="absolute"></ui-loader>-->
<!--<msfa-report-description-list [avrop]="avrop">-->
<!-- <dt>Orsak till slutredovisning:</dt>-->
<!-- <dd>bbbb</dd>-->
<!--</msfa-report-description-list>-->
<!--<digi-notification-alert-->
<!-- *ngIf="submitError$ | async as error"-->
<!-- af-variation="danger"-->
<!-- af-heading="Någonting gick fel"-->
<!--&gt;-->
<!-- <p>Kunde inte spara slutredovisning. Ladda om sidan och försök igen.</p>-->
<!-- <p *ngIf="error.message" class="msfa__small-text">{{error.message}}</p>-->
<!--</digi-notification-alert>-->

View File

@@ -0,0 +1,18 @@
@import 'variables/gutters';
.slutredovisning-report-form-step3 {
&__view {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
}
//&__form {
// display: flex;
// flex-direction: column;
// gap: $digi--layout--gutter--l;
//}
&__footer {
display: flex;
justify-content: space-between;
}
}

View File

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

View File

@@ -0,0 +1,71 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { slutredovisningFormDataToSlutredovisningRequest } from '../utils/form-data-to-slutredovisning.util';
import { CustomError } from '@msfa-models/error/custom-error';
import { BehaviorSubject } from 'rxjs';
import { SlutredovisningFormService } from '../slutredovisning-form.service';
import { SlutredovisningResponseMainOccupationDetails } from '@msfa-models/api/slutredovisning.response.model';
import { Avrop } from '@msfa-models/avrop.model';
import { map } from 'rxjs/operators';
import { MainOccupation, Slutredovisning } from '@msfa-models/slutredovisning.model';
import { SlutredovisningFormData } from '../models/slutredovisning-form-data.model';
@Component({
selector: 'msfa-slutredovisning-form-step3',
templateUrl: './slutredovisning-form-step3.component.html',
styleUrls: ['./slutredovisning-form-step3.component.scss'],
})
export class SlutredovisningFormStep3Component {
@Output() backClick = new EventEmitter<void>();
@Input() slutredovisningFormData: SlutredovisningFormData;
@Input() avrop: Avrop;
submitIsLoading$ = new BehaviorSubject<boolean>(false);
submitError$ = new BehaviorSubject<CustomError>(null);
submittedDate$ = new BehaviorSubject<Date | null>(null);
slutredovisning$ = this.slutredovisningFormService.yrkeToTextMap$.pipe(
map(yrkeToText => this._appendYrkeNames(this.slutredovisningFormData, yrkeToText))
);
private _appendYrkeNames(
slutredovisningFormData: SlutredovisningFormData,
yrkeToText: { [key: string]: string }
): Slutredovisning {
if (slutredovisningFormData.mainOccupation.type !== MainOccupation.Work) {
return slutredovisningFormData as Slutredovisning;
}
const newMainOccupation: SlutredovisningResponseMainOccupationDetails = {
...slutredovisningFormData.mainOccupation,
work: slutredovisningFormData.mainOccupation.work.map(yrke => ({
...yrke,
yrkesomradeName: yrkeToText[yrke.yrkesomrade],
yrkesgruppName: yrkeToText[yrke.yrkesgrupp],
})),
};
return { ...slutredovisningFormData, mainOccupation: newMainOccupation };
}
goBack(): void {
this.backClick.emit();
}
constructor(private slutredovisningFormService: SlutredovisningFormService) {}
submitSlutredovisning(): void {
this.submitIsLoading$.next(true);
const toBeSubmitted = slutredovisningFormDataToSlutredovisningRequest(this.slutredovisningFormData);
this.slutredovisningFormService.submitSlutredovisning$(toBeSubmitted).subscribe({
next: () => {
this.submitIsLoading$.next(false);
this.submittedDate$.next(new Date());
},
error: (customError: CustomError) => {
this.submitError$.next({ ...customError, message: customError.error.message });
this.submitIsLoading$.next(false);
throw { ...customError, avoidToast: true };
},
});
}
}

View File

@@ -0,0 +1,95 @@
<msfa-layout>
<msfa-report-layout
*ngIf="avrop$ | async as avrop; else skeletonRef"
[avrop]="avrop$ | async"
[description]="(showAvropDetails$ | async) ? 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet autem iusto natus officiis rerum ullam! Aliquam earum expedita hic in numquam porro totam ullam ut. Et harum in porro quae!' : ''"
reportTitle="Slutredovisning"
[showAvropDetails]="showAvropDetails$ | async"
>
<div class="slutredovisning-form">
<div class="slutredovisning-form__warning" *ngIf="!isAllowedToReport(avrop); else reportRef">
<digi-notification-alert af-variation="warning" af-heading="Kan inte skapa slutredovisning">
<p>{{notAllowedToReportWarning(avrop)}}</p>
</digi-notification-alert>
<msfa-back-link route="../">Tillbaka till deltagaren</msfa-back-link>
</div>
<ng-template #reportRef>
<!-- <digi-progressbar-->
<!-- [afTotalSteps]="totalSteps"-->
<!-- [afCompletedSteps]="getStepIndex(currentStep$ | async)"-->
<!-- af-variation="{${ProgressbarVariation.PRIMARY}}"-->
<!-- ></digi-progressbar>-->
<ng-container [ngSwitch]="currentStep$ | async">
<msfa-slutredovisning-form-step0
*ngSwitchCase="'step0'"
(nextClick)="processStep0Data($event)"
[formGroup]="step0FormGroup"
></msfa-slutredovisning-form-step0>
<msfa-slutredovisning-form-step1
*ngSwitchCase="'step1'"
[genomforandereferens]="avrop.genomforandeReferens"
(nextClick)="processStep1Data($event)"
(backClick)="backFromStep1()"
[formGroup]="step1FormGroup"
></msfa-slutredovisning-form-step1>
<msfa-slutredovisning-form-step2
*ngSwitchCase="'step2'"
[genomforandereferens]="avrop.genomforandeReferens"
(nextClick)="processStep2Data($event)"
(backClick)="backFromStep2()"
[formGroup]="step2FormGroup"
></msfa-slutredovisning-form-step2>
<msfa-slutredovisning-form-step3
*ngSwitchCase="'step3'"
[slutredovisningFormData]="slutredovisningFormData$ | async"
[avrop]="avrop$ | async"
(backClick)="backFromStep3()"
></msfa-slutredovisning-form-step3>
</ng-container>
</ng-template>
</div>
<!-- <h2>Step 0 valueChanges:</h2>-->
<!-- <pre>-->
<!-- {{ step0FormGroup.valueChanges | async | json}}-->
<!-- </pre>-->
<!-- <h2>Step 1 valueChanges:</h2>-->
<!-- <pre>-->
<!-- {{ step1FormGroup.valueChanges | async | json}}-->
<!-- </pre>-->
<!-- <h2>Step 2 valueChanges:</h2>-->
<!-- <pre>-->
<!-- {{ step2FormGroup.valueChanges | async | json}}-->
<!-- </pre>-->
<!-- <h2>Step 0 data:</h2>-->
<!-- <pre>-->
<!-- {{step0FormData$ | async | json}}-->
<!-- </pre-->
<!-- >-->
<!-- <h2>Step 1 data:</h2>-->
<!-- <pre>-->
<!-- {{step1FormData$ | async | json}}-->
<!-- </pre-->
<!-- >-->
<!-- <h2>Step 2 data:</h2>-->
<!-- <pre>-->
<!-- {{step2FormData$ | async | json}}-->
<!-- </pre-->
<!-- >-->
<!-- <h2>requestData$ data:</h2>-->
<!-- <pre>-->
<!-- {{slutredovisning$ | async | json}}-->
<!-- </pre-->
<!-- >-->
</msfa-report-layout>
</msfa-layout>
<ng-template #skeletonRef>
<ui-skeleton [uiCount]="3" uiText="Laddar data för att kunna skapa slutredovisning"></ui-skeleton>
</ng-template>
<ng-template #loadingRef>
<ui-loader uiType="padded"></ui-loader>
</ng-template>

View File

@@ -0,0 +1,19 @@
@import 'variables/gutters';
.slutredovisning-form {
max-width: var(--digi--typography--text--max-width);
&__confirmation,
&__textareas,
&__warning,
&__form {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
}
&__cta-wrapper {
display: flex;
gap: var(--digi--layout--gutter);
}
}

View File

@@ -0,0 +1,40 @@
import { DigiNgFormDatepickerModule } from '@af/digi-ng/_form/form-datepicker';
import { DigiNgFormRadiobuttonGroupModule } from '@af/digi-ng/_form/form-radiobutton-group';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { SlutredovisningFormComponent } from './slutredovisning-form.component';
import { SlutredovisningFormService } from './slutredovisning-form.service';
describe('SlutredovisningReportFormComponent', () => {
let component: SlutredovisningFormComponent;
let fixture: ComponentFixture<SlutredovisningFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [SlutredovisningFormComponent, LayoutComponent],
imports: [
RouterTestingModule,
HttpClientTestingModule,
ReactiveFormsModule,
DigiNgFormRadiobuttonGroupModule,
DigiNgFormDatepickerModule,
],
providers: [SlutredovisningFormService],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SlutredovisningFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,136 @@
import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Avrop } from '@msfa-models/avrop.model';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import { map, shareReplay, switchMap } from 'rxjs/operators';
import { SlutredovisningFormService } from './slutredovisning-form.service';
import { addDays } from 'date-fns';
import { SlutredovisningStep } from './slutredovisning-form.model';
import { SlutredovisningStep0FormData } from './slutredovisning-form-step0/slutredovisning-form-step0.component';
import { SlutredovisningStep1FormData } from './slutredovisning-form-step1/slutredovisning-form-step1.component';
import { SlutredovisningStep2FormData } from './slutredovisning-form-step2/slutredovisning-form-step2.component';
import { formsToSlutredovisningFormData } from './utils/form-data-to-slutredovisning.util';
import { SlutredovisningFormData } from './models/slutredovisning-form-data.model';
interface Params {
genomforandeReferens: string;
}
@Component({
selector: 'msfa-slutredovisning-form',
templateUrl: './slutredovisning-form.component.html',
styleUrls: ['./slutredovisning-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [SlutredovisningFormService],
})
export class SlutredovisningFormComponent implements OnDestroy {
totalSteps = 4;
currentStep$ = this.slutredovisningFormService.currentStep$;
showAvropDetails$ = this.currentStep$.pipe(map(step => step === 'step0' || step === 'step3'));
step0FormGroup: FormGroup = new FormGroup({});
step1FormGroup: FormGroup = new FormGroup({});
step2FormGroup: FormGroup = new FormGroup({});
step0FormData$: Observable<SlutredovisningStep0FormData> = this.slutredovisningFormService.step0FormData$;
step1FormData$: Observable<SlutredovisningStep1FormData> = this.slutredovisningFormService.step1FormData$;
step2FormData$: Observable<SlutredovisningStep2FormData> = this.slutredovisningFormService.step2FormData$;
shouldValidate$ = new BehaviorSubject<boolean>(false);
genomforandeReferens$: Observable<number> = this.activatedRoute.params.pipe(
map((params: Params) => +params.genomforandeReferens)
);
slutredovisningFormData$: Observable<SlutredovisningFormData> = combineLatest([
this.genomforandeReferens$,
this.step0FormData$,
this.step1FormData$,
this.step2FormData$,
]).pipe(
map(([genomforandereferens, step0, step1, step2]) =>
formsToSlutredovisningFormData(genomforandereferens, step0, step1, step2)
)
);
avrop$: Observable<Avrop> = this.genomforandeReferens$.pipe(
switchMap(genomforandeReferens => this.slutredovisningFormService.fetchAvropInformation$(genomforandeReferens)),
shareReplay(1)
);
private subscriptions: Subscription[] = [];
constructor(private slutredovisningFormService: SlutredovisningFormService, private activatedRoute: ActivatedRoute) {}
formControlIsInvalid(formControl: AbstractControl): boolean {
return formControl.invalid && (formControl.touched || this.shouldValidate$.value);
}
openConfirmDialog(): void {
this.shouldValidate$.next(true);
}
private _isAfterStartDate(startDate: Date): boolean {
return new Date() > startDate;
}
private _isBeforeLastPossibleReportDay(endDate: Date): boolean {
const lastPossibleReportDay = addDays(endDate, 5); // Reporting is allowed at latest 5 days past avrop end date.
return lastPossibleReportDay > new Date();
}
isAllowedToReport(avrop: Avrop): boolean {
return this._isAfterStartDate(avrop.startDate) && this._isBeforeLastPossibleReportDay(avrop.endDate);
}
notAllowedToReportWarning(avrop: Avrop): string {
if (!this._isBeforeLastPossibleReportDay(avrop.endDate)) {
return 'Det går inte att göra Slutredovisning eftersom tjänsten har avslutats.';
}
if (!this._isAfterStartDate(avrop.startDate)) {
return 'Det går inte att göra Slutredovisning eftersom tjänsten inte har startat ännu.';
}
}
ngOnDestroy(): void {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
}
getStepIndex(step: SlutredovisningStep): number {
switch (step) {
case 'step0':
return 0;
case 'step1':
return 1;
case 'step2':
return 2;
case 'step3':
return 3;
}
}
backFromStep1() {
this.slutredovisningFormService.setStep('step0');
}
backFromStep2() {
this.slutredovisningFormService.setStep('step1');
}
backFromStep3() {
this.slutredovisningFormService.setStep('step2');
}
processStep0Data(formData: SlutredovisningStep0FormData): void {
this.slutredovisningFormService.processStep0Data(formData);
}
processStep1Data(formData: SlutredovisningStep1FormData) {
this.slutredovisningFormService.processStep1Data(formData);
}
processStep2Data(formData: SlutredovisningStep2FormData) {
this.slutredovisningFormService.processStep2Data(formData);
}
}

View File

@@ -0,0 +1 @@
export type SlutredovisningStep = 'step0' | 'step1' | 'step2' | 'step3';

View File

@@ -0,0 +1,82 @@
import { DigiNgDialogModule } from '@af/digi-ng/_dialog/dialog';
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 { DigiNgFormRangeModule } from '@af/digi-ng/_form/form-range';
import { DigiNgFormSelectModule } from '@af/digi-ng/_form/form-select';
import { DigiNgProgressProgressbarModule } from '@af/digi-ng/_progress/progressbar';
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 { IconModule } from '@msfa-shared/components/icon/icon.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { UiCheckboxModule } from '@ui/checkbox/checkbox.module';
import { UiErrorListModule } from '@ui/error-list/error-list.module';
import { UiInputModule } from '@ui/input/input.module';
import { UiLoaderModule } from '@ui/loader/loader.module';
import { UiRadiobuttonGroupModule } from '@ui/radiobutton-group/radiobutton-group.module';
import { UiSelectModule } from '@ui/select/select.module';
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
import { UiTextareaModule } from '@ui/textarea/textarea.module';
import { ReportDescriptionListModule } from '../../../components/report-description-list/report-description-list.module';
import { ReportLayoutModule } from '../../../components/report-layout/report-layout.module';
import { SlutredovisningViewDescriptionListModule } from '../../../components/slutredovisning-view-description-list/slutredovisning-view-description-list.module';
import { SlutredovisningFormStep0EducationComponent } from './slutredovisning-form-step0/slutredovisning-form-step0-education/slutredovisning-form-step0-education.component';
import { SlutredovisningFormStep0OtherComponent } from './slutredovisning-form-step0/slutredovisning-form-step0-other/slutredovisning-form-step0-other.component';
import { SlutredovisningFormStep0StillUnemployedComponent } from './slutredovisning-form-step0/slutredovisning-form-step0-still-unemployed/slutredovisning-form-step0-still-unemployed.component';
import { SlutredovisningFormStep0WorkComponent } from './slutredovisning-form-step0/slutredovisning-form-step0-work/slutredovisning-form-step0-work.component';
import { SlutredovisningFormStep0Component } from './slutredovisning-form-step0/slutredovisning-form-step0.component';
import { SlutredovisningFormStep1Component } from './slutredovisning-form-step1/slutredovisning-form-step1.component';
import { SlutredovisningFormStep2Component } from './slutredovisning-form-step2/slutredovisning-form-step2.component';
import { SlutredovisningFormStep3Component } from './slutredovisning-form-step3/slutredovisning-form-step3.component';
import { SlutredovisningFormComponent } from './slutredovisning-form.component';
import { SlutredovisningFormService } from './slutredovisning-form.service';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [
SlutredovisningFormComponent,
SlutredovisningFormStep0Component,
SlutredovisningFormStep0EducationComponent,
SlutredovisningFormStep0StillUnemployedComponent,
SlutredovisningFormStep1Component,
SlutredovisningFormStep0WorkComponent,
SlutredovisningFormStep1Component,
SlutredovisningFormStep0OtherComponent,
SlutredovisningFormStep2Component,
SlutredovisningFormStep3Component,
],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: SlutredovisningFormComponent }]),
UiCheckboxModule,
LayoutModule,
ReactiveFormsModule,
DigiNgFormRadiobuttonGroupModule,
UiRadiobuttonGroupModule,
DigiNgFormDatepickerModule,
UiTextareaModule,
DigiNgProgressProgressbarModule,
ReportLayoutModule,
ConfirmDialogModule,
BackLinkModule,
UiSkeletonModule,
DigiNgFormSelectModule,
UiLoaderModule,
UiSelectModule,
ReportDescriptionListModule,
DigiNgFormInputModule,
DigiNgDialogModule,
DigiNgFormRangeModule,
IconModule,
UiErrorListModule,
UiInputModule,
SlutredovisningViewDescriptionListModule,
],
providers: [SlutredovisningFormService],
exports: [SlutredovisningFormComponent],
})
export class SlutredovisningFormModule {}

View File

@@ -0,0 +1,76 @@
import { Injectable } from '@angular/core';
import { Activity, mapResponseToActivity } from '@msfa-models/activity.model';
import { Avrop } from '@msfa-models/avrop.model';
import { mapResponseToYrkesomrade, Yrkesomrade, yrkeToTextMap } from '@msfa-models/yrkesomrade.model';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
import { GemensamPlaneringApiService } from '@msfa-services/api/gemensam-planering-api.service';
import { SlutredovisningApiService } from '@msfa-services/api/slutredovisning.api.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { SlutredovisningStep0FormData } from './slutredovisning-form-step0/slutredovisning-form-step0.component';
import { SlutredovisningStep1FormData } from './slutredovisning-form-step1/slutredovisning-form-step1.component';
import { SlutredovisningStep } from './slutredovisning-form.model';
import { SlutredovisningStep2FormData } from './slutredovisning-form-step2/slutredovisning-form-step2.component';
import { SlutredovisningRequest } from '@msfa-models/api/slutredovisning.request.model';
@Injectable()
export class SlutredovisningFormService {
private _currentStep$ = new BehaviorSubject<SlutredovisningStep>('step0');
currentStep$ = this._currentStep$.asObservable();
private _step0FormData$ = new BehaviorSubject<SlutredovisningStep0FormData | null>(null);
step0FormData$ = this._step0FormData$.asObservable();
private _step1FormData$ = new BehaviorSubject<SlutredovisningStep1FormData | null>(null);
step1FormData$ = this._step1FormData$.asObservable();
private _step2FormData$ = new BehaviorSubject<SlutredovisningStep2FormData | null>(null);
yrkesomraden$: Observable<Yrkesomrade[]> = this.slutredovisningApiService.fetchYrken$().pipe(
map(({ data }) => data.map(yo => mapResponseToYrkesomrade(yo))),
shareReplay(1)
);
yrkeToTextMap$: Observable<{ [key: string]: string }> = this.yrkesomraden$.pipe(
map(yrkesomraden => yrkeToTextMap(yrkesomraden))
);
step2FormData$ = this._step2FormData$.asObservable();
constructor(
private slutredovisningApiService: SlutredovisningApiService,
private deltagareApiService: DeltagareApiService,
private gemensamPlaneringApiService: GemensamPlaneringApiService
) {}
setStep(step: SlutredovisningStep) {
this._currentStep$.next(step);
}
submitSlutredovisning$(slutredovisning: SlutredovisningRequest): Observable<unknown> {
return this.slutredovisningApiService.submitSlutredovisning$(slutredovisning);
}
fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
}
processStep0Data(formData: SlutredovisningStep0FormData) {
this._step0FormData$.next(formData);
this.setStep('step1');
}
processStep1Data(formData: SlutredovisningStep1FormData) {
this._step1FormData$.next(formData);
this.setStep('step2');
}
processStep2Data(formData: SlutredovisningStep2FormData) {
this._step2FormData$.next(formData);
this.setStep('step3');
}
getAllChosenActivities(genomforandeReferens: number): Observable<Activity[]> {
return this.gemensamPlaneringApiService
.fetchAllChosenActivities(genomforandeReferens)
.pipe(map(({ data }) => data.map(activity => mapResponseToActivity(activity))));
}
}

View File

@@ -0,0 +1,94 @@
import { SlutredovisningStep0FormData } from '../slutredovisning-form-step0/slutredovisning-form-step0.component';
import { SlutredovisningStep1FormData } from '../slutredovisning-form-step1/slutredovisning-form-step1.component';
import { SlutredovisningStep2FormData } from '../slutredovisning-form-step2/slutredovisning-form-step2.component';
import {
Anstallningsform,
MainOccupation,
Omfattning,
StillUnemployedReason,
} from '@msfa-models/slutredovisning.model';
import {
SlutredovisningRequest,
SlutredovisningRequestMainOccupationDetails,
} from '@msfa-models/api/slutredovisning.request.model';
import { SlutredovisningFormData } from '../models/slutredovisning-form-data.model';
export function formsToSlutredovisningFormData(
genomforandereferens: number,
step0FormData: SlutredovisningStep0FormData,
step1FormData: SlutredovisningStep1FormData,
step2FormData: SlutredovisningStep2FormData
): SlutredovisningFormData {
if (!genomforandereferens || !step0FormData || !step1FormData || !step2FormData) {
return;
}
let mainOccupation: SlutredovisningRequestMainOccupationDetails;
if (step0FormData.mainOccupation === MainOccupation.Other) {
mainOccupation = {
type: MainOccupation.Other,
other: {
otherExplanation: step0FormData.other.otherExplanation,
},
};
}
if (step0FormData.mainOccupation === MainOccupation.Work) {
mainOccupation = {
type: MainOccupation.Work,
work: step0FormData.work.map(workItem => {
return {
yrkesomrade: workItem.yrkesomrade,
yrkesgrupp: workItem.yrkesgrupp,
omfattning: workItem.omfattning as Omfattning,
omfattningPercent: workItem.omfattningPercent,
otherExplanation: workItem.otherExplanation,
anstallningsform: workItem.anstallningsform as Anstallningsform,
};
}),
};
}
if (step0FormData.mainOccupation === MainOccupation.Education) {
mainOccupation = {
type: MainOccupation.Education,
education: {
educationLength: step0FormData.education.educationLength,
educationLevel: step0FormData.education.educationLevel,
otherExplanation: step0FormData.education.otherExplanation,
educationSpecification: step0FormData.education.educationSpecification,
},
};
}
if (step0FormData.mainOccupation === MainOccupation.StillUnemployed) {
mainOccupation = {
type: MainOccupation.StillUnemployed,
[MainOccupation.StillUnemployed]: {
reasonsGoalNotReached: Object.entries(step0FormData.stillUnemployed.reasonsGoalNotReached)
.map(([reason, isChecked]) => ({ reason: reason as StillUnemployedReason, isChecked }))
.filter(reasonChecked => !!reasonChecked.isChecked)
.map(({ reason }) => reason),
otherExplanation: step0FormData.stillUnemployed.stillUnemployedExplanation,
},
};
}
return {
genomforandereferens: genomforandereferens,
mainOccupation: mainOccupation,
activities: step1FormData.activities,
progressDescription: step2FormData.progressDescription,
nextStepDescription: step2FormData.nextStepDescription,
otherInformation: step2FormData.otherInformation,
};
}
export function slutredovisningFormDataToSlutredovisningRequest(
slutredovisningResponse: SlutredovisningFormData
): SlutredovisningRequest {
return {
...slutredovisningResponse,
activities: slutredovisningResponse.activities.map(({ whatHasBeenDone, id }) => ({ whatHasBeenDone, id })),
};
}

View File

@@ -0,0 +1,19 @@
<msfa-layout>
<msfa-report-layout
*ngIf="avrop$ | async as avrop; else skeletonRef"
reportTitle="Gemensam planering"
[avrop]="avrop"
>
<msfa-slutredovisning-view-description-list
[slutredovisning]="slutredovisning$ | async"
></msfa-slutredovisning-view-description-list>
</msfa-report-layout>
</msfa-layout>
<ng-template #skeletonRef>
<ui-skeleton [uiCount]="3" uiText="Laddar Gemensam planering"></ui-skeleton>
</ng-template>
<ng-template #loadingRef>
<ui-loader uiType="padded"></ui-loader>
</ng-template>

View File

@@ -0,0 +1,31 @@
@import 'mixins/list';
@import 'variables/gutters';
.gemensam-planering-view {
max-width: var(--digi--typography--text--max-width);
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
&__activity-list {
@include msfa__reset-list;
margin-bottom: var(--digi--layout--gutter--s);
}
&__activity-item {
display: flex;
align-items: center;
gap: var(--digi--layout--gutter--s);
margin-top: var(--digi--layout--gutter--s);
}
&__activity-check {
color: var(--digi--ui--color--border--success);
}
&__footer {
display: flex;
flex-direction: column;
gap: var(--digi--layout--gutter);
}
}

View File

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

View File

@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Params } from '@msfa-models/api/params.model';
import { Avrop } from '@msfa-models/avrop.model';
import { Observable } from 'rxjs';
import { map, shareReplay, switchMap } from 'rxjs/operators';
import { SlutredovisningViewService } from './slutredovisning-view.service';
import { Slutredovisning } from '@msfa-models/slutredovisning.model';
@Component({
selector: 'msfa-slutredovisning-view',
templateUrl: './slutredovisning-view.component.html',
styleUrls: ['./slutredovisning-view.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SlutredovisningViewComponent {
params$: Observable<Params> = this.activatedRoute.params.pipe(
map(params => ({
handlingId: params.handlingId as string,
genomforandeReferens: params.genomforandeReferens as string,
}))
);
avrop$: Observable<Avrop> = this.params$.pipe(
switchMap(({ genomforandeReferens }) =>
this.slutredovisningViewService.fetchAvropInformation$(+genomforandeReferens)
),
shareReplay(1)
);
slutredovisning$: Observable<Slutredovisning> = this.params$.pipe(
switchMap(({ handlingId }) => this.slutredovisningViewService.fetchSlutredovisning$(handlingId as string)),
shareReplay(1)
);
constructor(private slutredovisningViewService: SlutredovisningViewService, private activatedRoute: ActivatedRoute) {}
}

View File

@@ -0,0 +1,29 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { BackLinkModule } from '@msfa-shared/components/back-link/back-link.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { UiLoaderModule } from '@ui/loader/loader.module';
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
import { ReportLayoutModule } from '../../../components/report-layout/report-layout.module';
import { SlutredovisningViewComponent } from './slutredovisning-view.component';
import { SlutredovisningViewService } from './slutredovisning-view.service';
import { SlutredovisningViewDescriptionListModule } from '../../../components/slutredovisning-view-description-list/slutredovisning-view-description-list.module';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [SlutredovisningViewComponent],
imports: [
CommonModule,
RouterModule.forChild([{ path: '', component: SlutredovisningViewComponent }]),
LayoutModule,
ReportLayoutModule,
BackLinkModule,
UiLoaderModule,
UiSkeletonModule,
SlutredovisningViewDescriptionListModule,
],
providers: [SlutredovisningViewService],
exports: [SlutredovisningViewComponent],
})
export class SlutredovisningViewModule {}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Avrop } from '@msfa-models/avrop.model';
import { HandlingarApiService } from '@msfa-services/api/handlingar.api.service';
import { Observable } from 'rxjs';
import { DeltagareApiService } from '@msfa-services/api/deltagare.api.service';
import { SlutredovisningApiService } from '@msfa-services/api/slutredovisning.api.service';
import { SlutredovisningResponse } from '@msfa-models/api/slutredovisning.response.model';
import { map } from 'rxjs/operators';
@Injectable()
export class SlutredovisningViewService {
constructor(
private handlingarApiService: HandlingarApiService,
private deltagareApiService: DeltagareApiService,
private gemensamPlaneringApiService: SlutredovisningApiService
) {}
public fetchAvropInformation$(genomforandeReferens: number): Observable<Avrop> {
return this.deltagareApiService.fetchAvropInformation$(genomforandeReferens);
}
public fetchSlutredovisning$(handlingId: string): Observable<SlutredovisningResponse> {
return this.handlingarApiService.fetchSlutredovisning$(handlingId).pipe(map(({ data }) => data));
}
}

View File

@@ -1,34 +1,14 @@
<div class="error-list-wrapper" [hidden]="!validationErrorLinks || validationErrorLinks.length === 0">
<digi-notification-alert
class="error-list"
af-variation="danger"
[attr.af-heading]="headingText"
[af-heading-level]="headingLevel"
[afCloseable]="false"
<digi-form-error-list
*ngIf="validationErrorLinks && validationErrorLinks.length"
[afHeading]="headingText"
[afOverrideLink]="true"
(afOnClickLink)="setFocusOnFormElement($event.detail.detail.target)"
>
<a
msfaAnchorLink
*ngFor="let validationErrorLink of validationErrorLinks"
[attr.href]="'#' + validationErrorLink.elementId"
>
<ul class="error-list__validation-error-links">
<li *ngFor="let validationErrorLink of validationErrorLinks;">
<digi-ng-link-internal
msfaAnchorLink
class="error-list__validation-error-link"
[afHref]="'#' + validationErrorLink.elementId"
[afText]="validationErrorLink.text"
></digi-ng-link-internal>
</li>
</ul>
</digi-notification-alert>
</div>
<!-- <digi-form-error-list
class="edit-employee-form__validation-error-summary"
af-heading="Åtgärda följande fel för att spara dina ändringar:"
*ngIf="validationErrorLinks && validationErrorLinks.length !== 0"
>
Behöver hantera ankarlänkar kopplat till den här komponenten om det ska fungera att använda den...
<a
[attr.href]="'#' + validationErrorLink.elementId"
[attr.id]="first ? firstValidationErrorLinkId : 'validation-error-link-' + index"
*ngFor="let validationErrorLink of validationErrorLinks; let first = first; let index = index"
>
{{ validationErrorLink.text }}
</a>
</digi-form-error-list> -->
{{ validationErrorLink.text }}
</a>
</digi-form-error-list>

View File

@@ -1,5 +1,5 @@
import { TypographyDynamicHeadingLevel } from '@af/digi-ng/_typography/typography-dynamic-heading';
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
export interface ValidationErrorLink {
elementId: string;
@@ -16,4 +16,22 @@ export class ErrorListComponent {
@Input() validationErrorLinks: ValidationErrorLink[] = [];
@Input() headingText: string;
@Input() headingLevel: TypographyDynamicHeadingLevel = TypographyDynamicHeadingLevel.H3;
setFocusOnFormElement(element: HTMLElement): void {
const id = element.getAttribute('href').replace('#', '');
const target = document.getElementById(id);
if (target) {
const targetPosition: DOMRect = target.getBoundingClientRect();
const absoluteElementPosition = targetPosition.top + window.scrollY;
const newPosition = absoluteElementPosition - window.innerHeight / 2;
if (target.focus) {
target.tabIndex = -1;
target.focus();
window.scrollTo(0, newPosition);
target.tabIndex = 0;
}
}
}
}

View File

@@ -5,6 +5,7 @@ export const DELTAGARE_REPORTING_ROUTES = {
avvikelserapport: 'Avvikelserapport (avvikelse)',
signal: 'Signal om arbete eller studier',
'informativ-rapport': 'Informativ rapport',
slutredovisning: 'Slutredovisning',
};
export const NAVIGATION = {

File diff suppressed because it is too large Load Diff

View File

@@ -12,13 +12,17 @@ export class AnchorLinkDirective {
const elementId = href?.trim().replace('#', '');
const element = document.getElementById(elementId);
if (element && element.focus) {
element.tabIndex = -1;
element.focus();
}
if (element) {
const elementPosition: DOMRect = element.getBoundingClientRect();
const absoluteElementPosition = elementPosition.top + window.scrollY;
const newPosition = absoluteElementPosition - window.innerHeight / 2;
if (element.tabIndex < 0) {
element.scrollIntoView();
if (element.focus) {
element.tabIndex = -1;
element.focus();
window.scrollTo(0, newPosition);
element.tabIndex = 0;
}
}
event.stopPropagation();

View File

@@ -16,5 +16,6 @@ export enum Feature {
REPORTING_PERIODISK_REDOVISNING,
REPORTING_INFORMATIV_RAPPORT,
EXPORTS,
SLUTREDOVISNING,
NEWS,
}

View File

@@ -5,6 +5,7 @@ export enum ReportType {
Avvikelse = 'Avvikelserapport (avvikelse)',
Franvaro = 'Avvikelserapport (frånvaro)',
PeriodiskRedovisning = 'Periodisk redovisning',
Slutredovisning = 'Slutredovisning',
InformativRapport = 'Informativ rapport',
InformativRedovisning = 'Informativ rapport',
}

View File

@@ -0,0 +1,68 @@
import {
Anstallningsform,
EducationLength,
EducationLevel,
MainOccupation,
Omfattning,
StillUnemployedReason,
} from '@msfa-models/slutredovisning.model';
export interface SlutredovisningRequestMainOccupationWorkDetails {
yrkesomrade: string;
yrkesgrupp: string;
anstallningsform: Anstallningsform;
otherExplanation: string;
omfattning: Omfattning;
omfattningPercent: number;
}
export interface SlutredovisningRequestMainOccupationWork {
type: MainOccupation.Work;
work: SlutredovisningRequestMainOccupationWorkDetails[];
}
export interface SlutredovisningRequestMainOccupationEducationDetails {
educationLevel: EducationLevel;
otherExplanation: string;
educationLength: EducationLength;
educationSpecification: string;
}
export interface SlutredovisningRequestMainOccupationEducation {
type: MainOccupation.Education;
education: SlutredovisningRequestMainOccupationEducationDetails;
}
export interface SlutredovisningRequestMainOccupationOtherDetails {
otherExplanation: string;
}
export interface SlutredovisningRequestMainOccupationOther {
type: MainOccupation.Other;
other: SlutredovisningRequestMainOccupationOtherDetails;
}
export interface SlutredovisningRequestMainOccupationStillUnemployedDetails {
reasonsGoalNotReached: StillUnemployedReason[];
otherExplanation: string;
}
export interface SlutredovisningRequestMainOccupationStillUnemployed {
type: MainOccupation.StillUnemployed;
stillUnemployed: SlutredovisningRequestMainOccupationStillUnemployedDetails;
}
export type SlutredovisningRequestMainOccupationDetails =
| SlutredovisningRequestMainOccupationWork
| SlutredovisningRequestMainOccupationEducation
| SlutredovisningRequestMainOccupationOther
| SlutredovisningRequestMainOccupationStillUnemployed;
export interface SlutredovisningRequest {
genomforandereferens: number;
mainOccupation: SlutredovisningRequestMainOccupationDetails;
activities: { id: string; whatHasBeenDone: string }[];
progressDescription: string;
nextStepDescription: string;
otherInformation: string;
}

View File

@@ -0,0 +1,70 @@
import {
Anstallningsform,
EducationLength,
EducationLevel,
MainOccupation,
Omfattning,
StillUnemployedReason,
} from '@msfa-models/slutredovisning.model';
export interface SlutredovisningResponseMainOccupationWorkDetails {
yrkesomrade: string;
yrkesgrupp: string;
yrkesomradeName: string;
yrkesgruppName: string;
anstallningsform: Anstallningsform;
otherExplanation: string;
omfattning: Omfattning;
omfattningPercent: number;
}
export interface SlutredovisningResponseMainOccupationWork {
type: MainOccupation.Work;
work: SlutredovisningResponseMainOccupationWorkDetails[];
}
export interface SlutredovisningResponseMainOccupationEducationDetails {
educationLevel: EducationLevel;
otherExplanation: string;
educationLength: EducationLength;
educationSpecification: string;
}
export interface SlutredovisningResponseMainOccupationEducation {
type: MainOccupation.Education;
education: SlutredovisningResponseMainOccupationEducationDetails;
}
export interface SlutredovisningResponseMainOccupationOtherDetails {
otherExplanation: string;
}
export interface SlutredovisningResponseMainOccupationOther {
type: MainOccupation.Other;
other: SlutredovisningResponseMainOccupationOtherDetails;
}
export interface SlutredovisningResponseMainOccupationStillUnemployedDetails {
reasonsGoalNotReached: StillUnemployedReason[];
otherExplanation: string;
}
export interface SlutredovisningResponseMainOccupationStillUnemployed {
type: MainOccupation.StillUnemployed;
stillUnemployed: SlutredovisningResponseMainOccupationStillUnemployedDetails;
}
export type SlutredovisningResponseMainOccupationDetails =
| SlutredovisningResponseMainOccupationWork
| SlutredovisningResponseMainOccupationEducation
| SlutredovisningResponseMainOccupationOther
| SlutredovisningResponseMainOccupationStillUnemployed;
export interface SlutredovisningResponse {
genomforandereferens: number;
mainOccupation: SlutredovisningResponseMainOccupationDetails;
activities: { id: string; whatHasBeenDone: string; name: string }[];
progressDescription: string;
nextStepDescription: string;
otherInformation: string;
}

View File

@@ -0,0 +1,5 @@
export interface YrkesgruppResponse {
id: string;
name: string;
parentId: string;
}

View File

@@ -0,0 +1,7 @@
import { YrkesgruppResponse } from './yrkesgrupp.response.model';
export interface YrkesomradeResponse {
id: string;
name: string;
items: YrkesgruppResponse[];
}

View File

@@ -0,0 +1,108 @@
import { SlutredovisningResponse } from '@msfa-models/api/slutredovisning.response.model';
export enum MainOccupation {
Work = 'work',
Education = 'education',
StillUnemployed = 'stillUnemployed',
ByteTillNyLeverantorIRustaOchMatcha = 'byte till ny leverantör i rusta och matcha',
Other = 'other',
}
export enum EducationLevel {
HogskolaEllerUniversitet = 'högskola eller universitet',
Yrkeshogskola = 'yrkeshögskola',
KomvuxGymnasiumFolkhogskola = 'komvux, gymnasium eller folkhögskola',
VetEj = 'vet ej',
Annat = 'annat',
}
export function educationLevelToString(educationLevel: EducationLevel): string | null {
switch (educationLevel) {
case EducationLevel.HogskolaEllerUniversitet:
return 'Högskola eller universitet';
case EducationLevel.Yrkeshogskola:
return 'Yrkeshögskola';
case EducationLevel.KomvuxGymnasiumFolkhogskola:
return 'Komvux, gymnasium eller folkhögskola';
case EducationLevel.VetEj:
return 'Vet ej';
case EducationLevel.Annat:
return 'Annat';
default:
return null;
}
}
export enum StillUnemployedReason {
SaknarRelevantUtbildning = 'saknar relevant utbildning',
SaknarArbetslivserfarenhet = 'saknar arbetslivserfarenhet',
SaknarNatverkOchKontakter = 'saknar nätverk och kontakter',
BristandeSprakkunskaperISvenska = 'bristande språkkunskaper i svenska',
KonjukturLaget = 'konjunkturläget',
Annat = 'annat',
}
export enum EducationLength {
UpToOneYear = 'upp till ett år',
OneToTwoYears = 'ett år till två år',
OverTwoYears = 'två år eller längre',
}
export function educationLengthToString(educationLength: EducationLength): string | null {
switch (educationLength) {
case EducationLength.UpToOneYear:
return 'Upp till ett år';
case EducationLength.OneToTwoYears:
return 'Ett år till två år';
case EducationLength.OverTwoYears:
return 'Två år eller längre';
default:
return null;
}
}
export enum Anstallningsform {
Tillsvidare = 'tillsvidareanställning',
Prov = 'provanstallning',
Visstid = 'visstidsanställning',
VetEj = 'vet ej',
Annat = 'annat',
}
export enum Omfattning {
Heltid = 'heltid',
Deltid = 'deltid',
VetEj = 'vet ej',
}
export function omfattningToString(omfattning: Omfattning): string | null {
switch (omfattning) {
case Omfattning.Heltid:
return 'Heltid';
case Omfattning.Deltid:
return 'Deltid';
case Omfattning.VetEj:
return 'Vet ej';
default:
return null;
}
}
export type Slutredovisning = SlutredovisningResponse;
export function mainOccupationToString(mainOccupation: MainOccupation): string | null {
switch (mainOccupation) {
case MainOccupation.Education:
return 'Utbildning';
case MainOccupation.StillUnemployed:
return 'Fortsatt arbetssökande';
case MainOccupation.ByteTillNyLeverantorIRustaOchMatcha:
return 'Byte till ny leverantör i Rusta och matcha';
case MainOccupation.Other:
return 'Annat';
case MainOccupation.Work:
return 'Arbete';
default:
return null;
}
}

View File

@@ -0,0 +1,13 @@
import { YrkesgruppResponse } from './api/yrkesgrupp.response.model';
export interface Yrkesgrupp {
value: string;
name: string;
parentId: string;
}
export function mapResponseToYrkesgrupp(data: YrkesgruppResponse): Yrkesgrupp {
const { id, name, parentId } = data;
return { value: id, name, parentId };
}

View File

@@ -0,0 +1,26 @@
import { YrkesomradeResponse } from './api/yrkesomrade.response.model';
import { mapResponseToYrkesgrupp, Yrkesgrupp } from './yrkesgrupp.model';
export interface Yrkesomrade {
value: string;
name: string;
items: Yrkesgrupp[];
}
export function mapResponseToYrkesomrade(data: YrkesomradeResponse): Yrkesomrade {
const { id, name, items } = data;
return { value: id, name, items: items.map(item => mapResponseToYrkesgrupp(item)) };
}
export function yrkeToTextMap(yrkesomraden: Yrkesomrade[]): { [key: string]: string } {
const translateMap: { [key: string]: string } = {};
yrkesomraden.forEach(yrkesomrade => {
translateMap[yrkesomrade.value] = yrkesomrade.name;
yrkesomrade.items.forEach(yrkesgrupp => {
translateMap[yrkesgrupp.value] = yrkesgrupp.name;
});
});
return translateMap;
}

View File

@@ -6,6 +6,7 @@ import { FranvaroReportResponse } from '@msfa-models/api/franvaro-response.model
import { GemensamPlaneringResponse } from '@msfa-models/api/gemensam-planering.response.model';
import { InformativRapportResponse } from '@msfa-models/api/informativ-rapport.response.model';
import { Observable } from 'rxjs';
import { SlutredovisningResponse } from '@msfa-models/api/slutredovisning.response.model';
@Injectable({
providedIn: 'root',
@@ -34,4 +35,8 @@ export class HandlingarApiService {
public fetchAvvikelseReport$(handlingId: string): Observable<{ data: AvvikelseReportResponse }> {
return this.httpClient.get<{ data: AvvikelseReportResponse }>(`${this._apiBaseUrl}/avvikelse/${handlingId}`);
}
public fetchSlutredovisning$(handlingId: string) {
return this.httpClient.get<{ data: SlutredovisningResponse }>(`${this._apiBaseUrl}/slutredovisning/${handlingId}`);
}
}

View File

@@ -0,0 +1,43 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { YRKEN } from '@msfa-constants/yrken';
import { ErrorType } from '@msfa-enums/error-type.enum';
import { environment } from '@msfa-environment';
import { SlutredovisningRequest } from '@msfa-models/api/slutredovisning.request.model';
import { SlutredovisningResponse } from '@msfa-models/api/slutredovisning.response.model';
import { YrkesomradeResponse } from '@msfa-models/api/yrkesomrade.response.model';
import { CustomError } from '@msfa-models/error/custom-error';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class SlutredovisningApiService {
private _apiBaseUrl = `${environment.api.url}/rapporter/slutredovisning`;
private _handlingarBaseUrl = `${environment.api.url}/handlingar`;
constructor(private httpClient: HttpClient) {}
public fetchYrken$(): Observable<{ data: YrkesomradeResponse[] }> {
return of({ data: YRKEN });
}
public fetchSlutredovisning$(handlingId: string): Observable<{ data: SlutredovisningResponse }> {
return this.httpClient.get<{ data: SlutredovisningResponse }>(
`${this._handlingarBaseUrl}/slutredovisning/${handlingId}`
);
}
public submitSlutredovisning$(requestData: SlutredovisningRequest): Observable<void> {
return this.httpClient.post<void>(`${this._apiBaseUrl}`, requestData).pipe(
catchError((error: Error) => {
throw new CustomError({
error,
message: `Kunde inte spara Periodisk redovisning.\n\n${error.message}`,
type: ErrorType.API,
});
})
);
}
}

View File

@@ -0,0 +1,3 @@
export function capitalizeSentence(sentence: string): string {
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
}

View File

@@ -28,6 +28,7 @@ export const ACTIVE_FEATURES_TEST: Feature[] = [
Feature.VERSION_INFO,
Feature.REPORTING_PERIODISK_REDOVISNING,
Feature.REPORTING_INFORMATIV_RAPPORT,
Feature.SLUTREDOVISNING,
Feature.EXPORTS,
Feature.NEWS,
];

View File

@@ -0,0 +1,4 @@
export interface ErrorLink {
elementId: string;
text: string;
}

View File

@@ -0,0 +1,11 @@
<digi-form-error-list
*ngIf="uiErrorLinks && uiErrorLinks.length"
[afHeading]="uiHeadingText"
[afDescription]="uiDescription"
[afOverrideLink]="true"
(afOnClickLink)="setFocusOnFormElement($event.detail.detail.target)"
>
<a *ngFor="let validationErrorLink of uiErrorLinks" [attr.href]="'#' + validationErrorLink.elementId">
{{ validationErrorLink.text }}
</a>
</digi-form-error-list>

View File

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

View File

@@ -0,0 +1,33 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ErrorLink } from './error-link.model';
@Component({
selector: 'ui-error-list',
templateUrl: './error-list.component.html',
styleUrls: ['./error-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ErrorListComponent {
@Input() uiErrorLinks: ErrorLink[] = [];
@Input() uiHeadingText: string;
@Input() uiDescription: string;
setFocusOnFormElement(element: HTMLElement): void {
const id = element.getAttribute('href').replace('#', '');
const target = document.getElementById(id);
if (target) {
const targetPosition: DOMRect = target.getBoundingClientRect();
const absoluteElementPosition = targetPosition.top + window.scrollY;
const newPosition = absoluteElementPosition - window.innerHeight / 2;
if (target.focus) {
console.log(target);
target.tabIndex = -1;
target.focus();
window.scrollTo(0, newPosition);
target.tabIndex = 0;
}
}
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ErrorListComponent } from './error-list.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ErrorListComponent],
imports: [CommonModule],
exports: [ErrorListComponent],
})
export class UiErrorListModule {}

View File

@@ -0,0 +1,16 @@
export enum UiInputType {
COLOR = 'color',
DATE = 'date',
DATETIME_LOCAL = 'datetime-local',
EMAIL = 'email',
HIDDEN = 'hidden',
MONTH = 'month',
NUMBER = 'number',
PASSWORD = 'password',
SEARCH = 'search',
TEL = 'tel',
TEXT = 'text',
TIME = 'time',
URL = 'url',
WEEK = 'week',
}

View File

@@ -0,0 +1,5 @@
export enum UiInputVariation {
S = 's',
M = 'm',
L = 'l',
}

View File

@@ -0,0 +1,23 @@
<div class="ui-input" [ngClass]="{'ui-input--invalid': uiInvalid && uiValidationMessage}">
<digi-form-input
class="ui-input__input"
[afId]="uiId"
[afName]="uiName"
[afLabel]="uiLabel"
[afLabelDescription]="uiDescription"
[afType]="uiType"
[afVariation]="uiVariation"
[afRequired]="uiRequired"
[afAnnounceIfOptional]="uiAnnounceIfOptional"
[afMin]="uiMin"
[afMax]="uiMax"
[afValue]="currentValue"
[afValidation]="uiInvalid ? 'error' : 'neutral'"
(afOnInput)="checkForChange($event.detail.target.value)"
></digi-form-input>
<div class="ui-input__validation" aria-atomic="true" role="alert">
<digi-form-validation-message *ngIf="uiInvalid && uiValidationMessage" af-variation="error"
>{{uiValidationMessage}}</digi-form-validation-message
>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.ui-input {
display: flex;
flex-direction: column;
&--invalid {
gap: var(--digi--layout--gutter--s);
}
}

View File

@@ -0,0 +1,23 @@
/* tslint:disable:no-unused-variable */
import { InputComponent } from './input.component';
export class MockInjector {
get = jest.fn();
}
// tslint:disable-next-line: max-classes-per-file
export class MockChangeDetectorRef {
markForCheck = jest.fn();
detach = jest.fn();
detectChanges = jest.fn();
reattach = jest.fn();
checkNoChanges = jest.fn();
}
describe('InputComponent', () => {
let component: InputComponent;
it('should create', () => {
component = new InputComponent(new MockInjector(), new MockChangeDetectorRef());
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { ReactiveFormsModule } from '@angular/forms';
import { InputComponent } from './input.component';
import { UiInputModule } from './input.module';
export default { title: 'Input', component: InputComponent };
const componentModule = {
moduleMetadata: {
imports: [ReactiveFormsModule, UiInputModule],
},
};
export const standard = () => ({
...componentModule,
template: '<ui-input></ui-input>',
});

View File

@@ -0,0 +1,100 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Injector,
Input,
OnChanges,
Output,
} from '@angular/core';
import { ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { uuid } from '@utils/uuid.util';
import { UiInputType } from './input-type.enum';
import { UiInputVariation } from './input-variation.enum';
/**
* A input input. Implemented with control value accessor
*
* ## Usage
* ``import {UiInputModule} from '@ui/input/input.module';``
*/
@Component({
selector: 'ui-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: InputComponent,
multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InputComponent implements AfterViewInit, ControlValueAccessor, OnChanges {
@Input() uiId: string = uuid();
@Input() uiName: string;
@Input() uiLabel: string = '';
@Input() uiDescription: string;
@Input() uiVariation: UiInputVariation = UiInputVariation.M;
@Input() uiRequired: boolean;
@Input() uiAnnounceIfOptional: boolean = false;
@Input() uiMin: number;
@Input() uiMax: number;
@Input() uiType: UiInputType = UiInputType.TEXT;
@Input() uiInvalid: boolean;
@Input() uiValidationMessage: string;
@Output() uiOnChange: EventEmitter<any> = new EventEmitter();
name: string | number;
onTouched: () => {};
private onChange: (value: any) => {};
private _value: any;
constructor(private injector: Injector, private changeDetectorRef: ChangeDetectorRef) {}
get currentValue(): string {
return this._value || '';
}
ngAfterViewInit(): void {
const ngControl: NgControl = this.injector.get(NgControl, null);
if (ngControl) {
this.name = this.uiName || ngControl.name;
}
}
ngOnChanges(): void {
const ngControl: NgControl = this.injector.get(NgControl, null);
if (ngControl) {
this.name = this.uiName || ngControl.name;
}
}
checkForChange(rawValue: string): void {
const value = this.uiType === UiInputType.NUMBER ? +rawValue : rawValue;
if (this._value !== value) {
if (this.onChange) {
this.onChange(value);
}
this._value = value;
this.uiOnChange.emit(value);
}
}
writeValue(value: any): void {
this._value = value;
this.changeDetectorRef.detectChanges();
}
registerOnChange(fn: (value: string) => {}) {
this.onChange = fn;
}
registerOnTouched(fn: () => {}) {
this.onTouched = fn;
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { InputComponent } from './input.component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [CommonModule],
declarations: [InputComponent],
exports: [InputComponent],
})
export class UiInputModule {}

View File

@@ -1,8 +1,9 @@
<div class="ui-radiobutton-group" [ngClass]="{'ui-radiobutton-group--invalid': uiInvalid}">
<div class="ui-radiobutton-group__radiobuttons" [ngClass]="radiobuttonsModifierClass">
<digi-form-radiobutton
*ngFor="let item of uiRadiobuttons"
*ngFor="let item of uiRadiobuttons; let first = first; let index = index"
class="ui-radiobutton-group__radiobutton"
[afId]="first ? uiId : uiId + '-' + index"
[afLabel]="getLabelText(item.label)"
[afValue]="item.value"
[afChecked]="currentValue === item.value"

View File

@@ -10,6 +10,7 @@ import {
Output,
} from '@angular/core';
import { ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { uuid } from '@utils/uuid.util';
import { RadiobuttonGroupDirection } from './radiobutton-group-direction.enum';
import { Radiobutton } from './radiobutton.model';
@@ -41,6 +42,7 @@ export class RadiobuttonGroupComponent implements ControlValueAccessor, AfterVie
@Input() uiSecondary: boolean;
@Input() uiRequired: boolean;
@Input() uiName: string;
@Input() uiId: string = uuid();
@Input() uiAnnounceIfOptional: boolean;
@Output() uiOnChange = new EventEmitter<any>();
@@ -85,19 +87,20 @@ export class RadiobuttonGroupComponent implements ControlValueAccessor, AfterVie
ngAfterViewInit(): void {
const ngControl: NgControl = this.injector.get(NgControl, null);
if (ngControl) {
this.name = ngControl.name || this.uiName;
this.name = this.uiName || ngControl.name;
}
}
ngOnChanges(): void {
const ngControl: NgControl = this.injector.get(NgControl, null);
if (ngControl) {
this.name = ngControl.name || this.uiName;
this.name = this.uiName || ngControl.name;
}
}
checkForChange(rawValue: any): void {
const value = this._transformValue(rawValue);
console.log(value);
if (this._value !== value) {
if (this.onChange) {
this.onChange(value);

View File

@@ -0,0 +1,4 @@
export interface SelectOption {
value: any;
name: string;
}

View File

@@ -0,0 +1,45 @@
<div class="ui-select" [ngClass]="{'ui-select--invalid': uiInvalid}">
<div class="ui-select__input-wrapper">
<digi-form-label
[afLabel]="uiLabel"
[afAnnounceIfOptional]="uiAnnounceIfOptional"
[afRequired]="uiRequired"
[afFor]="uiId"
[afDescription]="uiDescription"
></digi-form-label>
<div class="ui-select__select-wrapper">
<select
class="ui-select__select"
[ngClass]="{'ui-select__select--invalid': uiInvalid}"
[attr.id]="uiId"
[attr.name]="name"
[attr.required]="uiRequired"
(change)="checkForChange($event.target.value)"
>
<option
*ngIf="uiPlaceholder"
[selected]="!currentValue"
class="ui-select__option ui-select__option--placeholder"
disabled
value=""
>
{{uiPlaceholder}}
</option>
<option
*ngFor="let option of uiOptions"
[selected]="option.value === currentValue"
[attr.value]="option.value"
class="ui-select__option"
>
{{option.name}}
</option>
</select>
<digi-icon-arrow-down aria-hidden="true" class="ui-select__icon" slot="icon"></digi-icon-arrow-down>
</div>
</div>
<div aria-atomic="true" role="alert">
<digi-form-validation-message *ngIf="uiInvalid && uiValidationMessage" af-variation="error"
>{{uiValidationMessage}}</digi-form-validation-message
>
</div>
</div>

View File

@@ -0,0 +1,45 @@
@import 'variables/gutters';
@import 'functions/rem';
.ui-select {
display: flex;
flex-direction: column;
&--invalid {
gap: var(--digi--layout--gutter--xs);
}
&__select-wrapper {
position: relative;
display: flex;
align-items: center;
}
&__select {
appearance: none;
width: 100%;
padding: var(--digi--ui--input--padding);
padding-right: var(--digi--layout--padding--30);
height: var(--digi--ui--input--height);
border-width: rem(1);
cursor: pointer;
font-size: var(--digi--typography--font-size--m);
color: var(--digi--typography--color--text);
background-color: var(--digi--ui--color--background);
border-color: var(--digi--ui--input--border--color);
&--invalid {
border-color: var(--digi--ui--color--border--error);
background-color: var(--digi--ui--color--background--error);
}
}
&__option {
font-size: var(--digi--typography--font-size--l);
}
&__icon {
position: absolute;
right: rem(12);
}
}

Some files were not shown because too many files have changed in this diff Show More