Compare busyTimes in UTC, re-implement hasErrors

This commit is contained in:
Alex van Andel 2021-06-27 22:30:11 +00:00
parent 698c64e657
commit 1eba242820
4 changed files with 88 additions and 57 deletions

View file

@ -1,17 +1,18 @@
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Slots from "./Slots"; import Slots from "./Slots";
import {ExclamationIcon} from "@heroicons/react/solid";
const AvailableTimes = ({ date, eventLength, eventTypeId, workingHours, timeFormat }) => { const AvailableTimes = ({ date, eventLength, eventTypeId, workingHours, timeFormat, user }) => {
const router = useRouter(); const router = useRouter();
const { user, rescheduleUid } = router.query; const { rescheduleUid } = router.query;
const { slots, isFullyBooked } = Slots({ date, eventLength, workingHours }); const { slots, isFullyBooked, hasErrors } = Slots({ date, eventLength, workingHours });
return ( return (
<div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:max-h-97 overflow-y-auto"> <div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:max-h-97 overflow-y-auto">
<div className="text-gray-600 font-light text-xl mb-4 text-left"> <div className="text-gray-600 font-light text-xl mb-4 text-left">
<span className="w-1/2">{date.format("dddd DD MMMM YYYY")}</span> <span className="w-1/2">{date.format("dddd DD MMMM YYYY")}</span>
</div> </div>
{slots.length > 0 ? ( {slots.length > 0 && (
slots.map((slot) => ( slots.map((slot) => (
<div key={slot.format()}> <div key={slot.format()}>
<Link <Link
@ -25,12 +26,32 @@ const AvailableTimes = ({ date, eventLength, eventTypeId, workingHours, timeForm
</Link> </Link>
</div> </div>
)) ))
) : isFullyBooked ? )}
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4"> {isFullyBooked && <div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
<h1 className="text-xl font">{user} is all booked today.</h1> <h1 className="text-xl font">{user.name} is all booked today.</h1>
</div>}
{!isFullyBooked && slots.length === 0 && !hasErrors && <div className="loader" />}
{hasErrors && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">
Could not load the available time slots.{" "}
<a
href={"mailto:" + user.email}
className="font-medium underline text-yellow-700 hover:text-yellow-600">
Contact {user.name} via e-mail
</a>
</p>
</div>
</div> </div>
: <div className="loader" /> </div>
} )}
</div> </div>
); );
}; };

View file

@ -3,7 +3,9 @@ import { useRouter } from "next/router";
import getSlots from "../../lib/slots"; import getSlots from "../../lib/slots";
import dayjs, {Dayjs} from "dayjs"; import dayjs, {Dayjs} from "dayjs";
import isBetween from "dayjs/plugin/isBetween"; import isBetween from "dayjs/plugin/isBetween";
import utc from "dayjs/plugin/utc";
dayjs.extend(isBetween); dayjs.extend(isBetween);
dayjs.extend(utc);
type Props = { type Props = {
eventLength: number; eventLength: number;
@ -19,18 +21,25 @@ const Slots = ({ eventLength, minimumBookingNotice, date, workingHours }: Props)
const { user } = router.query; const { user } = router.query;
const [slots, setSlots] = useState([]); const [slots, setSlots] = useState([]);
const [isFullyBooked, setIsFullyBooked ] = useState(false); const [isFullyBooked, setIsFullyBooked ] = useState(false);
const [hasErrors, setHasErrors ] = useState(false);
useEffect(() => { useEffect(() => {
setSlots([]); setSlots([]);
setIsFullyBooked(false); setIsFullyBooked(false);
setHasErrors(false);
fetch( fetch(
`/api/availability/${user}?dateFrom=${date.startOf("day").utc().format()}&dateTo=${date `/api/availability/${user}?dateFrom=${date.startOf("day").utc().startOf('day').format()}&dateTo=${date
.endOf("day") .endOf("day")
.utc() .utc()
.endOf('day')
.format()}` .format()}`
) )
.then((res) => res.json()) .then((res) => res.json())
.then(handleAvailableSlots); .then(handleAvailableSlots)
.catch( e => {
console.error(e);
setHasErrors(true);
})
}, [date]); }, [date]);
const handleAvailableSlots = (busyTimes: []) => { const handleAvailableSlots = (busyTimes: []) => {
@ -47,26 +56,24 @@ const Slots = ({ eventLength, minimumBookingNotice, date, workingHours }: Props)
// Check for conflicts // Check for conflicts
for (let i = times.length - 1; i >= 0; i -= 1) { for (let i = times.length - 1; i >= 0; i -= 1) {
busyTimes.forEach((busyTime) => { busyTimes.forEach((busyTime) => {
const startTime = dayjs(busyTime.start);
const endTime = dayjs(busyTime.end); const startTime = dayjs(busyTime.start).utc();
const endTime = dayjs(busyTime.end).utc();
// Check if start times are the same // Check if start times are the same
if (dayjs(times[i]).format("HH:mm") == startTime.format("HH:mm")) { if (times[i].utc().format("HH:mm") == startTime.format("HH:mm")) {
times.splice(i, 1); times.splice(i, 1);
} }
// Check if time is between start and end times // Check if time is between start and end times
if (dayjs(times[i]).isBetween(startTime, endTime)) { else if (times[i].utc().isBetween(startTime, endTime)) {
times.splice(i, 1); times.splice(i, 1);
} }
// Check if slot end time is between start and end time // Check if slot end time is between start and end time
if (dayjs(times[i]).add(eventLength, "minutes").isBetween(startTime, endTime)) { else if (times[i].utc().add(eventLength, "minutes").isBetween(startTime, endTime)) {
times.splice(i, 1); times.splice(i, 1);
} }
// Check if startTime is between slot // Check if startTime is between slot
if (startTime.isBetween(dayjs(times[i]), dayjs(times[i]).add(eventLength, "minutes"))) { else if (startTime.isBetween(times[i].utc(), times[i].utc().add(eventLength, "minutes"))) {
times.splice(i, 1); times.splice(i, 1);
} }
}); });
@ -82,6 +89,7 @@ const Slots = ({ eventLength, minimumBookingNotice, date, workingHours }: Props)
return { return {
slots, slots,
isFullyBooked, isFullyBooked,
hasErrors,
}; };
}; };

View file

@ -1,11 +1,11 @@
import dayjs, { Dayjs } from "dayjs"; import dayjs, {Dayjs} from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
dayjs.extend(utc); dayjs.extend(utc);
interface GetSlotsType { interface GetSlotsType {
inviteeDate: Dayjs; inviteeDate: Dayjs;
frequency: number; frequency: number;
workingHours: { [WeekDay]: Boundary[] }; workingHours: [];
minimumBookingNotice?: number; minimumBookingNotice?: number;
} }
@ -17,34 +17,30 @@ interface Boundary {
const freqApply: number = (cb, value: number, frequency: number): number => cb(value / frequency) * frequency; const freqApply: number = (cb, value: number, frequency: number): number => cb(value / frequency) * frequency;
const intersectBoundary = (a: Boundary, b: Boundary) => { const intersectBoundary = (a: Boundary, b: Boundary) => {
if (a.upperBound < b.lowerBound || a.lowerBound > b.upperBound) { if (
a.upperBound < b.lowerBound || a.lowerBound > b.upperBound
) {
return; return;
} }
return { return {
lowerBound: Math.max(b.lowerBound, a.lowerBound), lowerBound: Math.max(b.lowerBound, a.lowerBound),
upperBound: Math.min(b.upperBound, a.upperBound), upperBound: Math.min(b.upperBound, a.upperBound)
}; };
}; }
// say invitee is -60,1380, and boundary is -120,240 - the overlap is -60,240 // say invitee is -60,1380, and boundary is -120,240 - the overlap is -60,240
const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) => const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) => boundaries
boundaries.map((boundary) => intersectBoundary(inviteeBoundary, boundary)).filter(Boolean); .map(
(boundary) => intersectBoundary(inviteeBoundary, boundary)
).filter(Boolean);
const organizerBoundaries = (workingHours: [], inviteeDate: Dayjs, inviteeBounds: Boundary): Boundary[] => { const organizerBoundaries = (workingHours: [], inviteeDate: Dayjs, inviteeBounds: Boundary): Boundary[] => {
const boundaries: Boundary[] = []; const boundaries: Boundary[] = [];
const startDay: number = +inviteeDate const startDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.lowerBound, 'minutes').format('d');
.utc() const endDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.upperBound, 'minutes').format('d');
.startOf("day")
.add(inviteeBounds.lowerBound, "minutes")
.format("d");
const endDay: number = +inviteeDate
.utc()
.startOf("day")
.add(inviteeBounds.upperBound, "minutes")
.format("d");
workingHours.forEach((item) => { workingHours.forEach( (item) => {
const lowerBound: number = item.startTime; const lowerBound: number = item.startTime;
const upperBound: number = lowerBound + item.length; const upperBound: number = lowerBound + item.length;
if (startDay !== endDay) { if (startDay !== endDay) {
@ -66,7 +62,7 @@ const organizerBoundaries = (workingHours: [], inviteeDate: Dayjs, inviteeBounds
} }
} }
} else { } else {
boundaries.push({ lowerBound, upperBound }); boundaries.push({lowerBound, upperBound});
} }
}); });
return boundaries; return boundaries;
@ -76,33 +72,38 @@ const inviteeBoundary = (startTime: number, utcOffset: number, frequency: number
const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency); const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency);
const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency); const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency);
return { return {
lowerBound, lowerBound, upperBound,
upperBound,
}; };
}; };
const getSlotsBetweenBoundary = (frequency: number, { lowerBound, upperBound }: Boundary) => { const getSlotsBetweenBoundary = (frequency: number, {lowerBound,upperBound}: Boundary) => {
const slots: Dayjs[] = []; const slots: Dayjs[] = [];
for (let minutes = 0; lowerBound + minutes <= upperBound - frequency; minutes += frequency) { for (
slots.push( let minutes = 0;
<Dayjs>dayjs lowerBound + minutes <= upperBound - frequency;
.utc() minutes += frequency
.startOf("day") ) {
.add(lowerBound + minutes, "minutes") slots.push(<Dayjs>dayjs.utc().startOf('day').add(lowerBound + minutes, 'minutes'));
);
} }
return slots; return slots;
}; };
const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours }: GetSlotsType): Dayjs[] => { const getSlots = (
const startTime = dayjs.utc().isSame(dayjs(inviteeDate), "day") { inviteeDate, frequency, minimumBookingNotice, workingHours, }: GetSlotsType
? inviteeDate.hour() * 60 + inviteeDate.minute() + minimumBookingNotice ): Dayjs[] => {
: 0;
const startTime = (
dayjs.utc().isSame(dayjs(inviteeDate), 'day') ? inviteeDate.hour() * 60 + inviteeDate.minute() + minimumBookingNotice : 0
);
const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency); const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency);
return getOverlaps(inviteeBounds, organizerBoundaries(workingHours, inviteeDate, inviteeBounds)) return getOverlaps(
.reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], []) inviteeBounds, organizerBoundaries(workingHours, inviteeDate, inviteeBounds)
.map((slot) => slot.utcOffset(dayjs(inviteeDate).utcOffset())); ).reduce(
}; (slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary) ], []
).map(
(slot) => slot.utcOffset(dayjs(inviteeDate).utcOffset())
)
}
export default getSlots; export default getSlots;

View file

@ -122,6 +122,7 @@ export default function Type(props): Type {
eventLength={props.eventType.length} eventLength={props.eventType.length}
eventTypeId={props.eventType.id} eventTypeId={props.eventType.id}
date={selectedDate} date={selectedDate}
user={props.user}
/> />
)} )}
</div> </div>