From 4c0e6da1cde1e4dfe89b4623c6ecb604d5dda7ce Mon Sep 17 00:00:00 2001 From: caebr Date: Tue, 15 Sep 2020 11:16:25 +0200 Subject: [PATCH] Feature/189 incidents (#201) * Replace comment page with incident modal * Incident can only be changed when present * Add form with incident types radio buttons * Save the selected incident type * Remove comment component * Replace comment with incident in labels * Remove update comment service methods * Preselect existing incident * Make incidents deletable * Adapt layout of reason/incident column * Show incident designations on edit button * Separate no incident option with line * Rename output of presence control entry to changeIncident * Align incident button with other elements and with bottom of image * Add sepeerate incidents drop-down * Refactor small issues --- absenzenmanagement.iml | 12 ++ .../edit-absences-header.component.html | 14 +- .../edit-absences-header.component.ts | 15 +- .../edit-absences-list.component.html | 15 +- .../edit-absences-list.component.scss | 8 +- .../edit-absences-list.component.ts | 1 + .../services/edit-absences-state.service.ts | 5 +- .../open-absences-detail.component.html | 3 - .../open-absences-detail.component.scss | 12 +- .../presence-control-comment.component.html | 74 ------- .../presence-control-comment.component.scss | 17 -- ...presence-control-comment.component.spec.ts | 96 --------- .../presence-control-comment.component.ts | 184 ------------------ .../presence-control-entry.component.html | 20 +- .../presence-control-entry.component.scss | 29 ++- .../presence-control-entry.component.ts | 7 + .../presence-control-incident.component.html | 36 ++++ .../presence-control-incident.component.scss | 6 + ...resence-control-incident.component.spec.ts | 30 +++ .../presence-control-incident.component.ts | 58 ++++++ .../presence-control-list.component.html | 2 + .../presence-control-list.component.ts | 26 +++ .../models/presence-control-entry.model.ts | 6 + .../presence-control-routing.module.ts | 10 +- .../presence-control.module.ts | 4 +- .../presence-control-state.service.spec.ts | 25 --- .../presence-control-state.service.ts | 28 +-- .../utils/lesson-presences.spec.ts | 88 +-------- .../utils/lesson-presences.ts | 35 ---- .../utils/presence-types.spec.ts | 8 + .../presence-control/utils/presence-types.ts | 7 +- .../lesson-presences-rest.service.spec.ts | 1 + .../services/lesson-presences-rest.service.ts | 8 + .../shared/services/presence-types.service.ts | 2 +- src/assets/locales/de-CH.json | 15 +- src/assets/locales/fr-CH.json | 13 +- 36 files changed, 290 insertions(+), 630 deletions(-) create mode 100644 absenzenmanagement.iml delete mode 100644 src/app/presence-control/components/presence-control-comment/presence-control-comment.component.html delete mode 100644 src/app/presence-control/components/presence-control-comment/presence-control-comment.component.scss delete mode 100644 src/app/presence-control/components/presence-control-comment/presence-control-comment.component.spec.ts delete mode 100644 src/app/presence-control/components/presence-control-comment/presence-control-comment.component.ts create mode 100644 src/app/presence-control/components/presence-control-incident/presence-control-incident.component.html create mode 100644 src/app/presence-control/components/presence-control-incident/presence-control-incident.component.scss create mode 100644 src/app/presence-control/components/presence-control-incident/presence-control-incident.component.spec.ts create mode 100644 src/app/presence-control/components/presence-control-incident/presence-control-incident.component.ts diff --git a/absenzenmanagement.iml b/absenzenmanagement.iml new file mode 100644 index 000000000..da8860ce3 --- /dev/null +++ b/absenzenmanagement.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.html b/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.html index 694c18bd6..ed1dc82af 100644 --- a/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.html +++ b/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.html @@ -35,6 +35,14 @@
+
+ + +
+
- +
diff --git a/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.ts b/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.ts index 1833b999c..4c0ef1b37 100644 --- a/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.ts +++ b/src/app/edit-absences/components/edit-absences-header/edit-absences-header.component.ts @@ -15,7 +15,10 @@ import { map } from 'rxjs/operators'; import { startOfDay } from 'date-fns'; import { not } from 'src/app/shared/utils/filter'; -import { isComment } from 'src/app/presence-control/utils/presence-types'; +import { + isComment, + isIncident, +} from 'src/app/presence-control/utils/presence-types'; import { DateParserFormatter } from 'src/app/shared/services/date-parser-formatter'; import { createPresenceTypesDropdownItems } from 'src/app/shared/utils/presence-types'; import { EducationalEventsRestService } from '../../../shared/services/educational-events-rest.service'; @@ -46,6 +49,7 @@ export class EditAbsencesHeaderComponent implements OnInit { dateTo: null, presenceType: null, confirmationState: null, + incidentType: null, }; @Output() filterChange = new EventEmitter(); @@ -53,7 +57,14 @@ export class EditAbsencesHeaderComponent implements OnInit { absenceConfirmationStates$ = this.state.absenceConfirmationStates$; presenceTypes$ = this.state.presenceTypes$.pipe( - map((presenceTypes) => presenceTypes.filter(not(isComment))), + map((presenceTypes) => + presenceTypes.filter(not(isComment)).filter(not(isIncident)) + ), + map(createPresenceTypesDropdownItems) + ); + + incidentTypes$ = this.state.presenceTypes$.pipe( + map((presenceTypes) => presenceTypes.filter(isIncident)), map(createPresenceTypesDropdownItems) ); diff --git a/src/app/edit-absences/components/edit-absences-list/edit-absences-list.component.html b/src/app/edit-absences/components/edit-absences-list/edit-absences-list.component.html index 17198f4f1..2c1cfb8f8 100644 --- a/src/app/edit-absences/components/edit-absences-list/edit-absences-list.component.html +++ b/src/app/edit-absences/components/edit-absences-list/edit-absences-list.component.html @@ -77,9 +77,8 @@ {{ 'edit-absences.list.header.confirmation-state' | translate }} - {{ 'edit-absences.list.header.presence-type' | translate - }}
- {{ 'edit-absences.list.header.comment' | translate }} + {{ 'edit-absences.list.header.presence-type' | translate }} / + {{ 'edit-absences.list.header.incident' | translate }} {{ 'edit-absences.list.header.teacher' | translate }} @@ -151,16 +150,12 @@ - {{ - entry.presenceType?.Designation - }}
- {{ entry.lessonPresence.Comment }} + {{ entry.presenceType?.Designation }} - {{ absence.Comment }} -
diff --git a/src/app/open-absences/components/open-absences-detail/open-absences-detail.component.scss b/src/app/open-absences/components/open-absences-detail/open-absences-detail.component.scss index fe7137b4c..483b8ac96 100644 --- a/src/app/open-absences/components/open-absences-detail/open-absences-detail.component.scss +++ b/src/app/open-absences/components/open-absences-detail/open-absences-detail.component.scss @@ -41,9 +41,7 @@ padding: $spacer; border-bottom: 1px solid $gray-200; display: grid; - grid-template-areas: - "checkbox lesson-class time teacher" - "checkbox comment comment comment"; + grid-template-areas: "checkbox lesson-class time teacher"; grid-template-columns: min-content 3fr 1fr 1fr; } @@ -88,11 +86,6 @@ display: none; } -.comment { - color: $gray-500; - grid-area: comment; -} - @media (max-width: 750px) { .content { padding-left: 0; @@ -107,8 +100,7 @@ .absence-entry { grid-template-areas: "checkbox lesson-class" - "checkbox time-teacher" - "checkbox comment"; + "checkbox time-teacher"; grid-template-columns: min-content 1fr; } diff --git a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.html b/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.html deleted file mode 100644 index 61bb33ad7..000000000 --- a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.html +++ /dev/null @@ -1,74 +0,0 @@ -
- - -
-
- notes -
- {{ - (presenceControlEntry$ | async)?.lessonPresence?.Comment || - 'presence-control.comment.placeholder' | translate - }} -
-
- -
- {{ - 'global.validation-errors.' + error.error | translate: error.params - }} -
-
-
-
- - -
-
-
diff --git a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.scss b/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.scss deleted file mode 100644 index 2fab91679..000000000 --- a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import "../../../../bootstrap-variables"; - -.comment-icon, -.comment-field, -.comment-display { - color: $gray-600; -} - -.comment-display { - min-height: 10em; - white-space: pre-line; -} - -.comment-icon, -.comment-display { - cursor: pointer; -} diff --git a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.spec.ts b/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.spec.ts deleted file mode 100644 index b835a49a9..000000000 --- a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { of } from 'rxjs'; -import { withConfig } from 'src/app/rest-error-interceptor'; -import { LessonPresence } from 'src/app/shared/models/lesson-presence.model'; -import { Lesson } from 'src/app/shared/models/lesson.model'; -import { PresenceType } from 'src/app/shared/models/presence-type.model'; -import { LessonPresencesUpdateRestService } from 'src/app/shared/services/lesson-presences-update-rest.service'; -import { - buildLesson, - buildLessonPresence, - buildPresenceControlEntry, - buildPresenceType, -} from 'src/spec-builders'; -import { ActivatedRouteMock, buildTestModuleMetadata } from 'src/spec-helpers'; -import { PresenceControlEntry } from '../../models/presence-control-entry.model'; -import { PresenceControlStateService } from '../../services/presence-control-state.service'; -import { PresenceControlCommentComponent } from './presence-control-comment.component'; -import { StudentBacklinkComponent } from 'src/app/shared/components/student-backlink/student-backlink.component'; - -describe('PresenceControlCommentComponent', () => { - let component: PresenceControlCommentComponent; - let fixture: ComponentFixture; - - let restServiceMock: LessonPresencesUpdateRestService; - let stateServiceMock: PresenceControlStateService; - let activatedRouteMock: ActivatedRouteMock; - - let lessonPresence: LessonPresence; - let lesson: Lesson; - let absence: PresenceType; - let entry: PresenceControlEntry; - - beforeEach(async(() => { - lessonPresence = buildLessonPresence(123, new Date(), new Date(), ''); - lessonPresence.Comment = 'comment'; - lesson = buildLesson(133, new Date(), new Date(), ''); - absence = buildPresenceType(143, true, false); - entry = buildPresenceControlEntry(lessonPresence, absence); - - activatedRouteMock = new ActivatedRouteMock({ - studentId: lessonPresence.StudentRef.Id, - lessonId: lesson.LessonRef.Id, - }); - - stateServiceMock = ({ - getPresenceControlEntry: jasmine - .createSpy('getPresenceControlEntry') - .and.callFake(() => of(entry)), - } as unknown) as PresenceControlStateService; - - restServiceMock = ({ - editLessonPresences: jasmine - .createSpy('editLessonPresences') - .and.callFake(() => of()), - } as unknown) as LessonPresencesUpdateRestService; - - TestBed.configureTestingModule( - buildTestModuleMetadata({ - declarations: [ - PresenceControlCommentComponent, - StudentBacklinkComponent, - ], - providers: [ - { provide: ActivatedRoute, useValue: activatedRouteMock }, - { - provide: PresenceControlStateService, - useValue: stateServiceMock, - }, - { - provide: LessonPresencesUpdateRestService, - useValue: restServiceMock, - }, - ], - }) - ).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PresenceControlCommentComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should update the comment', () => { - component.onSubmit(); - expect(restServiceMock.editLessonPresences).toHaveBeenCalledWith( - [lessonPresence.LessonRef.Id], - [lessonPresence.StudentRef.Id], - undefined, - undefined, - 'comment', - withConfig({ disableErrorHandling: true }) - ); - }); -}); diff --git a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.ts b/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.ts deleted file mode 100644 index 07dc386cd..000000000 --- a/src/app/presence-control/components/presence-control-comment/presence-control-comment.component.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - ElementRef, - OnInit, - ViewChild, -} from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; -import { ToastrService } from 'ngx-toastr'; -import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; -import { - catchError, - finalize, - map, - pluck, - shareReplay, - startWith, - switchMap, - take, - tap, -} from 'rxjs/operators'; -import { withConfig } from 'src/app/rest-error-interceptor'; -import { getValidationErrors } from 'src/app/shared/utils/form'; -import { LessonPresencesUpdateRestService } from 'src/app/shared/services/lesson-presences-update-rest.service'; -import { StudentsRestService } from 'src/app/shared/services/students-rest.service'; -import { PresenceControlStateService } from '../../services/presence-control-state.service'; -import { PresenceControlEntry } from '../../models/presence-control-entry.model'; - -@Component({ - selector: 'erz-presence-control-comment', - templateUrl: './presence-control-comment.component.html', - styleUrls: ['./presence-control-comment.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class PresenceControlCommentComponent implements OnInit { - @ViewChild('commentField') commentField?: ElementRef; - - private params$ = this.route.paramMap.pipe( - map((params) => ({ - studentId: Number(params.get('studentId')), - lessonId: Number(params.get('lessonId')), - })) - ); - - editing$ = new BehaviorSubject(false); - saving$ = new BehaviorSubject(false); - - studentId$ = this.params$.pipe(pluck('studentId')); - student$ = this.studentId$.pipe( - switchMap((id) => this.studentsService.get(id)), - shareReplay(1) - ); - - presenceControlEntry$ = this.params$.pipe( - switchMap(({ studentId, lessonId }) => - this.state.getPresenceControlEntry(studentId, lessonId) - ) - ); - - formGroup$ = this.presenceControlEntry$.pipe( - map(this.createFormGroup.bind(this)), - startWith(this.createFormGroup()), - shareReplay(1) - ); - - commentErrors$ = this.formGroup$.pipe( - switchMap((formGroup) => getValidationErrors(formGroup.get('comment'))), - startWith([]), - shareReplay(1) - ); - - constructor( - private route: ActivatedRoute, - private fb: FormBuilder, - private toastr: ToastrService, - private translate: TranslateService, - private state: PresenceControlStateService, - private studentsService: StudentsRestService, - private lessonPresencesRestService: LessonPresencesUpdateRestService - ) {} - - ngOnInit(): void { - // Disable comment field during saving - combineLatest([this.formGroup$, this.saving$]).subscribe( - ([formGroup, saving]) => { - const control = formGroup.get('comment'); - if (control) { - saving ? control.disable() : control.enable(); - } - } - ); - } - - onCommentIconClick(): void { - this.editing$ - .pipe(take(1)) - .subscribe((editing) => - editing ? this.focusCommentField() : this.startEditing() - ); - } - - startEditing(): void { - this.editing$.next(true); - setTimeout(this.focusCommentField.bind(this)); - } - - stopEditing(): void { - this.editing$.next(false); - } - - cancel(): void { - this.stopEditing(); - } - - onSubmit(): void { - combineLatest([ - this.formGroup$.pipe(take(1)), - this.presenceControlEntry$.pipe(take(1)), - ]) - .pipe( - switchMap(([formGroup, entry]) => { - if (formGroup.valid && entry) { - const comment = formGroup.value.comment; - return this.saveComment(entry, comment).pipe( - tap(() => { - this.state.updateLessonPresenceComment( - entry.lessonPresence, - comment - ); - this.stopEditing(); - }) - ); - } - return of(null); - }), - catchError((error) => this.onSaveError(error)) - ) - .subscribe(); - } - - private createFormGroup( - entry: Option = null - ): FormGroup { - return this.fb.group({ - comment: [ - entry ? entry.lessonPresence.Comment || '' : '', - Validators.maxLength(255), - ], - }); - } - - private focusCommentField(): void { - if (this.commentField) { - this.commentField.nativeElement.focus(); - } - } - - private saveComment( - entry: PresenceControlEntry, - newComment: string - ): Observable { - this.saving$.next(true); - return this.lessonPresencesRestService - .editLessonPresences( - [entry.lessonPresence.LessonRef.Id], - [entry.lessonPresence.StudentRef.Id], - undefined, - undefined, - newComment ? newComment : null, - withConfig({ disableErrorHandling: true }) - ) - .pipe(finalize(() => this.saving$.next(false))); - } - - private onSaveError(error: any): Observable { - console.error('Error while saving comment:', error); - this.toastr.error( - this.translate.instant('presence-control.comment.save-error') - ); - return of(undefined); - } -} diff --git a/src/app/presence-control/components/presence-control-entry/presence-control-entry.component.html b/src/app/presence-control/components/presence-control-entry/presence-control-entry.component.html index 62f4a26b2..43185e6ca 100644 --- a/src/app/presence-control/components/presence-control-entry/presence-control-entry.component.html +++ b/src/app/presence-control/components/presence-control-entry/presence-control-entry.component.html @@ -10,7 +10,9 @@ (click)="updatePresenceType(entry)" class="presence-category designation btn btn-link" > - {{ entry.presenceType?.Designation }} + {{ + entry.presenceType?.Designation + }} + + diff --git a/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.scss b/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.scss new file mode 100644 index 000000000..1fba978be --- /dev/null +++ b/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.scss @@ -0,0 +1,6 @@ +@import "../../../../bootstrap-variables"; + +form > div:first-child { + padding-bottom: $spacer / 2; + border-bottom: 1px solid $gray-200; +} diff --git a/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.spec.ts b/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.spec.ts new file mode 100644 index 000000000..f0c20c382 --- /dev/null +++ b/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.spec.ts @@ -0,0 +1,30 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PresenceControlIncidentComponent } from './presence-control-incident.component'; +import { buildTestModuleMetadata } from 'src/spec-helpers'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +describe('PresenceControlIncidentComponent', () => { + let component: PresenceControlIncidentComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule( + buildTestModuleMetadata({ + declarations: [PresenceControlIncidentComponent], + providers: [NgbActiveModal], + }) + ).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PresenceControlIncidentComponent); + component = fixture.componentInstance; + component.incidentTypes = []; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.ts b/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.ts new file mode 100644 index 000000000..41f8cd3f9 --- /dev/null +++ b/src/app/presence-control/components/presence-control-incident/presence-control-incident.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { PresenceType } from '../../../shared/models/presence-type.model'; +import { TranslateService } from '@ngx-translate/core'; + +interface IncidentOption { + id: Option; + label: Option; +} + +@Component({ + selector: 'erz-presence-control-incident', + templateUrl: './presence-control-incident.component.html', + styleUrls: ['./presence-control-incident.component.scss'], +}) +export class PresenceControlIncidentComponent implements OnInit { + @Input() incident: Option; + @Input() incidentTypes: ReadonlyArray; + incidentOptions: Array = []; + selected: IncidentOption; + + constructor( + public activeModal: NgbActiveModal, + private translate: TranslateService + ) {} + + ngOnInit(): void { + const emptyOption = this.createIncidentOption(); + + this.incidentOptions = this.incidentTypes.map((incidentType) => + this.createIncidentOption(incidentType) + ); + this.incidentOptions.unshift(emptyOption); + + this.selected = + this.incidentOptions.find((option) => option.id === this.incident?.Id) || + emptyOption; + } + + createIncidentOption(incidentType?: PresenceType): IncidentOption { + return { + id: incidentType ? incidentType.Id : null, + label: incidentType + ? incidentType.Designation + : this.translate.instant('presence-control.incident.no-incident'), + }; + } + + onSelectionChange(option: IncidentOption): void { + this.selected = option; + } + + getSelectedIncident(): Option { + return ( + this.incidentTypes.find((type) => type.Id === this.selected?.id) || null + ); + } +} diff --git a/src/app/presence-control/components/presence-control-list/presence-control-list.component.html b/src/app/presence-control/components/presence-control-list/presence-control-list.component.html index 44f5fc4a4..19c7eb3db 100644 --- a/src/app/presence-control/components/presence-control-list/presence-control-list.component.html +++ b/src/app/presence-control/components/presence-control-list/presence-control-list.component.html @@ -44,6 +44,7 @@ returnparams: (state.queryParams$ | async)?.toString() }" (togglePresenceType)="togglePresenceType($event)" + (changeIncident)="changeIncident($event)" > @@ -63,6 +64,7 @@ returnparams: (state.queryParams$ | async)?.toString() }" (togglePresenceType)="togglePresenceType($event)" + (changeIncident)="changeIncident($event)" > diff --git a/src/app/presence-control/components/presence-control-list/presence-control-list.component.ts b/src/app/presence-control/components/presence-control-list/presence-control-list.component.ts index a097fcfef..ff7397a0a 100644 --- a/src/app/presence-control/components/presence-control-list/presence-control-list.component.ts +++ b/src/app/presence-control/components/presence-control-list/presence-control-list.component.ts @@ -26,6 +26,8 @@ import { import { PresenceControlDialogComponent } from '../presence-control-dialog/presence-control-dialog.component'; import { ScrollPositionService } from 'src/app/shared/services/scroll-position.service'; import { parseISOLocalDate } from 'src/app/shared/utils/date'; +import { PresenceControlIncidentComponent } from '../presence-control-incident/presence-control-incident.component'; +import { PresenceTypesService } from '../../../shared/services/presence-types.service'; @Component({ selector: 'erz-presence-control-list', @@ -54,6 +56,7 @@ export class PresenceControlListComponent constructor( public state: PresenceControlStateService, private lessonPresencesUpdateService: LessonPresencesUpdateService, + private presenceTypesService: PresenceTypesService, private modalService: NgbModal, private scrollPosition: ScrollPositionService, private route: ActivatedRoute @@ -112,6 +115,29 @@ export class PresenceControlListComponent }); } + updateIncident(entry: PresenceControlEntry, presenceTypeId: number): void { + this.lessonPresencesUpdateService.updatePresenceTypes( + [entry.lessonPresence], + presenceTypeId + ); + } + + changeIncident(entry: PresenceControlEntry): void { + this.presenceTypesService.incidentTypes$.subscribe((incidentTypes) => { + const modalRef = this.modalService.open(PresenceControlIncidentComponent); + modalRef.componentInstance.incident = + incidentTypes.find((type) => type.Id === entry.presenceType?.Id) || + null; + modalRef.componentInstance.incidentTypes = incidentTypes; + modalRef.result.then( + (selectedIncident) => { + this.updateIncident(entry, selectedIncident?.Id || null); + }, + () => {} + ); + }); + } + private restoreStateFromParams(params: Params): void { if (params.date) { this.state.setDate(parseISOLocalDate(params.date)); diff --git a/src/app/presence-control/models/presence-control-entry.model.ts b/src/app/presence-control/models/presence-control-entry.model.ts index 10a4933d0..4b074b600 100644 --- a/src/app/presence-control/models/presence-control-entry.model.ts +++ b/src/app/presence-control/models/presence-control-entry.model.ts @@ -7,6 +7,8 @@ import { isLate, isDefaultAbsence, canChangePresenceType, + isPresent, + isIncident, } from '../utils/presence-types'; import { DropDownItem } from 'src/app/shared/models/drop-down-item.model'; @@ -73,6 +75,10 @@ export class PresenceControlEntry implements Searchable { ); } + get canChangeIncident(): boolean { + return !isAbsent(this.presenceType); + } + get presenceCategoryIcon(): string { switch (this.presenceCategory) { case 'absent': diff --git a/src/app/presence-control/presence-control-routing.module.ts b/src/app/presence-control/presence-control-routing.module.ts index 6ecf23fa2..7c0d09368 100644 --- a/src/app/presence-control/presence-control-routing.module.ts +++ b/src/app/presence-control/presence-control-routing.module.ts @@ -3,7 +3,6 @@ import { Routes, RouterModule } from '@angular/router'; import { PresenceControlComponent } from './components/presence-control/presence-control.component'; import { PresenceControlListComponent } from './components/presence-control-list/presence-control-list.component'; -import { PresenceControlCommentComponent } from './components/presence-control-comment/presence-control-comment.component'; import { ConfirmAbsencesComponent } from '../shared/components/confirm-absences/confirm-absences.component'; import { StudentProfileComponent } from '../shared/components/student-profile/student-profile.component'; @@ -16,10 +15,7 @@ const routes: Routes = [ path: '', component: PresenceControlListComponent, data: { - restoreScrollPositionFrom: [ - '/presence-control/student/:id', - '/presence-control/comment/:studentId/:lessonId', - ], + restoreScrollPositionFrom: ['/presence-control/student/:id'], }, }, { @@ -35,10 +31,6 @@ const routes: Routes = [ }, ], }, - { - path: 'comment/:studentId/:lessonId', - component: PresenceControlCommentComponent, - }, ], }, ]; diff --git a/src/app/presence-control/presence-control.module.ts b/src/app/presence-control/presence-control.module.ts index 4e60e3d20..2aa77213e 100644 --- a/src/app/presence-control/presence-control.module.ts +++ b/src/app/presence-control/presence-control.module.ts @@ -5,19 +5,19 @@ import { PresenceControlRoutingModule } from './presence-control-routing.module' import { PresenceControlComponent } from './components/presence-control/presence-control.component'; import { PresenceControlHeaderComponent } from './components/presence-control-header/presence-control-header.component'; import { PresenceControlListComponent } from './components/presence-control-list/presence-control-list.component'; -import { PresenceControlCommentComponent } from './components/presence-control-comment/presence-control-comment.component'; import { PresenceControlEntryComponent } from './components/presence-control-entry/presence-control-entry.component'; import { PresenceControlDialogComponent } from './components/presence-control-dialog/presence-control-dialog.component'; import { STUDENT_PROFILE_BACKLINK } from '../shared/tokens/student-profile-backlink'; +import { PresenceControlIncidentComponent } from './components/presence-control-incident/presence-control-incident.component'; @NgModule({ declarations: [ PresenceControlComponent, PresenceControlHeaderComponent, PresenceControlListComponent, - PresenceControlCommentComponent, PresenceControlEntryComponent, PresenceControlDialogComponent, + PresenceControlIncidentComponent, ], providers: [ { provide: STUDENT_PROFILE_BACKLINK, useValue: '/presence-control' }, diff --git a/src/app/presence-control/services/presence-control-state.service.spec.ts b/src/app/presence-control/services/presence-control-state.service.spec.ts index 559981fa0..b7925f46c 100644 --- a/src/app/presence-control/services/presence-control-state.service.spec.ts +++ b/src/app/presence-control/services/presence-control-state.service.spec.ts @@ -196,31 +196,6 @@ describe('PresenceControlStateService', () => { }); }); - describe('.updateLessonPresenceComment', () => { - it('updates the comment of the affected lesson presence', () => { - expectLessonPresencesRequest(); - expectPresenceTypesRequest(); - service.setLesson( - buildLesson( - 3, - new Date(2000, 0, 23, 9, 0), - new Date(2000, 0, 23, 10, 0), - 'Mathematik' - ) - ); - resetCallbackSpies(); - - service.updateLessonPresenceComment(mathEinstein1, 'e = mc^2'); - - expect(selectedLessonCb).not.toHaveBeenCalled(); - expect(selectedPresenceControlEntriesCb).toHaveBeenCalledTimes(1); - - const [entries] = selectedPresenceControlEntriesCb.calls.argsFor(0); - expect(entries.length).toBe(1); - expect(entries[0].lessonPresence.Comment).toBe('e = mc^2'); - }); - }); - describe('.getBlockLessonPresences', () => { it('returns all block lessons for the given entry', () => { expectLessonPresencesRequest(); diff --git a/src/app/presence-control/services/presence-control-state.service.ts b/src/app/presence-control/services/presence-control-state.service.ts index 2e6539f2e..8d96cd436 100644 --- a/src/app/presence-control/services/presence-control-state.service.ts +++ b/src/app/presence-control/services/presence-control-state.service.ts @@ -34,10 +34,7 @@ import { lessonsEqual, } from '../utils/lessons'; import { getCategoryCount } from '../utils/presence-control-entries'; -import { - updatePresenceTypeForPresences, - updateCommentForPresence, -} from '../utils/lesson-presences'; +import { updatePresenceTypeForPresences } from '../utils/lesson-presences'; import { PresenceControlEntry } from '../models/presence-control-entry.model'; import { LessonPresenceUpdate } from '../../shared/services/lesson-presences-update.service'; import { Settings, SETTINGS } from 'src/app/settings'; @@ -171,29 +168,6 @@ export class PresenceControlStateService ); } - updateLessonPresenceComment( - lessonPresence: LessonPresence, - newComment: Option - ): void { - combineLatest([ - this.lessonPresences$.pipe(take(1)), - this.presenceTypes$.pipe(take(1)), - ]) - .pipe( - map(([lessonPresences, presenceTypes]) => - updateCommentForPresence( - lessonPresences, - lessonPresence, - newComment, - presenceTypes - ) - ) - ) - .subscribe((lessonPresences) => - this.updateLessonPresences$.next(lessonPresences) - ); - } - getNextPresenceType( entry: PresenceControlEntry ): Observable> { diff --git a/src/app/presence-control/utils/lesson-presences.spec.ts b/src/app/presence-control/utils/lesson-presences.spec.ts index 06cb0affa..c84e2f80a 100644 --- a/src/app/presence-control/utils/lesson-presences.spec.ts +++ b/src/app/presence-control/utils/lesson-presences.spec.ts @@ -6,10 +6,7 @@ import { buildReference, } from 'src/spec-builders'; import { settings } from 'src/spec-helpers'; -import { - updateCommentForPresence, - updatePresenceTypeForPresences, -} from './lesson-presences'; +import { updatePresenceTypeForPresences } from './lesson-presences'; describe('lesson presences utils', () => { let absent: PresenceType; @@ -137,87 +134,4 @@ describe('lesson presences utils', () => { expect(result[4].Type).toBeNull(); }); }); - - describe('updateCommentForPresence', () => { - it('updates comment of absent lesson presence', () => { - const result = updateCommentForPresence( - presences, - deutschEinsteinAbwesend, - 'Heureka!', - presenceTypes - ); - expect(result.length).toBe(5); - expect(result[0]).toBe(presences[0]); - expect(result[2]).toBe(presences[2]); - expect(result[3]).toBe(presences[3]); - expect(result[4]).toBe(presences[4]); - - expect(result[1].Comment).toBe('Heureka!'); - expect(result[1].Type).toBe('Abwesend'); - expect(result[1].TypeRef).toEqual({ - Id: absent.Id, - HRef: null, - }); - }); - - it('removes comment of absent lesson presence', () => { - const result = updateCommentForPresence( - presences, - deutschEinsteinAbwesend, - null, - presenceTypes - ); - expect(result.length).toBe(5); - expect(result[0]).toBe(presences[0]); - expect(result[2]).toBe(presences[2]); - expect(result[3]).toBe(presences[3]); - expect(result[4]).toBe(presences[4]); - - expect(result[1].Comment).toBeNull(); - expect(result[1].Type).toBe('Abwesend'); - expect(result[1].TypeRef).toEqual({ - Id: absent.Id, - HRef: null, - }); - }); - - it('adds comment to present lesson presence', () => { - const result = updateCommentForPresence( - presences, - deutschFrisch, - 'Meetings sind meist Zeiträuber.', - presenceTypes - ); - expect(result.length).toBe(5); - expect(result[0]).toEqual(presences[0]); - expect(result[1]).toEqual(presences[1]); - expect(result[3]).toEqual(presences[3]); - expect(result[4]).toEqual(presences[4]); - - expect(result[2].Comment).toBe('Meetings sind meist Zeiträuber.'); - expect(result[2].Type).toBeNull(); - expect(result[2].TypeRef).toEqual({ - Id: comment.Id, - HRef: null, - }); - }); - - it('removes comment from present lesson presence', () => { - const result = updateCommentForPresence( - presences, - deutschWalser, - null, - presenceTypes - ); - expect(result.length).toBe(5); - expect(result[0]).toEqual(presences[0]); - expect(result[1]).toEqual(presences[1]); - expect(result[2]).toEqual(presences[2]); - expect(result[4]).toEqual(presences[4]); - - expect(result[3].Comment).toBeNull(); - // expect(result[3].Type).toBeNull(); - // expect(result[3].TypeRef).toBeNull(); - }); - }); }); diff --git a/src/app/presence-control/utils/lesson-presences.ts b/src/app/presence-control/utils/lesson-presences.ts index 3e5439e31..a59ab67dc 100644 --- a/src/app/presence-control/utils/lesson-presences.ts +++ b/src/app/presence-control/utils/lesson-presences.ts @@ -40,41 +40,6 @@ export function updatePresenceTypeForPresences( }); } -export function updateCommentForPresence( - allLessonPresences: ReadonlyArray, - affectedLessonPresence: LessonPresence, - newComment: Option, - presenceTypes: ReadonlyArray -): ReadonlyArray { - return allLessonPresences.map((lessonPresence) => { - if (lessonPresenceEquals(lessonPresence, affectedLessonPresence)) { - let presenceTypeRef = lessonPresence.TypeRef; - let presenceDesignation = lessonPresence.Type; - let newPresenceType: Maybe; - if (newComment && !presenceTypeRef.Id) { - // Set to comment presence type - newPresenceType = presenceTypes.find((p) => p.IsComment); - } else if (!newComment && presenceTypeRef) { - // TODO: Unset presence type if it has `IsComment=1`? - } - if (newPresenceType) { - presenceTypeRef = { - Id: newPresenceType.Id, - HRef: null, - }; - presenceDesignation = newPresenceType.Designation; - } - return { - ...lessonPresence, - Comment: newComment, - TypeRef: presenceTypeRef, - Type: presenceDesignation, - }; - } - return lessonPresence; - }); -} - function lessonPresenceEquals(a: LessonPresence, b: LessonPresence): boolean { return ( a.LessonRef.Id === b.LessonRef.Id && a.StudentRef.Id === b.StudentRef.Id diff --git a/src/app/presence-control/utils/presence-types.spec.ts b/src/app/presence-control/utils/presence-types.spec.ts index cf35d07df..fbdff43e7 100644 --- a/src/app/presence-control/utils/presence-types.spec.ts +++ b/src/app/presence-control/utils/presence-types.spec.ts @@ -11,6 +11,7 @@ describe('presence types', () => { let absenceType: PresenceType; let lateType: PresenceType; let commentType: PresenceType; + let incidentType: PresenceType; let lessonPresenceConfirmed: LessonPresence; let lessonPresence: LessonPresence; @@ -43,6 +44,7 @@ describe('presence types', () => { ); lateType = buildPresenceType(settings.latePresenceTypeId, false, true); commentType = buildPresenceType(6, false, false, true); + incidentType = buildPresenceType(14, false, true, false); }); describe('.getNewConfirmationStateId', () => { @@ -87,5 +89,11 @@ describe('presence types', () => { canChangePresenceType(lessonPresence, null, settings) ).toBeTruthy(); }); + + it('should return true if is incident type', () => { + expect( + canChangePresenceType(lessonPresence, incidentType, settings) + ).toBeTruthy(); + }); }); }); diff --git a/src/app/presence-control/utils/presence-types.ts b/src/app/presence-control/utils/presence-types.ts index ec5d3d5cb..45d217e43 100644 --- a/src/app/presence-control/utils/presence-types.ts +++ b/src/app/presence-control/utils/presence-types.ts @@ -10,6 +10,10 @@ export function isComment(presenceType: Option): boolean { return Boolean(presenceType && presenceType.IsComment); } +export function isIncident(presenceType: Option): boolean { + return Boolean(presenceType && presenceType.IsIncident); +} + export function isAbsent(presenceType: Option): boolean { return Boolean( presenceType && @@ -46,7 +50,8 @@ export function canChangePresenceType( ): boolean { if ( (isPresent(presenceType) && lessonPresence.ConfirmationStateId === null) || - isComment(presenceType) + isComment(presenceType) || + isIncident(presenceType) ) { return true; } diff --git a/src/app/shared/services/lesson-presences-rest.service.spec.ts b/src/app/shared/services/lesson-presences-rest.service.spec.ts index 5c547b027..2cabdf78c 100644 --- a/src/app/shared/services/lesson-presences-rest.service.spec.ts +++ b/src/app/shared/services/lesson-presences-rest.service.spec.ts @@ -259,6 +259,7 @@ describe('LessonPresencesRestService', () => { dateTo: null, presenceType: null, confirmationState: null, + incidentType: null, }; }); diff --git a/src/app/shared/services/lesson-presences-rest.service.ts b/src/app/shared/services/lesson-presences-rest.service.ts index a5511acaa..9a35785c5 100644 --- a/src/app/shared/services/lesson-presences-rest.service.ts +++ b/src/app/shared/services/lesson-presences-rest.service.ts @@ -106,6 +106,7 @@ export class LessonPresencesRestService extends RestService< [absencesFilter.educationalEvent, 'EventRef'], [absencesFilter.studyClass, 'StudyClassRef'], [absencesFilter.presenceType, 'TypeRef'], + [absencesFilter.incidentType, 'TypeRef'], [absencesFilter.confirmationState, 'ConfirmationStateId'], ], new HttpParams({ fromObject: additionalParams }) @@ -135,6 +136,13 @@ export class LessonPresencesRestService extends RestService< } } + if (absencesFilter.incidentType && absencesFilter.presenceType) { + params = params.set( + 'filter.TypeRef', + `=${absencesFilter.presenceType},${absencesFilter.incidentType}` + ); + } + return this.http .get(`${this.baseUrl}/`, { params: paginatedParams(offset, this.settings.paginationLimit, params), diff --git a/src/app/shared/services/presence-types.service.ts b/src/app/shared/services/presence-types.service.ts index 3ca7bd102..efc12ebc7 100644 --- a/src/app/shared/services/presence-types.service.ts +++ b/src/app/shared/services/presence-types.service.ts @@ -74,7 +74,7 @@ export class PresenceTypesService { private filterIncidentTypes( presenceTypes: ReadonlyArray ): ReadonlyArray { - return presenceTypes.filter((t) => t.IsIncident); + return presenceTypes.filter((t) => t.IsIncident && t.Active); } private isHalfDayActive(presenceTypes: ReadonlyArray): boolean { diff --git a/src/assets/locales/de-CH.json b/src/assets/locales/de-CH.json index d37fb71b1..e6614ea1b 100644 --- a/src/assets/locales/de-CH.json +++ b/src/assets/locales/de-CH.json @@ -39,15 +39,15 @@ "search-by-name": "Name suchen..." }, "entry": { - "comment": "Kommentar...", + "incident": "Vorfall erfassen", "update-warning": "Statuswechsel nicht möglich", "unconfirmed-absences": "Offene Absenzen" }, - "comment": { - "placeholder": "Kommentar...", + "incident": { + "text": "Vorfall erfassen:", + "no-incident": "Kein Vorfall", "cancel": "Abbrechen", - "save": "Übernehmen", - "save-error": "Beim Speichern des Kommentars ist ein Fehler aufgetreten." + "save": "Übernehmen" }, "dialog": { "text": "Die ausgewählte Lektion ist Teil eines Blocks. Für welche Lektionen möchten Sie den Präsenzstatus ändern?", @@ -96,6 +96,7 @@ "date-to": "Datum bis", "presence-type": "Grund", "confirmation-state": "Status", + "incident": "Vorfall", "show": "Anzeigen" }, "list": { @@ -109,11 +110,11 @@ "time": "Zeit", "confirmation-state": "Status", "presence-type": "Grund", - "comment": "Kommentar", + "incident": "Vorfall", "date": "Datum", "teacher": "Lehrkraft", "mobil-student-module-instance-study-class": "Name, Fach, Klasse", - "mobil-presence-type-comment": "Kommentar, Grund" + "mobil-presence-type-incident": "Grund / Vorfall" } }, "edit": { diff --git a/src/assets/locales/fr-CH.json b/src/assets/locales/fr-CH.json index eac2215c0..cd1cdeabe 100644 --- a/src/assets/locales/fr-CH.json +++ b/src/assets/locales/fr-CH.json @@ -38,16 +38,10 @@ "search-by-name": "Rechercher un nom..." }, "entry": { - "comment": "Commentaire...", + "incident": "Enregistrer incident", "update-warning": "Il n'est pas possible de modifier le statut.", "unconfirmed-absences": "Absences en suspens" }, - "comment": { - "placeholder": "Commentaire...", - "cancel": "Annuler", - "save": "Valider", - "save-error": "Une erreur s'est produite lors de l'enregistrement du commentaire." - }, "dialog": { "text": "La leçon sélectionnée fait partie d'un bloc. Pour quelle(s) période(s) voulez-vous changer le statut de présence ?", "cancel": "Annuler", @@ -94,6 +88,7 @@ "date-from": "Date du", "date-to": "Date au", "presence-type": "Motif", + "incident": "Incident", "confirmation-state": "Statut", "show": "Afficher" }, @@ -108,11 +103,11 @@ "time": "Période", "confirmation-state": "Statut", "presence-type": "Motif", - "comment": "Commentaire", + "incident": "Incident", "date": "Date", "teacher": "Enseignant-e", "mobil-student-module-instance-study-class": "Nom, Discipline, Classe", - "mobil-presence-type-comment": "Commentaire, Motif" + "mobil-presence-type-incident": "Motif / Incident" } }, "edit": {