Skip to content

Commit

Permalink
CSCEXAM-478 Check exception events when deciding calendar view limits
Browse files Browse the repository at this point in the history
  • Loading branch information
lupari committed Feb 10, 2024
1 parent 250f7ca commit 79eee96
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 86 deletions.
32 changes: 23 additions & 9 deletions ui/src/app/calendar/booking-calendar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export class BookingCalendarComponent implements OnInit, OnChanges, AfterViewIni
@ViewChild('fc') calendar!: FullCalendarComponent;

calendarOptions = signal<CalendarOptions>({});
searchStart = DateTime.now().startOf('week').toISO();
searchEnd = DateTime.now().endOf('week').toISO();

constructor(
private translate: TranslateService,
Expand All @@ -94,11 +96,9 @@ export class BookingCalendarComponent implements OnInit, OnChanges, AfterViewIni
events: this.refetch,
eventClick: this.eventClicked.bind(this),
});
this.translate.onLangChange.subscribe((event) => {
this.calendarOptions.set({ ...this.calendarOptions(), locale: event.lang });
//this.calendar.getApi().destroy();
//this.calendar.getApi().render();
});
this.translate.onLangChange.subscribe((event) =>
this.calendarOptions.set({ ...this.calendarOptions(), locale: event.lang }),
);
}

ngOnInit() {
Expand All @@ -118,19 +118,19 @@ export class BookingCalendarComponent implements OnInit, OnChanges, AfterViewIni

ngOnChanges(changes: SimpleChanges) {
if (changes.room && this.room) {
const earliestOpening = this.Calendar.getEarliestOpening(this.room);
const earliestOpening = this.Calendar.getEarliestOpening(this.room, this.searchStart, this.searchEnd);
const minTime =
earliestOpening.getHours() > 1
? DateTime.fromJSDate(earliestOpening).minus({ hour: 1 }).toJSDate()
: earliestOpening;
const latestClosing = this.Calendar.getLatestClosing(this.room);
const latestClosing = this.Calendar.getLatestClosing(this.room, this.searchStart, this.searchEnd);
const maxTime =
latestClosing.getHours() < 23
? DateTime.fromJSDate(latestClosing).plus({ hour: 1 }).toJSDate()
: latestClosing;
this.calendarOptions.update((cos) => ({
...cos,
hiddenDays: this.Calendar.getClosedWeekdays(this.room),
hiddenDays: this.Calendar.getClosedWeekdays(this.room, this.searchStart, this.searchEnd),
slotMinTime: DateTime.fromJSDate(minTime).toFormat('HH:mm:ss'),
slotMaxTime: DateTime.fromJSDate(maxTime).toFormat('HH:mm:ss'),
timeZone: this.room.localTimezone,
Expand All @@ -141,8 +141,22 @@ export class BookingCalendarComponent implements OnInit, OnChanges, AfterViewIni
this.calendar.getApi().refetchEvents();
}
}
refetch = (input: { startStr: string; timeZone: string }, success: (events: EventInput[]) => void) =>

refetch = (input: { startStr: string; timeZone: string }, success: (events: EventInput[]) => void) => {
this.searchStart = input.startStr;
this.searchEnd = DateTime.fromISO(input.startStr).endOf('week').toISO() as string;
const hidden = this.Calendar.getClosedWeekdays(this.room, this.searchStart, this.searchEnd);
const earliestOpening = this.Calendar.getEarliestOpening(this.room, this.searchStart, this.searchEnd);
const latestClosing = this.Calendar.getLatestClosing(this.room, this.searchStart, this.searchEnd);
this.calendarOptions.update((cos) => ({
...cos,
hiddenDays: hidden,
slotMinTime: DateTime.fromJSDate(earliestOpening).toFormat('HH:mm:ss'),
slotMaxTime: DateTime.fromJSDate(latestClosing).toFormat('HH:mm:ss'),
}));

this.moreEventsNeeded.emit({ date: input.startStr, timeZone: input.timeZone, success: success });
};

eventClicked(arg: EventClickArg): void {
if (arg.event.extendedProps?.availableMachines > 0) {
Expand Down
53 changes: 33 additions & 20 deletions ui/src/app/calendar/calendar.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DateTime } from 'luxon';
import { DateTime, Interval } from 'luxon';
import type { Observable } from 'rxjs';
import { ExamEnrolment } from '../enrolment/enrolment.model';
import { Course, Exam, ExamSection, MaintenancePeriod } from '../exam/exam.model';
Expand Down Expand Up @@ -68,7 +68,7 @@ export class CalendarService {
constructor(
private http: HttpClient,

private DateTime: DateTimeService,
private DateTimeService: DateTimeService,
private Session: SessionService,
) {}

Expand Down Expand Up @@ -101,7 +101,7 @@ export class CalendarService {
const lang = this.Session.getUser().lang;
const locale = lang.toLowerCase() + '-' + lang.toUpperCase();
const options: Intl.DateTimeFormatOptions = { weekday: 'short' };
const weekday = this.DateTime.getDateForWeekday;
const weekday = this.DateTimeService.getDateForWeekday;
return {
SUNDAY: { ord: 7, name: weekday(0).toLocaleDateString(locale, options) },
MONDAY: { ord: 1, name: weekday(1).toLocaleDateString(locale, options) },
Expand Down Expand Up @@ -163,30 +163,41 @@ export class CalendarService {
return room.calendarExceptionEvents.map((e) => this.formatExceptionEvent(e, room.localTimezone));
}

getEarliestOpening(room: ExamRoom): Date {
getEarliestOpening(room: ExamRoom, start: string, end: string): Date {
const tz = room.localTimezone;
const openings = room.defaultWorkingHours.map((dwh) => {
const start = DateTime.fromISO(dwh.startTime, { zone: tz });
return DateTime.now().set({ hour: start.hour, minute: start.minute, second: start.second });
});
return DateTime.min(...openings)
.set({ minute: 0 })
.toJSDate();
const regularOpenings = room.defaultWorkingHours.map((dwh) =>
this.normalize(DateTime.fromISO(dwh.startTime, { zone: tz })),
);
const extraOpenings = room.calendarExceptionEvents
.filter((e) => !e.outOfService && e.startDate >= start && e.endDate <= end)
.flatMap((d) => this.daysBetween(DateTime.fromISO(d.startDate), DateTime.fromISO(d.endDate)))
.map((d) => this.normalize(d.start as DateTime));

return DateTime.min(...regularOpenings.concat(extraOpenings)).toJSDate();
}

getLatestClosing(room: ExamRoom): Date {
getLatestClosing(room: ExamRoom, start: string, end: string): Date {
const tz = room.localTimezone;
const closings = room.defaultWorkingHours.map((dwh) => {
const end = DateTime.fromISO(dwh.endTime, { zone: tz });
return DateTime.now().set({ hour: end.hour, minute: end.minute, second: end.second });
});
return DateTime.max(...closings).toJSDate();
const regularClosings = room.defaultWorkingHours.map((dwh) =>
this.normalize(DateTime.fromISO(dwh.endTime, { zone: tz })),
);
const extraClosings = room.calendarExceptionEvents
.filter((e) => !e.outOfService && e.startDate >= start && e.endDate <= end)
.flatMap((d) => this.daysBetween(DateTime.fromISO(d.startDate), DateTime.fromISO(d.endDate)))
.map((d) => this.normalize(d.end as DateTime));

return DateTime.max(...regularClosings.concat(extraClosings)).toJSDate();
}

getClosedWeekdays(room: ExamRoom): number[] {
getClosedWeekdays(room: ExamRoom, start: string, end: string): number[] {
const weekdays = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'];
const openedDays = room.defaultWorkingHours.map((dwh) => weekdays.indexOf(dwh.weekday));
return [0, 1, 2, 3, 4, 5, 6].filter((x) => openedDays.indexOf(x) === -1);
const regularDays = room.defaultWorkingHours.map((d) => weekdays.indexOf(d.weekday));
const extraDays = room.calendarExceptionEvents
.filter((e) => !e.outOfService && e.startDate >= start && e.endDate <= end)
.flatMap((d) => this.daysBetween(DateTime.fromISO(d.startDate), DateTime.fromISO(d.endDate)))
.map((d) => (d.start?.weekday === 7 ? 0 : (d.start as DateTime).weekday - 1)); // locale nuisances

return [0, 1, 2, 3, 4, 5, 6].filter((x) => regularDays.concat(extraDays).indexOf(x) === -1);
}

listRooms$ = () => this.http.get<ExamRoom[]>('/app/rooms');
Expand All @@ -211,6 +222,8 @@ export class CalendarService {
getExamInfo$ = (collaborative: boolean, id: number) =>
this.http.get<ExamInfo>(collaborative ? `/app/iop/exams/${id}/info` : `/app/student/exam/${id}/info`);

private daysBetween = (start: DateTime, end: DateTime) => Interval.fromDateTimes(start, end).splitBy({ day: 1 });
private normalize = (d: DateTime) => DateTime.now().set({ hour: d.hour, minute: d.minute, second: d.second });
private adjustBack(date: DateTime): string {
const offset = date.isInDST ? 1 : 0;
return date.toUTC().plus({ hour: offset }).toISO() as string;
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/calendar/helpers/selected-room.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { CalendarService } from '../calendar.service';
</div>
</div>
}
@if (exceptionHours.length > 0) {
@if (exceptionHours().length > 0) {
<div class="row mt-2">
<div class="col-md-2 col-12">{{ 'i18n_exception_datetimes' | translate }}:</div>
<div class="col-md-10 col-12">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import type { OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { parseISO } from 'date-fns';
import type { Exam, ExaminationEventConfiguration } from '../../../exam/exam.model';
import { DateTime } from 'luxon';

@Component({
selector: 'xm-select-examination-event-dialog',
Expand Down Expand Up @@ -84,7 +84,11 @@ export class SelectExaminationEventDialogComponent implements OnInit {
ngOnInit() {
// for all confs over
this.configs = this.exam.examinationEventConfigurations
.filter((ec) => parseISO(ec.examinationEvent.start) > new Date() && ec.id !== this.existingEventId)
.filter(
(ec) =>
DateTime.fromISO(ec.examinationEvent.start).toJSDate() > new Date() &&
ec.id !== this.existingEventId,
)
.sort((a, b) => (a.examinationEvent.start < b.examinationEvent.start ? -1 : 1));
}

Expand Down
9 changes: 7 additions & 2 deletions ui/src/app/enrolment/finished/exam-feedback.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import { HttpClient } from '@angular/common/http';
import { Component, Input, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { format } from 'date-fns';
import { Exam } from 'src/app/exam/exam.model';
import { AttachmentService } from '../../shared/attachment/attachment.service';
import { FileService } from '../../shared/file/file.service';
import type { ReviewedExam, Scores } from '../enrolment.model';
import { ExamAnswersDialogComponent } from './exam-answers-dialog.component';
import { DateTime } from 'luxon';

@Component({
selector: 'xm-exam-feedback',
Expand Down Expand Up @@ -76,6 +76,11 @@ export class ExamFeedbackComponent implements OnInit {

downloadScoreReport = () => {
const url = `/app/feedback/exams/${this.assessment.id}/report`;
this.Files.download(url, `${this.assessment.name}_${format(new Date(), 'dd-MM-yyyy')}.xlsx`, undefined, false);
this.Files.download(
url,
`${this.assessment.name}_${DateTime.now().toFormat('dd-LL-yyyy')}.xlsx`,
undefined,
false,
);
};
}
7 changes: 3 additions & 4 deletions ui/src/app/enrolment/wrong-location/wrong-location.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
*/
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { format, parseISO } from 'date-fns';
import { DateTime } from 'luxon';
import { ToastrService } from 'ngx-toastr';

Expand Down Expand Up @@ -50,10 +49,10 @@ export class WrongLocationService {
};

displayWrongUserAgent = (startsAtTxt: string) => {
const startsAt = parseISO(startsAtTxt); // TODO: what about timezone here?
if (startsAt > new Date()) {
const startsAt = DateTime.fromISO(startsAtTxt); // TODO: what about timezone here?
if (startsAt.toJSDate() > new Date()) {
this.toast.warning(
`${this.translate.instant('i18n_seb_exam_about_to_begin')} ${format(startsAt, 'HH:mm')}`,
`${this.translate.instant('i18n_seb_exam_about_to_begin')} ${startsAt.toFormat('HH:mm')}`,
'', // TODO: should we have some title for this (needs translation)
{ timeOut: 10000 },
);
Expand Down
43 changes: 11 additions & 32 deletions ui/src/app/facility/schedule/exception-dialog.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,17 @@ <h4><i class="fa fa-exclamation"></i>&nbsp;&nbsp;{{ 'i18n_exception_time' | tran
</div>
</div>
<div class="sitnet-info-text">{{ 'i18n_repeating_info' | translate }}</div>
<div>
<span class="dropdown" ngbDropdown>
<button
ngbDropdownToggle
class="btn btn-outline-secondary"
type="button"
id="dropDownMenu1"
aria-expanded="true"
>
{{ 'i18n_' + repeats.toLowerCase() | translate }}&nbsp;<span class="caret"></span>
</button>
<div
ngbDropdownMenu
style="padding-left: 0; min-width: 17em"
role="menu"
aria-labelledby="dropDownMenu1"
>
@for (ro of repeatOptions; track ro) {
<button
ngbDropdownItem
role="presentation"
class="pointer"
(click)="updateRepeatOption(ro)"
(keydown.enter)="updateRepeatOption(ro)"
>
<a role="menuitem" title="{{ 'i18n_' + ro.toLowerCase() | translate }}">{{
'i18n_' + ro.toLowerCase() | translate
}}</a>
</button>
}
</div>
</span>
<div ngbDropdown>
<button ngbDropdownToggle class="btn btn-outline-secondary" type="button" id="dropDownMenu1">
{{ 'i18n_' + repeats.toLowerCase() | translate }}&nbsp;<span class="caret"></span>
</button>
<div ngbDropdownMenu aria-labelledby="dropDownMenu1">
@for (ro of repeatOptions; track ro) {
<button ngbDropdownItem (click)="updateRepeatOption(ro)" (keydown.enter)="updateRepeatOption(ro)">
{{ 'i18n_' + ro.toLowerCase() | translate }}"
</button>
}
</div>
</div>
<div class="row align-items-center">
@if (repeats.toString() === 'ONCE') {
Expand Down
22 changes: 15 additions & 7 deletions ui/src/app/facility/schedule/exception-dialog.component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { formatDate, NgClass } from '@angular/common';
import { Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgbActiveModal, NgbTimepickerModule } from '@ng-bootstrap/ng-bootstrap';
import { NgbActiveModal, NgbDropdownModule, NgbTimepickerModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { areIntervalsOverlapping, eachDayOfInterval } from 'date-fns';
import { ToastrService } from 'ngx-toastr';
import { range } from 'ramda';
import { DatePickerComponent } from 'src/app/shared/date/date-picker.component';
import { DateTimePickerComponent } from 'src/app/shared/date/date-time-picker.component';
import { ExceptionWorkingHours } from '../../reservation/reservation.model';
import { DateTimeService, REPEAT_OPTIONS } from '../../shared/date/date.service';
import { DateTimeService, REPEAT_OPTION } from '../../shared/date/date.service';
import { ConfirmationDialogService } from '../../shared/dialogs/confirmation-dialog.service';

enum ORDINAL {
Expand All @@ -22,7 +22,15 @@ enum ORDINAL {

@Component({
standalone: true,
imports: [FormsModule, TranslateModule, NgClass, DateTimePickerComponent, DatePickerComponent, NgbTimepickerModule],
imports: [
FormsModule,
TranslateModule,
NgClass,
DateTimePickerComponent,
DatePickerComponent,
NgbTimepickerModule,
NgbDropdownModule,
],
templateUrl: './exception-dialog.component.html',
})
export class ExceptionDialogComponent {
Expand All @@ -43,8 +51,8 @@ export class ExceptionDialogComponent {
dayOfMonth = 1;
selectableWeekDays: { selected: boolean; day: string; number: number }[];
selectableMonths: { selected: boolean; month: string; number: number }[];
repeatOptions: REPEAT_OPTIONS[] = Object.values(REPEAT_OPTIONS);
repeats: REPEAT_OPTIONS = REPEAT_OPTIONS.once;
repeatOptions: REPEAT_OPTION[] = Object.values(REPEAT_OPTION);
repeats: REPEAT_OPTION = REPEAT_OPTION.once;
isNumericNotWeekday = true;
weeks = [range(1, 8), range(8, 15), range(15, 22), range(22, 29)];
ordinals: { ordinal: string; number: number }[] = Object.values(ORDINAL).map((o, i) => ({ ordinal: o, number: i }));
Expand Down Expand Up @@ -82,7 +90,7 @@ export class ExceptionDialogComponent {
this.endTime.minute = 59;
}
if (
this.repeats === REPEAT_OPTIONS.once
this.repeats === REPEAT_OPTION.once
? this.startDate >= this.endDate
: this.startTime.hour * 100 + this.startTime.minute >= this.endTime.hour * 100 + this.endTime.minute ||
new Date(this.startDate.getFullYear(), this.startDate.getMonth() + 1, this.startDate.getDate()) >
Expand Down Expand Up @@ -335,7 +343,7 @@ export class ExceptionDialogComponent {
}
}

updateRepeatOption(select: REPEAT_OPTIONS) {
updateRepeatOption(select: REPEAT_OPTION) {
this.repeats = select;
}

Expand Down
10 changes: 5 additions & 5 deletions ui/src/app/shared/date/apply-dst.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@
*/
import type { PipeTransform } from '@angular/core';
import { Pipe } from '@angular/core';
import { addHours, formatISO, parseISO } from 'date-fns';
import { DateTimeService } from './date.service';
import { DateTime } from 'luxon';

@Pipe({
name: 'applyDst',
standalone: true,
})
export class ApplyDstPipe implements PipeTransform {
constructor(private DateTime: DateTimeService) {}
constructor(private DateTimeService: DateTimeService) {}
transform = (input?: string): string => {
if (!input) return '';
const date = parseISO(input);
if (this.DateTime.isDST(date)) {
return formatISO(addHours(date, -1));
const date = DateTime.fromISO(input);
if (this.DateTimeService.isDST(date.toJSDate())) {
return date.minus({ hours: 1 }).toISO() as string;
}
return input;
};
Expand Down
Loading

0 comments on commit 79eee96

Please sign in to comment.