Completely rebuilt logic when dealing with timezones. Now all available events should appear when selecting a date.
This commit is contained in:
parent
96e2b762c6
commit
2b0e8bef7a
3 changed files with 145 additions and 54 deletions
94
lib/slots.ts
Normal file
94
lib/slots.ts
Normal 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
|
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}]
|
}]
|
||||||
|
|
Loading…
Reference in a new issue