import { Timeslot, TimeslotJSON, TimeslotInterface, TimeslotFirestoreInterface, TimeslotSearchHitInterface, } from './timeslot'; /** * One's schedule contains all your booked timeslots (the inverse of one's * availability). * @deprecated We have no use of this for now (though we might in the future * when we implement a dashboard view). */ export type ScheduleAlias = TimeslotInterface[]; /** * One's availability contains all your open timeslots (the inverse of one's * schedule). */ export type AvailabilityAlias = TimeslotInterface[]; export type AvailabilityJSON = TimeslotJSON[]; export type AvailabilityFirestoreAlias = TimeslotFirestoreInterface[]; export type AvailabilitySearchHitAlias = TimeslotSearchHitInterface[]; /** * Class that contains a bunch of time slots or openings that represents a * user's availability (inverse of their schedule, which contains a bunch of * booked time slots or appointments). This provides some useful methods for * finding time slots and a better `toString` representation than * `[Object object]`. */ export class Availability extends Array implements AvailabilityAlias { /** * Note that this method (`Availability.prototype.contains`) is **very** * different from the `Availability.prototype.hasTimeslot` method; this method * checks to see if any `Timeslot` contains the given `Timeslot` whereas the * `hasTimeslot` methods checks to see if this availability contains the exact * given `Timeslot`. */ public contains(other: Timeslot): boolean { const contains = (a: boolean, t: Timeslot) => a || t.contains(other); return this.reduce(contains, false); } /** * Helper function to remove a given `Timeslot` from this `Availability`. Note * that this **does not** just remove that exact `Timeslot` but rather ensures * that there are no `Timeslot`s remaining that overlap with the given * `Timeslot` by (where A is the given `Timeslot` and B is a `Timeslot` in * `this`): * 1. If (they overlap; B's close time is contained w/in A): * - B's open time is before A's open time AND; * - B's close time is before A's close time AND; * - B's close time is after A's open time. * Then we'll adjust B such that it's close time is equal to A's open time. * 2. If (B's open time is contained w/in A; opposite of scenario #1): * - B's close time is after A's close time AND; * - B's open time is before A's close time AND; * - B's open time is after A's open time. * Then we'll adjust B's open time to be equal to A's close time. * 3. If (A contains B): * - B's open time is after A's open time AND; * - B's close time is before A's close time. * Then we'll remove B altogether. * 4. If (B contains A; opposite of scenario #2): * - B's open time is before A's open time AND; * - B's close time is after A's close time. * Then we'll split B into two timeslots (i.e. essentically cutting out A): * - One timeslot will be `{ from: B.from, to: A.from }` * - The other timeslot will be `{ from: A.to, to: B.to }` * 5. If B and A are equal, we just remove B altogether. * 6. Otherwise, we keep B and continue to the next check. */ public remove(a: Timeslot): void { const temp: Availability = new Availability(); const aFrom = a.from.valueOf(); const aTo = a.to.valueOf(); this.forEach((b: Timeslot) => { /* eslint-disable no-param-reassign */ const bFrom = b.from.valueOf(); const bTo = b.to.valueOf(); if (bFrom < aFrom && bTo < aTo && bTo > aFrom) { // Adjust `b` such that it's close time is equal to `a`'s open time. b.to = new Date(aFrom); temp.push(b); } else if (bTo > aTo && bFrom < aTo && bFrom > aFrom) { // Adjust `b` such that it's open time is equal to `a`'s close time. b.from = new Date(aTo); temp.push(b); } else if (a.contains(b)) { // Remove `b` altogether (i.e. don't add to `temp`). } else if (b.contains(a)) { // Split `b` into two timeslots (i.e. essentially cutting out `a`). temp.push(new Timeslot(new Date(bFrom), new Date(aFrom))); temp.push(new Timeslot(new Date(aTo), new Date(bTo))); } else if (a.equalTo(b)) { // Remove `b` altogether (i.e. don't add to `temp`). } else { temp.push(b); } /* eslint-enable no-param-reassign */ }); this.length = 0; temp.forEach((timeslot: Timeslot) => this.push(timeslot)); } /** * Converts this `Availability` into a comma-separated string of all of it's * constituent timeslots. * @deprecated We're going to put these into strings within the React tree so * that we can use `react-intl` for better i18n support (e.g. we'll set the * localization in the `pages/_app.tsx` top-level component and all children * components will render their `Date`s properly for that locale). */ public toString(showDay = true): string { return this.length > 0 ? this.map((timeslot: Timeslot) => timeslot.toString(showDay)).join(', ') : ''; } public hasTimeslot(timeslot: TimeslotInterface): boolean { return !!this.filter((t) => t.equalTo(timeslot)).length; } public toFirestore(): AvailabilityFirestoreAlias { return Array.from(this.map((timeslot: Timeslot) => timeslot.toFirestore())); } /** * Takes in an array of `Timeslot` objects (but w/ Firestore `Timestamp` * objects in the `from` and `to` fields instead of `Date` objects) and * returns an `Availability` object. */ public static fromFirestore(data: AvailabilityFirestoreAlias): Availability { const availability: Availability = new Availability(); data.forEach((t) => availability.push(Timeslot.fromFirestore(t))); return availability; } /** * Returns a basic `Array` object containing `TimeslotJSON`s. Note * that we **must** wrap the `this.map` statement with an `Array.from` call * because otherwise, we'd just return an invalid `Availability` object (which * would cause subsequent `toJSON` calls to fail because the new array * wouldn't contain valid `Timeslot` objects). */ public toJSON(): AvailabilityJSON { return Array.from(this.map((timeslot: Timeslot) => timeslot.toJSON())); } public static fromJSON(json: AvailabilityJSON): Availability { const availability: Availability = new Availability(); json.forEach((t) => availability.push(Timeslot.fromJSON(t))); return availability; } public static fromSearchHit(hit: AvailabilitySearchHitAlias): Availability { const availability: Availability = new Availability(); hit.forEach((t) => availability.push(Timeslot.fromSearchHit(t))); return availability; } public toURLParam(): string { return encodeURIComponent(JSON.stringify(this)); } public static fromURLParam(param: string): Availability { const availability: Availability = new Availability(); const params: string[] = JSON.parse(decodeURIComponent(param)) as string[]; params.forEach((timeslotParam: string) => { availability.push(Timeslot.fromURLParam(timeslotParam)); }); return availability; } /** * Checks if two availabilities contain all the same timeslots by ensuring * that: * 1. This availability contains all the timeslots of the other availability. * 2. The other availability contains all the timeslots of this availability. */ public equalTo(other: Availability): boolean { if (!other.every((t: Timeslot) => this.hasTimeslot(t))) return false; if (!this.every((t: Timeslot) => other.hasTimeslot(t))) return false; return true; } }