Completely rebuilt logic when dealing with timezones. Now all available events should appear when selecting a date.

This commit is contained in:
Leonardo Stenico 2021-04-17 02:08:35 +02:00
parent 96e2b762c6
commit 2b0e8bef7a
3 changed files with 145 additions and 54 deletions

94
lib/slots.ts Normal file
View file

@ -0,0 +1,94 @@
const dayjs = require("dayjs");
const isToday = require("dayjs/plugin/isToday");
const utc = require("dayjs/plugin/utc");
const timezone = require("dayjs/plugin/timezone");
dayjs.extend(isToday);
dayjs.extend(utc);
dayjs.extend(timezone);
const getMinutesFromMidnight = (date) => {
return date.hour() * 60 + date.minute();
};
const getSlots = ({
calendarTimeZone,
eventLength,
selectedTimeZone,
selectedDate,
dayStartTime,
dayEndTime
}) => {
if(!selectedDate) return []
const lowerBound = selectedDate.startOf("day");
// Simple case, same timezone
if (calendarTimeZone === selectedTimeZone) {
const slots = [];
const now = dayjs();
for (
let minutes = dayStartTime;
minutes <= dayEndTime - eventLength;
minutes += parseInt(eventLength, 10)
) {
const slot = lowerBound.add(minutes, "minutes");
if (slot > now) {
slots.push(slot);
}
}
return slots;
}
const upperBound = selectedDate.endOf("day");
// We need to start generating slots from the start of the calendarTimeZone day
const startDateTime = lowerBound
.tz(calendarTimeZone)
.startOf("day")
.add(dayStartTime, "minutes");
let phase = 0;
if (startDateTime < lowerBound) {
// Getting minutes of the first event in the day of the chooser
const diff = lowerBound.diff(startDateTime, "minutes");
// finding first event
phase = diff + eventLength - (diff % eventLength);
}
// We can stop as soon as the selectedTimeZone day ends
const endDateTime = upperBound
.tz(calendarTimeZone)
.subtract(eventLength, "minutes");
const maxMinutes = endDateTime.diff(startDateTime, "minutes");
const slots = [];
const now = dayjs();
for (
let minutes = phase;
minutes <= maxMinutes;
minutes += parseInt(eventLength, 10)
) {
const slot = startDateTime.add(minutes, "minutes");
const minutesFromMidnight = getMinutesFromMidnight(slot);
if (
minutesFromMidnight < dayStartTime ||
minutesFromMidnight > dayEndTime - eventLength ||
slot < now
) {
continue;
}
slots.push(slot.tz(selectedTimeZone));
}
return slots;
};
export default getSlots

View file

@ -1,4 +1,4 @@
import {useEffect, useState} from 'react'; import {useEffect, useState, useMemo} from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import prisma from '../../lib/prisma'; import prisma from '../../lib/prisma';
@ -13,6 +13,8 @@ dayjs.extend(isBetween);
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
import getSlots from '../../lib/slots'
export default function Type(props) { export default function Type(props) {
// Initialise state // Initialise state
const [selectedDate, setSelectedDate] = useState(''); const [selectedDate, setSelectedDate] = useState('');
@ -33,6 +35,21 @@ export default function Type(props) {
setSelectedMonth(selectedMonth - 1); setSelectedMonth(selectedMonth - 1);
} }
// Need to define the bounds of the 24-hour window
const lowerBound = useMemo(() => {
if(!selectedDate) {
return
}
return selectedDate.startOf('day')
}, [selectedDate])
const upperBound = useMemo(() => {
if(!selectedDate) return
return selectedDate.endOf('day')
}, [selectedDate])
// Set up calendar // Set up calendar
var daysInMonth = dayjs().month(selectedMonth).daysInMonth(); var daysInMonth = dayjs().month(selectedMonth).daysInMonth();
var days = []; var days = [];
@ -41,76 +58,57 @@ export default function Type(props) {
} }
const calendar = days.map((day) => const calendar = days.map((day) =>
<button key={day} onClick={(e) => setSelectedDate(dayjs().month(selectedMonth).date(day).format("YYYY-MM-DD"))} disabled={selectedMonth < dayjs().format('MM') && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}> <button key={day} onClick={(e) => setSelectedDate(dayjs().tz(dayjs.tz.guess()).month(selectedMonth).date(day))} disabled={selectedMonth < dayjs().format('MM') && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}>
{day} {day}
</button> </button>
); );
// Handle date change // Handle date change
useEffect(async () => { useEffect(async () => {
if(!selectedDate) {
return
}
setLoading(true); setLoading(true);
const res = await fetch('/api/availability/' + user + '?date=' + dayjs(selectedDate).format("YYYY-MM-DD")); const res = await fetch(`/api/availability/${user}?dateFrom=${lowerBound.utc().format()}&dateTo=${upperBound.utc().format()}`);
const data = await res.json(); const data = await res.json();
setBusy(data.primary.busy); setBusy(data.primary.busy);
setLoading(false); setLoading(false);
}, [selectedDate]); }, [selectedDate]);
// Set up timeslots
let times = [];
// If we're looking at availability throughout the current date, work out the current number of minutes elapsed throughout the day const times = getSlots({
if (selectedDate == dayjs().format("YYYY-MM-DD")) { calendarTimeZone: props.user.timeZone,
var i = (parseInt(dayjs().startOf('hour').format('H') * 60) + parseInt(dayjs().startOf('hour').format('m'))); selectedTimeZone: dayjs.tz.guess(),
} else { eventLength: props.eventType.length,
var i = props.user.startTime; selectedDate: selectedDate,
} dayStartTime: props.user.startTime,
dayEndTime: props.user.endTime,
// Adding first availability time })
times.push(dayjs(selectedDate).tz(props.user.timeZone).hour(Math.floor(i / 60)).minute(i % 60).startOf(props.eventType.length, 'minute'));
// Until day end, push new times every x minutes
for (;i < props.user.endTime; i += parseInt(props.eventType.length)) {
times.push(dayjs(selectedDate).tz(props.user.timeZone).hour(Math.floor(i / 60)).minute(i % 60).startOf(props.eventType.length, 'minute').add(props.eventType.length, 'minute'));
}
// Check for conflicts // Check for conflicts
for(i = times.length - 1; i >= 0; i -= 1) { for(let i = times.length - 1; i >= 0; i -= 1) {
busy.forEach(busyTime => { busy.forEach(busyTime => {
let startTime = dayjs(busyTime.start); let startTime = dayjs(busyTime.start);
let endTime = dayjs(busyTime.end); let endTime = dayjs(busyTime.end);
// Check if time has passed // Check if start times are the same
if (dayjs(times[i]).isBefore(dayjs())) { if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) {
times.splice(i, 1); times.splice(i, 1);
} }
// Check if start times are the same // Check if time is between start and end times
if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) { if (dayjs(times[i]).isBetween(startTime, endTime)) {
times.splice(i, 1); times.splice(i, 1);
} }
});
// Check if time is between start and end times
if (dayjs(times[i]).isBetween(startTime, endTime)) {
times.splice(i, 1);
}
});
// If event ends after endTime, remove the slot
if(dayjs(times[i]).add(props.eventType.length, 'minute') > dayjs(times[i]).hour(0).minute(0).add(props.user.endTime, 'minute') ) {
times.splice(i, 1);
}
//If slot starts before startTime, remove it
if(dayjs(times[i]) < dayjs(times[i]).hour(0).minute(0).add(props.user.startTime, 'minute') ) {
times.splice(i, 1);
}
} }
// Display available times // Display available times
const availableTimes = times.map((time) => const availableTimes = times.map((time) =>
<div key={time.format("YYYY-MM-DDTHH:mm:ss")}> <div key={dayjs(time).utc().format()}>
<Link href={`/${props.user.username}/book?date=${time.format("YYYY-MM-DDTHH:mm:ss")}&type=${props.eventType.id}`}> <Link href={`/${props.user.username}/book?date=${dayjs(time).utc().format()}&type=${props.eventType.id}`}>
<a key={time.format("YYYY-MM-DDTHH:mm:ss")} className="block font-medium mb-4 text-blue-600 border border-blue-600 rounded hover:text-white hover:bg-blue-600 py-4">{dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}</a> <a key={dayjs(time).format("hh:mma")} className="block font-medium mb-4 text-blue-600 border border-blue-600 rounded hover:text-white hover:bg-blue-600 py-4">{dayjs(time).tz(dayjs.tz.guess()).format("hh:mma")}</a>
</Link> </Link>
</div> </div>
); );
@ -205,4 +203,4 @@ export async function getServerSideProps(context) {
eventType eventType
}, },
} }
} }

View file

@ -33,9 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const calendar = google.calendar({version: 'v3', auth}); const calendar = google.calendar({version: 'v3', auth});
calendar.freebusy.query({ calendar.freebusy.query({
requestBody: { requestBody: {
timeMin: req.query.date + "T00:00:00.00Z", timeMin: req.query.dateFrom,
timeMax: req.query.date + "T23:59:59.59Z", timeMax: req.query.dateTo,
timeZone: currentUser.timeZone,
items: [{ items: [{
"id": "primary" "id": "primary"
}] }]