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 9, 2024
1 parent 250f7ca commit 498e5d7
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 67 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
49 changes: 32 additions & 17 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,29 +163,42 @@ 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)
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));

const openings = room.defaultWorkingHours.map((dwh) =>
this.normalize(DateTime.fromISO(dwh.startTime, { zone: tz })),
);
return DateTime.min(...openings.concat(extraOpenings))
.set({ minute: 0 })
.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 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));

const closings = room.defaultWorkingHours.map((dwh) =>
this.normalize(DateTime.fromISO(dwh.endTime, { zone: tz })),
);
return DateTime.max(...closings.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));
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

const openedDays = extraDays.concat(room.defaultWorkingHours.map((d) => weekdays.indexOf(d.weekday)));
return [0, 1, 2, 3, 4, 5, 6].filter((x) => openedDays.indexOf(x) === -1);
}

Expand All @@ -211,6 +224,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
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
2 changes: 1 addition & 1 deletion ui/src/app/shared/date/date.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { format, roundToNearestMinutes } from 'date-fns';
import { DateTime, WeekdayNumbers } from 'luxon';
import { range } from 'ramda';

export enum REPEAT_OPTIONS {
export enum REPEAT_OPTION {
once = 'ONCE',
daily_weekly = 'DAILY_WEEKLY',
monthly = 'MONTHLY',
Expand Down

0 comments on commit 498e5d7

Please sign in to comment.