Skip to content

Commit

Permalink
Display open absences hint in presence control #149
Browse files Browse the repository at this point in the history
  • Loading branch information
hupf committed Apr 30, 2020
1 parent d5a112a commit f6b2a35
Show file tree
Hide file tree
Showing 16 changed files with 289 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@
.student {
color: $body-color;
}

@media screen and (max-width: 820px) {
.edit-absences-checkbox {
text-align: left;
}

.presence-category {
text-align: right;
}

.designation-comment {
max-width: initial;
}
}
29 changes: 15 additions & 14 deletions src/app/open-absences/services/open-absences.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';

import { buildTestModuleMetadata } from 'src/spec-helpers';
import { OpenAbsencesService } from './open-absences.service';
import { LessonPresencesRestService } from 'src/app/shared/services/lesson-presences-rest.service';

describe('OpenAbsencesService', () => {
let storeMock: any;
beforeEach(() => {
storeMock = {};
spyOn(localStorage, 'getItem').and.callFake(
(key: string) => storeMock[key] || null
);
spyOn(localStorage, 'setItem').and.callFake(
(key: string) => storeMock[key] || null
);
storeMock['CLX.LoginToken'] =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJvYXV0aCIsImF1ZCI6Imh0dHBzOi8vZGV2NDIwMC8iLCJuYmYiOjE1NjkzOTM5NDMsImV4cCI6MTU2OTQwODM0MywidG9rZW5fcHVycG9zZSI6IlVzZXIiLCJzY29wZSI6IlR1dG9yaW5nIiwiY29uc3VtZXJfaWQiOiJkZXY0MjAwIiwidXNlcm5hbWUiOiJMMjQzMSIsImluc3RhbmNlX2lkIjoiR1ltVEVTVCIsImN1bHR1cmVfaW5mbyI6ImRlLUNIIiwicmVkaXJlY3RfdXJpIjoiaHR0cDovL2xvY2FsaG9zdDo0MjAwIiwiaWRfbWFuZGFudCI6IjIxMCIsImlkX3BlcnNvbiI6IjI0MzEiLCJmdWxsbmFtZSI6IlRlc3QgUnVkeSIsInJvbGVzIjoiTGVzc29uVGVhY2hlclJvbGU7Q2xhc3NUZWFjaGVyUm9sZSIsInRva2VuX2lkIjoiMzc0OSJ9.9lDju5CIIUaISRSz0x8k-kcF7Q6IhN_6HEMOlnsiDRA';

TestBed.configureTestingModule(
buildTestModuleMetadata({ providers: [OpenAbsencesService] })
buildTestModuleMetadata({
providers: [
OpenAbsencesService,
{
provide: LessonPresencesRestService,
useValue: {
getListOfUnconfirmed(): any {
return of([]);
},
},
},
],
})
);
});

it('should be created', () => {
storeMock['CLX.LoginToken'] =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJvYXV0aCIsImF1ZCI6Imh0dHBzOi8vZGV2NDIwMC8iLCJuYmYiOjE1NjkzOTM5NDMsImV4cCI6MTU2OTQwODM0MywidG9rZW5fcHVycG9zZSI6IlVzZXIiLCJzY29wZSI6IlR1dG9yaW5nIiwiY29uc3VtZXJfaWQiOiJkZXY0MjAwIiwidXNlcm5hbWUiOiJMMjQzMSIsImluc3RhbmNlX2lkIjoiR1ltVEVTVCIsImN1bHR1cmVfaW5mbyI6ImRlLUNIIiwicmVkaXJlY3RfdXJpIjoiaHR0cDovL2xvY2FsaG9zdDo0MjAwIiwiaWRfbWFuZGFudCI6IjIxMCIsImlkX3BlcnNvbiI6IjI0MzEiLCJmdWxsbmFtZSI6IlRlc3QgUnVkeSIsInJvbGVzIjoiTGVzc29uVGVhY2hlclJvbGU7Q2xhc3NUZWFjaGVyUm9sZSIsInRva2VuX2lkIjoiMzc0OSJ9.9lDju5CIIUaISRSz0x8k-kcF7Q6IhN_6HEMOlnsiDRA';
const service: OpenAbsencesService = TestBed.inject(OpenAbsencesService);
expect(service).toBeTruthy();
});
Expand Down
22 changes: 3 additions & 19 deletions src/app/open-absences/services/open-absences.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Observable,
Subject,
merge,
forkJoin,
} from 'rxjs';
import { map, shareReplay, take } from 'rxjs/operators';
import { LessonPresence } from 'src/app/shared/models/lesson-presence.model';
Expand All @@ -17,9 +16,7 @@ import {
buildOpenAbsencesEntries,
sortOpenAbsencesEntries,
removeOpenAbsences,
mergeUniqueLessonPresences,
} from '../utils/open-absences-entries';
import { StorageService } from 'src/app/shared/services/storage.service';

export type PrimarySortKey = 'date' | 'name';

Expand All @@ -33,7 +30,6 @@ export class OpenAbsencesService {
loading$ = this.loadingService.loading$;
search$ = new BehaviorSubject<string>('');

private storage = new StorageService();
private updateUnconfirmedAbsences$ = new Subject<
ReadonlyArray<LessonPresence>
>();
Expand Down Expand Up @@ -128,20 +124,8 @@ export class OpenAbsencesService {
}

private loadUnconfirmedAbsences(): Observable<ReadonlyArray<LessonPresence>> {
const tokenPayload = this.storage.getPayload();
const roles = tokenPayload ? tokenPayload.roles : '';
const classTeacher = roles.indexOf('ClassTeacherRole') > 0 ? true : false;
if (classTeacher) {
return this.loadingService.load(
forkJoin([
this.lessonPresencesService.getListOfUnconfirmedLessonTeacher(),
this.lessonPresencesService.getListOfUnconfirmedClassTeacher(),
]).pipe(map(spreadTuple(mergeUniqueLessonPresences)))
);
} else {
return this.loadingService.load(
this.lessonPresencesService.getListOfUnconfirmedLessonTeacher().pipe()
);
}
return this.loadingService.load(
this.lessonPresencesService.getListOfUnconfirmed()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
*ngIf="!isListViewMode"
[studentId]="studentId$ | async"
[link]="['/presence-control/student', entry.lessonPresence.StudentRef.Id]"
class="avatar mr-3 large"
class="avatar large"
></erz-avatar>
<button
type="button"
*ngIf="!entry.canChangePresenceType"
(click)="updatePresenceType(entry)"
class="presence-category designation btn btn-link text-right"
class="presence-category designation btn btn-link"
>
{{ entry.presenceType?.Designation }}
</button>
Expand All @@ -25,8 +25,18 @@
entry.lessonPresence.StudentRef.Id
]"
class="student-name"
>{{ entry.lessonPresence.StudentFullName }}</a
>
<span class="student-name-inner">
{{ entry.lessonPresence.StudentFullName }}
</span>
<span class="unconfirmed-absences">
{{
hasUnconfirmedAbsences
? ('presence-control.entry.unconfirmed-absences' | translate)
: ''
}}
</span>
</a>
<a
*ngIf="entry.canChangePresenceType"
class="comment"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
background-color: $body-bg;
display: grid;
grid-template-areas:
"avatar designation status"
"avatar status designation"
"avatar student-name student-name"
"avatar comment comment";
grid-template-columns: min-content 1fr min-content;
grid-template-columns: min-content min-content 1fr;
}

:host > * {
Expand Down Expand Up @@ -49,23 +49,44 @@ a.comment,

.avatar {
grid-area: avatar;
}

.designation {
padding-top: 0;
padding-bottom: 0;
grid-area: designation;
margin-right: 1.5 * $spacer;
}

.status {
padding-left: 0;
grid-area: status;
}
.status .material-icons {
font-size: 2rem;
}

.designation {
grid-area: designation;
text-align: left;
line-height: 2rem + $btn-padding-y;
}

.student-name {
color: $body-color;
grid-area: student-name;
display: flex;
flex-direction: column;

&:hover,
&:active {
text-decoration: none;
}
}
a:hover,
a:active {
.student-name-inner {
text-decoration: underline;
}
}

.unconfirmed-absences {
color: $absent-color;
font-size: 0.8rem;
line-height: 1;
}

.comment {
Expand All @@ -79,7 +100,21 @@ a.comment,
margin-right: 0.3em;
}

// List layout
// Grid layout specifics
:host.grid {
.status,
.designation {
align-self: start;
margin-left: -$btn-padding-x;
margin-top: -$btn-padding-y;
}

.unconfirmed-absences {
height: 0.8rem; // Always occupy space, event if hint is not present
}
}

// List layout overrides
:host.list {
grid-template-areas: "student-name commentordesignation status";
grid-template-columns: 1fr 2fr min-content;
Expand All @@ -89,6 +124,9 @@ a.comment,
.designation {
grid-area: commentordesignation;
}
.designation {
text-align: right;
}

@media (max-width: 750px) {
grid-template-areas:
Expand All @@ -97,18 +135,3 @@ a.comment,
grid-template-columns: 1fr min-content;
}
}

:host-context(.ie11) {
// To force generation of missing -ms-grid-column-span property
.comment,
.designation {
grid-column: span 1;
}

@media (max-width: 750px) {
.comment,
.designation {
grid-column: span 2;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ViewMode } from '../../services/presence-control-state.service';
})
export class PresenceControlEntryComponent implements OnInit, OnChanges {
@Input() entry: PresenceControlEntry;
@Input() hasUnconfirmedAbsences = false;
@Input() viewMode: ViewMode;

@Output() togglePresenceType = new EventEmitter<PresenceControlEntry>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<erz-presence-control-entry
*ngFor="let entry of data.previouslyAbsentEntries"
[entry]="entry"
[hasUnconfirmedAbsences]="
state.hasUnconfirmedAbsences(entry) | async
"
[viewMode]="state.viewMode$ | async"
(togglePresenceType)="togglePresenceType($event)"
></erz-presence-control-entry>
Expand All @@ -50,6 +53,9 @@
<erz-presence-control-entry
*ngFor="let entry of data.previouslyPresentEntries"
[entry]="entry"
[hasUnconfirmedAbsences]="
state.hasUnconfirmedAbsences(entry) | async
"
[viewMode]="state.viewMode$ | async"
(togglePresenceType)="togglePresenceType($event)"
></erz-presence-control-entry>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe('PresenceControlListComponent', () => {
getBlockLessonPresences: jasmine
.createSpy('getBlockLessonPresences')
.and.callFake(() => of(blockLessons)),
hasUnconfirmedAbsences: () => of(false),
} as unknown) as PresenceControlStateService;

lessonPresencesUpdateServiceMock = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
merge,
Observable,
Subject,
timer,
} from 'rxjs';
import {
map,
Expand All @@ -14,6 +15,8 @@ import {
withLatestFrom,
distinctUntilChanged,
} from 'rxjs/operators';
import { uniq } from 'lodash-es';

import { LessonPresence } from '../../shared/models/lesson-presence.model';
import { Lesson } from '../../shared/models/lesson.model';
import { PresenceType } from '../../shared/models/presence-type.model';
Expand Down Expand Up @@ -73,6 +76,11 @@ export class PresenceControlStateService {
distinctUntilChanged(lessonsEqual)
);

studentIdsWithUnconfirmedAbsences$ = this.selectedDateSubject$.pipe(
switchMap(() => this.loadStudentIdsWithUnconfirmedAbsences()),
shareReplay(1)
);

loading$ = this.loadingService.loading$;

selectedLesson$ = merge(this.currentLesson$, this.selectLesson$).pipe(
Expand Down Expand Up @@ -216,6 +224,12 @@ export class PresenceControlStateService {
);
}

hasUnconfirmedAbsences(entry: PresenceControlEntry): Observable<boolean> {
return this.studentIdsWithUnconfirmedAbsences$.pipe(
map((ids) => ids.indexOf(entry.lessonPresence.StudentRef.Id) > 0)
);
}

/**
* Lesson presences for which the presence type cannot be updated are filtered from the list of block lesson presences
*/
Expand Down Expand Up @@ -258,4 +272,21 @@ export class PresenceControlStateService {
private loadPresenceTypes(): Observable<ReadonlyArray<PresenceType>> {
return this.loadingService.load(this.presenceTypesService.getList());
}

/**
* Starts polling the unconfirmed absences, returns an array with
* student ids that have unconfirmed absences.
*/
private loadStudentIdsWithUnconfirmedAbsences(): Observable<
ReadonlyArray<number>
> {
return timer(
0,
// Only start polling if a refresh time is defined
this.settings.unconfirmedAbsencesRefreshTime || undefined
).pipe(
switchMap(() => this.lessonPresencesService.getListOfUnconfirmed()),
map((unconfirmed) => uniq(unconfirmed.map((p) => p.StudentRef.Id)))
);
}
}
3 changes: 3 additions & 0 deletions src/app/settings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { InjectionToken } from '@angular/core';
import * as t from 'io-ts';

import { Option } from './shared/models/common-types';

const Settings = t.type({
apiUrl: t.string,
scriptsAndAssetsPath: t.string,
Expand All @@ -12,6 +14,7 @@ const Settings = t.type({
unconfirmedAbsenceStateId: t.number,
unexcusedAbsenceStateId: t.number,
excusedAbsenceStateId: t.number,
unconfirmedAbsencesRefreshTime: Option(t.number),
});

type Settings = t.TypeOf<typeof Settings>;
Expand Down
Loading

0 comments on commit f6b2a35

Please sign in to comment.