Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions packages/frontend/app/components/learner-group/calendar.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import t from 'ember-intl/helpers/t';
import FaIcon from 'ilios-common/components/fa-icon';
import IliosCalendarWeek from 'ilios-common/components/ilios-calendar-week';
import LoadingSpinner from 'ilios-common/components/loading-spinner';
import Event from 'ilios-common/classes/event';

export default class LearnerGroupCalendarComponent extends Component {
@service localeDays;
@service schoolEvents;
@tracked selectedDate = DateTime.now();
@tracked showSubgroupEvents = false;

Expand Down Expand Up @@ -63,12 +63,10 @@ export default class LearnerGroupCalendarComponent extends Component {
const session = await offering.session;
const course = await session.course;
const school = await course.school;
return this.schoolEvents.createEventFromData(
return new Event(
{
startDate: offering.startDate.toISOString(),
endDate: offering.endDate.toISOString(),
calendarStartDate: offering.startDate.toISOString(),
calendarEndDate: offering.endDate.toISOString(),
courseTitle: course.title,
name: session.title,
offering: offering.id,
Expand All @@ -79,7 +77,6 @@ export default class LearnerGroupCalendarComponent extends Component {
postrequisites: [],
isScheduled: session.isScheduled || course.isScheduled,
isPublished: session.isPublished && course.isPublished,
isBlanked: false,
},
false,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import t from 'ember-intl/helpers/t';
import { on } from '@ember/modifier';
import FaIcon from 'ilios-common/components/fa-icon';
import IliosCalendarWeek from 'ilios-common/components/ilios-calendar-week';
import Event from 'ilios-common/classes/event';

export default class UserProfileCalendar extends Component {
@service fetch;
Expand All @@ -28,7 +29,7 @@ export default class UserProfileCalendar extends Component {
get calendarEvents() {
if (this.eventsData.isResolved && this.eventsData.value) {
return sortBy(
this.eventsData.value.map((obj) => this.userEvents.createEventFromData(obj, true)),
this.eventsData.value.map((obj) => new Event(obj, true)),
['startDate', 'name'],
);
} else {
Expand Down
143 changes: 143 additions & 0 deletions packages/ilios-common/addon/classes/event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { cached } from '@glimmer/tracking';
import { DateTime } from 'luxon';
import { sortBy } from 'ilios-common/utils/array-helpers';

/**
* This is an object representation of an event, to be used in the
* various calendars and week-at-a-glance.
*/
export default class Event {
/** @var { Boolean } isUserEvent indicates whether this is a user event or a school event */
isUserEvent;

/**
* @param { Object } data A plain-old JS object, containing all the event's data.
* @param { Boolean } isUserEvent TRUE if the given object represents a user event, FALSE if it represents a school event.
*/
constructor(data, isUserEvent) {
this.isUserEvent = isUserEvent;
// copies all attributes of the given data input to this Event object.
Object.assign(this, data);
// converts pre- and post-requisites into Events as well.
this.prerequisites = sortBy(
this.prerequisites.map((prereq) => {
return new Event(
{
...prereq,
...{
startDate: this.startDate,
postrequisiteName: this.name,
postrequisiteSlug: this.slug,
},
},
this.isUserEvent,
);
}),
['startDate', 'name'],
);
this.postrequisites = sortBy(
this.postrequisites.map((postreq) => {
return new Event(postreq, this.isUserEvent);
}),
['startDate', 'name'],
);
}

/**
* The start-date/time of this event, for display purposes in a calendar.
* This may differ from its actual start-date/time.
* @return { String } The event's calendar start-date/time as ISO-formatted text.
*/
@cached
get calendarStartDate() {
// ILMs don't really have a duration, they have due-date.
// But in order to display them in a calendar, we have to make up a duration for them, otherwise they won't show up.
// So we're filling in a start-date that's the equivalent of the actual due-date,
// and give it an end-date that's fifteen minutes out.
// However, if the given start-date is 11:45p or later in the day, that event would be considered to continue
// into the next day, effectively making it a "multi-day" event.
// Multi-days are currently not shown in the calendar, instead they are displayed in a table below the calendar.
// To prevent this from happening, we pin the calendar display start-date to 11:45p and the end-date to 11:59p.
if (this.ilmSession) {
const startDate = DateTime.fromISO(this.startDate);
if (23 === startDate.hour && 45 <= startDate.minute) {
return startDate.set({ minute: 45 }).toUTC().toISO();
}
}
return this.startDate;
}

/**
* The end-date/time of this event, for display purposes in a calendar.
* This may differ from its actual end-date/time.
* @return { String } The event's calendar end-date/time as ISO-formatted text.
*/
@cached
get calendarEndDate() {
// See comment block inside `Event::calendarStartDate()` for details on this logic.
if (this.ilmSession) {
const startDate = DateTime.fromISO(this.startDate);
if (23 === startDate.hour && 45 <= startDate.minute) {
return startDate.set({ minute: 59 }).toUTC().toISO();
}
}
return this.endDate;
}

/**
* Whether this event is considered "blanked" or not.
* @return { Boolean }
*/
@cached
get isBlanked() {
return !this.offering && !this.ilmSession;
}

/**
* The event slug.
* @return { String }
*/
@cached
get slug() {
return this.isUserEvent
? this.#getSlugForUserEvent(this.startDate, this.offering, this.ilmSession)
: this.#getSlugForSchoolEvent(this.startDate, this.school, this.offering, this.ilmSession);
}
/**
* Generates a slug from given user event data.
* @return { String }
*/
#getSlugForUserEvent() {
let slug = 'U';
slug += DateTime.fromISO(this.startDate).toFormat('yyyyMMdd');
if (this.offering) {
slug += 'O' + this.offering;
}
if (this.ilmSession) {
slug += 'I' + this.ilmSession;
}
return slug;
}

/**
* Generates a slug for a given school event.
* @return { String }
*/
#getSlugForSchoolEvent() {
let slug = 'S';
let schoolId = parseInt(this.school, 10).toString();
//always use a two digit schoolId
if (schoolId.length === 1) {
schoolId = '0' + schoolId;
}
slug += schoolId;
slug += DateTime.fromISO(this.startDate).toFormat('yyyyMMdd');
if (this.offering) {
slug += 'O' + this.offering;
}
if (this.ilmSession) {
slug += 'I' + this.ilmSession;
}
return slug;
}
}
95 changes: 1 addition & 94 deletions packages/ilios-common/addon/classes/events-base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Service from '@ember/service';
import { DateTime } from 'luxon';
import { mapBy, sortBy, uniqueValues } from 'ilios-common/utils/array-helpers';
import { mapBy, uniqueValues } from 'ilios-common/utils/array-helpers';

export default class EventsBase extends Service {
/**
Expand Down Expand Up @@ -88,96 +87,4 @@ export default class EventsBase extends Service {
const cohorts = await course.get('cohorts');
return mapBy(cohorts, 'id');
}

/**
* Parses event and does some transformation
* @method createEventFromData
* @param {Object} obj
* @param {Boolean} isUserEvent TRUE if the given object represents a user event, FALSE if it represents a school event.
* @return {Object}
*/
createEventFromData(obj, isUserEvent) {
obj.isBlanked = !obj.offering && !obj.ilmSession;
obj.slug = isUserEvent ? this.getSlugForUserEvent(obj) : this.getSlugForSchoolEvent(obj);
obj.prerequisites = obj.prerequisites.map((prereq) => {
const rhett = this.createEventFromData(prereq, isUserEvent);
rhett.startDate = obj.startDate;
rhett.postrequisiteName = obj.name;
rhett.postrequisiteSlug = obj.slug;

return rhett;
});
obj.prerequisites = sortBy(obj.prerequisites, 'startDate');
obj.prerequisites = sortBy(obj.prerequisites, 'name');
obj.postrequisites = obj.postrequisites.map((postreq) =>
this.createEventFromData(postreq, isUserEvent),
);
obj.postrequisites = sortBy(obj.postrequisites, 'startDate');
obj.postrequisites = sortBy(obj.postrequisites, 'name');
obj.isUserEvent = isUserEvent;

// The start and end date of the event, for display purposes on the calendar. See comment block below.
obj.calendarStartDate = obj.startDate;
obj.calendarEndDate = obj.endDate;

// ACHTUNG!
// ILMs don't really have a duration, they have due-date.
// But in order to display them in a calendar, we have to make up a duration for them, otherwise they won't show up.
// So we're filling in a start-date that's the equivalent of the actual due-date,
// and give it an end-date that's fifteen minutes out.
// However, if the given start-date is 11:45p or later in the day, that event would be considered to continue
// into the next day, effectively making it a "multi-day" event.
// Multi-days are currently not shown in the calendar, instead they are displayed in a table below the calendar.
// To prevent this from happening, we pin the calendar display start-date to 11:45p and the end-date to 11:59p.
// [ST 2025/11/07]
const startDate = DateTime.fromISO(obj.startDate);
if (obj.ilmSession && 23 === startDate.hour && startDate.minute >= 45) {
obj.calendarStartDate = startDate.set({ minute: 45 }).toUTC().toISO();
obj.calendarEndDate = startDate.set({ minute: 59 }).toUTC().toISO();
}

return obj;
}

/**
* Generates a slug for a given user event.
* @method getSlugForUserEvent
* @param {Object} event
* @return {String}
*/
getSlugForUserEvent(event) {
let slug = 'U';
slug += DateTime.fromISO(event.startDate).toFormat('yyyyMMdd');
if (event.offering) {
slug += 'O' + event.offering;
}
if (event.ilmSession) {
slug += 'I' + event.ilmSession;
}
return slug;
}

/**
* Generates a slug for a given school event.
* @method getSlugForSchoolEvent
* @param {Object} event
* @return {String}
*/
getSlugForSchoolEvent(event) {
let slug = 'S';
let schoolId = parseInt(event.school, 10).toString();
//always use a two digit schoolId
if (schoolId.length === 1) {
schoolId = '0' + schoolId;
}
slug += schoolId;
slug += DateTime.fromISO(event.startDate).toFormat('yyyyMMdd');
if (event.offering) {
slug += 'O' + event.offering;
}
if (event.ilmSession) {
slug += 'I' + event.ilmSession;
}
return slug;
}
}
15 changes: 4 additions & 11 deletions packages/ilios-common/addon/components/offering-calendar.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ import t from 'ember-intl/helpers/t';
import ToggleYesno from 'ilios-common/components/toggle-yesno';
import { fn } from '@ember/helper';
import { on } from '@ember/modifier';
import { service } from '@ember/service';
import set from 'ember-set-helper/helpers/set';
import not from 'ember-truth-helpers/helpers/not';
import toggle from 'ilios-common/helpers/toggle';
import IliosCalendarWeek from 'ilios-common/components/ilios-calendar-week';
import LoadingSpinner from 'ilios-common/components/loading-spinner';
import Event from 'ilios-common/classes/event';

export default class OfferingCalendar extends Component {
@tracked showLearnerGroupEvents = true;
@tracked showSessionEvents = true;
@tracked learnerGroupEvents = [];
@tracked sessionEvents = [];
@tracked currentEvent = null;
@service schoolEvents;

@cached
get calendarEventsData() {
Expand All @@ -44,12 +43,10 @@ export default class OfferingCalendar extends Component {
const session = await offering.session;
const course = await session.course;
const school = await course.school;
return this.schoolEvents.createEventFromData(
return new Event(
{
startDate: DateTime.fromJSDate(offering.startDate).toISO(),
endDate: DateTime.fromJSDate(offering.endDate).toISO(),
calendarStartDate: DateTime.fromJSDate(offering.startDate).toISO(),
calendarEndDate: DateTime.fromJSDate(offering.endDate).toISO(),
courseTitle: course.title,
school: school.id,
name: session.title,
Expand Down Expand Up @@ -78,12 +75,10 @@ export default class OfferingCalendar extends Component {
const course = await session.course;
const school = await course.school;
this.sessionEvents = await map(offerings, async (offering) => {
return this.schoolEvents.createEventFromData(
return new Event(
{
startDate: DateTime.fromJSDate(offering.startDate).toISO(),
endDate: DateTime.fromJSDate(offering.endDate).toISO(),
calendarStartDate: DateTime.fromJSDate(offering.startDate).toISO(),
calendarEndDate: DateTime.fromJSDate(offering.endDate).toISO(),
courseTitle: course.title,
school: school.id,
name: session.title,
Expand All @@ -97,12 +92,10 @@ export default class OfferingCalendar extends Component {
);
});

this.currentEvent = this.schoolEvents.createEventFromData(
this.currentEvent = new Event(
{
startDate: DateTime.fromJSDate(startDate).toISO(),
endDate: DateTime.fromJSDate(endDate).toISO(),
calendarStartDate: DateTime.fromJSDate(startDate).toISO(),
calendarEndDate: DateTime.fromJSDate(endDate).toISO(),
courseTitle: course.title,
school: school.id,
name: session.title,
Expand Down
3 changes: 2 additions & 1 deletion packages/ilios-common/addon/services/school-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import EventsBase from 'ilios-common/classes/events-base';
import { service } from '@ember/service';
import { DateTime } from 'luxon';
import { sortBy } from 'ilios-common/utils/array-helpers';
import Event from 'ilios-common/classes/event';

export default class SchoolEvents extends EventsBase {
@service store;
Expand Down Expand Up @@ -29,7 +30,7 @@ export default class SchoolEvents extends EventsBase {
const data = await this.fetch.getJsonFromApiHost(url);

return sortBy(
data.events.map((obj) => this.createEventFromData(obj, false)),
data.events.map((obj) => new Event(obj, false)),
['startDate', 'name'],
);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/ilios-common/addon/services/user-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import EventsBase from 'ilios-common/classes/events-base';
import { service } from '@ember/service';
import { DateTime } from 'luxon';
import { sortBy } from 'ilios-common/utils/array-helpers';
import Event from 'ilios-common/classes/event';

export default class UserEvents extends EventsBase {
@service store;
Expand Down Expand Up @@ -33,7 +34,7 @@ export default class UserEvents extends EventsBase {
const data = await this.fetch.getJsonFromApiHost(url);

return sortBy(
data.userEvents.map((obj) => this.createEventFromData(obj, true)),
data.userEvents.map((obj) => new Event(obj, true)),
['startDate', 'name'],
);
}
Expand Down
Loading