Functionality works, only thing left is a bit of cleaning up and all done

This commit is contained in:
Alex van Andel 2021-06-16 22:27:27 +00:00
parent 03f583b021
commit 9d5186f1e3
12 changed files with 317 additions and 269 deletions

View File

@ -1,100 +0,0 @@
import {ClockIcon} from "@heroicons/react/outline";
import {useRef} from "react";
export default function SetTimesModal(props) {
const isNew = props.isNew || false;
const {startDate, endDate} = props.schedule;
const startHoursRef = useRef<HTMLInputElement>();
const startMinsRef = useRef<HTMLInputElement>();
const endHoursRef = useRef<HTMLInputElement>();
const endMinsRef = useRef<HTMLInputElement>();
function updateStartEndTimesHandler(event) {
event.preventDefault();
const enteredStartHours = parseInt(startHoursRef.current.value);
const enteredStartMins = parseInt(startMinsRef.current.value);
const enteredEndHours = parseInt(endHoursRef.current.value);
const enteredEndMins = parseInt(endMinsRef.current.value);
props.onChange({
startDate: startDate.minute(enteredStartMins).hour(enteredStartHours),
endDate: endDate.minute(enteredEndMins).hour(enteredEndHours),
});
props.onExit(0);
}
return (
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div
className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<ClockIcon className="h-6 w-6 text-blue-600"/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Change when you are available for bookings
</h3>
<div>
<p className="text-sm text-gray-500">
Set your work schedule
</p>
</div>
</div>
</div>
<form onSubmit={updateStartEndTimesHandler} noValidate>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
<div>
<label htmlFor="startHours" className="sr-only">Hours</label>
<input ref={startHoursRef} type="number" min="0" max="23" maxLength="2" name="hours" id="startHours"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="9" defaultValue={startDate.format('H')} />
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="startMinutes" className="sr-only">Minutes</label>
<input ref={startMinsRef} type="number" min="0" max="59" step="15" maxLength="2" name="minutes" id="startMinutes"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="30" defaultValue={startDate.format('m')} />
</div>
</div>
<div className="flex">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
<div>
<label htmlFor="endHours" className="sr-only">Hours</label>
<input ref={endHoursRef} type="number" min="0" max="23" maxLength="2" name="hours" id="endHours"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="17" defaultValue={endDate.format('H')} />
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="endMinutes" className="sr-only">Minutes</label>
<input ref={endMinsRef} type="number" min="0" max="59" maxLength="2" step="15" name="minutes" id="endMinutes"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="30" defaultValue={endDate.format('m')} />
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
Save
</button>
<button onClick={props.onExit} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>);
}

View File

@ -1,8 +1,8 @@
import React, {useEffect, useState} from "react";
import TimezoneSelect from "react-timezone-select";
import {PencilAltIcon, TrashIcon} from "@heroicons/react/outline";
import {WeekdaySelect} from "./WeekdaySelect";
import SetTimesModal from "../modal/SetTimesModal";
import {WeekdaySelect, Weekday} from "./WeekdaySelect";
import SetTimesModal from "./modal/SetTimesModal";
import Schedule from '../../lib/schedule.model';
import dayjs, {Dayjs} from "dayjs";
import utc from 'dayjs/plugin/utc';
@ -12,44 +12,45 @@ dayjs.extend(timezone);
export const Scheduler = (props) => {
const [ showSetTimesModal, setShowSetTimesModal ]: boolean = useState(false);
const [ schedules, setSchedules ]: Schedule[] = useState(
props.schedules.map( (schedule, idx) => ({
startDate: dayjs(schedule.startDate),
endDate: dayjs(schedule.startDate).startOf('day').add(schedule.length, 'minutes'),
key: idx
}) )
);
const [ schedules, setSchedules ]: Schedule[] = useState(props.schedules.map( schedule => {
const startDate = schedule.isOverride ? dayjs(schedule.startDate) : dayjs.utc().startOf('day').add(schedule.startTime, 'minutes')
return (
{
days: schedule.days,
startDate,
endDate: startDate.add(schedule.length, 'minutes')
}
)
}));
const [ timeZone, setTimeZone ] = useState(props.timeZone);
const [ selectedSchedule, setSelectedSchedule ]: Schedule | null = useState(null);
const [ editSchedule, setEditSchedule ] = useState(-1);
const addNewSchedule = () => {
setSelectedSchedule({
startDate: dayjs().startOf('day').add(0, 'minutes'),
endDate: dayjs().startOf('day').add(1439, 'minutes'),
});
setShowSetTimesModal(true);
useEffect( () => {
props.onChange(schedules);
}, [schedules])
const addNewSchedule = () => setEditSchedule(schedules.length);
const applyEditSchedule = (changed: Schedule) => {
const replaceWith = {
...schedules[editSchedule],
...changed
};
schedules.splice(editSchedule, 1, replaceWith);
setSchedules([].concat(schedules));
}
const upsertSchedule = (changed: Schedule) => {
if (changed.key) {
schedules.splice(
schedules.findIndex( (schedule) => changed.key === schedule.key ), 1, changed
)
setSchedules([].concat(schedules)); // update
}
else {
console.log(changed);
setSchedules(schedules.concat([changed])); // insert
}
}
const removeSchedule = (toRemove: Schedule) => {
schedules.splice(schedules.findIndex( (schedule) => schedule.key === toRemove.key ), 1);
const removeScheduleAt = (toRemove: number) => {
schedules.splice(toRemove, 1);
setSchedules([].concat(schedules));
};
const setWeekdays = (idx: number, days: number[]) => {
schedules[idx].days = days;
setSchedules([].concat(schedules));
}
return (
<div>
<div className="rounded border flex">
@ -63,15 +64,15 @@ export const Scheduler = (props) => {
</div>
</div>
<ul>
{schedules.length > 0 && schedules.map( (schedule) =>
<li key={schedule.key} className="py-2 flex justify-between border-t">
{schedules.map( (schedule, idx) =>
<li key={idx} className="py-2 flex justify-between border-t">
<div className="inline-flex ml-2">
<WeekdaySelect />
<button className="ml-2 text-sm px-2" type="button" onClick={() => { setSelectedSchedule(schedule); setShowSetTimesModal(true) }}>
<WeekdaySelect defaultValue={schedules[idx].days} onSelect={(days: number[]) => setWeekdays(idx, days)} />
<button className="ml-2 text-sm px-2" type="button" onClick={() => setEditSchedule(idx)}>
{schedule.startDate.format(schedule.startDate.minute() === 0 ? 'ha' : 'h:mma')} until {schedule.endDate.format(schedule.endDate.minute() === 0 ? 'ha' : 'h:mma')}
</button>
</div>
<button type="button" onClick={() => removeSchedule(schedule)}
<button type="button" onClick={() => removeScheduleAt(idx)}
className="btn-sm bg-transparent px-2 py-1 ml-1">
<TrashIcon className="h-6 w-6 inline text-gray-400 -mt-1" />
</button>
@ -88,10 +89,10 @@ export const Scheduler = (props) => {
<button className="btn-sm btn-white">Add a date override</button>*/}
</div>
</div>
{showSetTimesModal &&
<SetTimesModal schedule={selectedSchedule}
onChange={upsertSchedule}
onExit={() => setShowSetTimesModal(false)} />
{editSchedule >= 0 &&
<SetTimesModal schedule={schedules[editSchedule]}
onChange={applyEditSchedule}
onExit={() => setEditSchedule(-1)} />
}
{/*{showDateOverrideModal &&
<DateOverrideModal />

View File

@ -1,14 +1,17 @@
import React, {useState} from "react";
import React, {useEffect, useState} from "react";
export const WeekdaySelect = (props) => {
const [ activeDays, setActiveDays ] = useState([false, true, true, true, true, true, false]);
const [ activeDays, setActiveDays ] = useState([1,2,3,4,5,6,7].map( (v) => (props.defaultValue || []).indexOf(v) !== -1));
const days = [ 'S', 'M', 'T', 'W', 'T', 'F', 'S' ];
useEffect( () => {
props.onSelect(activeDays.map( (isActive, idx) => isActive ? idx + 1 : 0).filter( (v) => 0 !== v ));
}, [activeDays]);
const toggleDay = (e, idx: number) => {
e.preventDefault();
activeDays[idx] = !activeDays[idx];
console.log(activeDays);
setActiveDays([].concat(activeDays));
}

View File

@ -0,0 +1,101 @@
import {ClockIcon} from "@heroicons/react/outline";
import {useRef} from "react";
import dayjs from "dayjs";
export default function SetTimesModal(props) {
const {startDate, endDate} = props.schedule || {
startDate: dayjs().startOf('day').add(540, 'minutes'),
endDate: dayjs().startOf('day').add(1020, 'minutes'),
};
const startHoursRef = useRef<HTMLInputElement>();
const startMinsRef = useRef<HTMLInputElement>();
const endHoursRef = useRef<HTMLInputElement>();
const endMinsRef = useRef<HTMLInputElement>();
function updateStartEndTimesHandler(event) {
event.preventDefault();
const enteredStartHours = parseInt(startHoursRef.current.value);
const enteredStartMins = parseInt(startMinsRef.current.value);
const enteredEndHours = parseInt(endHoursRef.current.value);
const enteredEndMins = parseInt(endMinsRef.current.value);
props.onChange({
startDate: startDate.minute(enteredStartMins).hour(enteredStartHours),
endDate: endDate.minute(enteredEndMins).hour(enteredEndHours),
});
props.onExit(0);
}
return (
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div
className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<ClockIcon className="h-6 w-6 text-blue-600"/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Change when you are available for bookings
</h3>
<div>
<p className="text-sm text-gray-500">
Set your work schedule
</p>
</div>
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
<div>
<label htmlFor="startHours" className="sr-only">Hours</label>
<input ref={startHoursRef} type="number" min="0" max="23" maxLength="2" name="hours" id="startHours"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="9" defaultValue={startDate.format('H')} />
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="startMinutes" className="sr-only">Minutes</label>
<input ref={startMinsRef} type="number" min="0" max="59" step="15" maxLength="2" name="minutes" id="startMinutes"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="30" defaultValue={startDate.format('m')} />
</div>
</div>
<div className="flex">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
<div>
<label htmlFor="endHours" className="sr-only">Hours</label>
<input ref={endHoursRef} type="number" min="0" max="23" maxLength="2" name="hours" id="endHours"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="17" defaultValue={endDate.format('H')} />
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="endMinutes" className="sr-only">Minutes</label>
<input ref={endMinsRef} type="number" min="0" max="59" maxLength="2" step="15" name="minutes" id="endMinutes"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="30" defaultValue={endDate.format('m')} />
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button onClick={updateStartEndTimesHandler} type="submit" className="btn btn-primary">
Save
</button>
<button onClick={props.onExit} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</div>
</div>
</div>);
}

View File

@ -1,7 +1,7 @@
import {Dayjs} from "dayjs";
export default interface Schedule {
key: number;
id: number | null;
startDate: Dayjs;
endDate: Dayjs;
}

View File

@ -1,45 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getSession } from 'next-auth/client';
import prisma from '../../../lib/prisma';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({req: req});
if (!session) {
res.status(401).json({message: "Not authenticated"});
return;
}
PUT /api/availability/schedule/{id}/timezone
{
"timeZone": "Europe/London"
}
PATCH /api/availability/schedule {
"schedules": [
{
}
],
"overrides": {
}
}
if (req.method == "PATCH") {
const startMins = req.body.start;
const endMins = req.body.end;
const updateDay = await prisma.schedule.update({
where: {
id: session.user.id,
},
data: {
startTime: startMins,
endTime: endMins
},
});
res.status(200).json({message: 'Start and end times updated successfully'});
}
}

View File

@ -0,0 +1,69 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getSession } from 'next-auth/client';
import prisma from '../../../../lib/prisma';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({req});
if (!session) {
res.status(401).json({message: "Not authenticated"});
return;
}
if (req.method == "PUT") {
const openingHours = req.body.openingHours || [];
const overrides = req.body.overrides || [];
const removeSchedule = await prisma.schedule.deleteMany({
where: {
eventTypeId: +req.query.eventtype,
}
})
const updateSchedule = Promise.all(openingHours.map( (schedule) => prisma.schedule.create({
data: {
eventTypeId: +req.query.eventtype,
days: schedule.days,
startTime: schedule.startTime,
length: schedule.endTime - schedule.startTime,
},
})))
.catch( (error) => {
console.log(error);
})
}
res.status(200).json({message: 'Created schedule'});
/*if (req.method == "PATCH") {
const openingHours = req.body.openingHours || [];
const overrides = req.body.overrides || [];
openingHours.forEach( (schedule) => {
const updateSchedule = await prisma.schedule.update({
where: {
id: req.body.id,
},
data: {
eventTypeId: req.query.eventtype,
days: req.body.days,
startTime: 333,
endTime: 540 - req.body.startTime,
},
});
});
overrides.forEach( (schedule) => {
const updateSchedule = await prisma.schedule.update({
where: {
id: req.body.id,
},
data: {
eventTypeId: req.query.eventtype,
startDate: req.body.startDate,
length: 540,
},
});
});*/
}

View File

@ -1,18 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import prisma from '../../../lib/prisma';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { user } = req.query
const schedules = await prisma.schedule.find({
where: {
eventTypeId: req.query.type,
},
select: {
credentials: true,
timeZone: true
}
});
return res.status(202).send(null);
}

View File

@ -29,6 +29,7 @@ export default function EventType(props) {
const [ showLocationModal, setShowLocationModal ] = useState(false);
const [ selectedLocation, setSelectedLocation ] = useState<OptionBase | undefined>(undefined);
const [ locations, setLocations ] = useState(props.eventType.locations || []);
const [ schedule, setSchedule ] = useState(undefined);
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
@ -40,8 +41,6 @@ export default function EventType(props) {
return <p className="text-gray-400">Loading...</p>;
}
console.log(props);
async function updateEventTypeHandler(event) {
event.preventDefault();
@ -60,6 +59,31 @@ export default function EventType(props) {
}
});
if (schedule) {
let schedulePayload = { "overrides": [], "timeZone": props.user.timeZone, "openingHours": [] };
schedule.forEach( (item) => {
if (item.isOverride) {
delete item.isOverride;
schedulePayload.overrides.push(item);
} else {
schedulePayload.openingHours.push({
days: item.days,
startTime: item.startDate.hour() * 60 + item.startDate.minute(),
endTime: item.endDate.hour() * 60 + item.endDate.minute()
});
}
});
const response = await fetch('/api/availability/schedule/' + props.eventType.id, {
method: 'PUT',
body: JSON.stringify(schedulePayload),
headers: {
'Content-Type': 'application/json'
}
});
}
router.push('/availability');
}
@ -262,16 +286,16 @@ export default function EventType(props) {
</div>
</div>
</div>
</form>
<hr className="my-4"/>
<div>
<h3 className="mb-2">How do you want to offer your availability for this event type?</h3>
<Scheduler timeZone={props.user.timeZone} schedules={props.schedules} />
<div className="py-4 flex justify-end">
<Link href="/availability"><a className="mr-2 btn btn-white">Cancel</a></Link>
<button type="submit" className="btn btn-primary">Update</button>
<hr className="my-4"/>
<div>
<h3 className="mb-2">How do you want to offer your availability for this event type?</h3>
<Scheduler onChange={setSchedule} timeZone={props.user.timeZone} schedules={props.schedules} />
<div className="py-4 flex justify-end">
<Link href="/availability"><a className="mr-2 btn btn-white">Cancel</a></Link>
<button type="submit" className="btn btn-primary">Update</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
@ -340,52 +364,63 @@ export default function EventType(props) {
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
username: true,
timeZone: true,
startTime: true,
endTime: true,
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
username: true,
timeZone: true,
startTime: true,
endTime: true,
}
});
});
const eventType = await prisma.eventType.findUnique({
where: {
id: parseInt(context.query.type),
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
hidden: true,
locations: true,
}
});
const eventType = await prisma.eventType.findUnique({
where: {
id: parseInt(context.query.type),
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
hidden: true,
locations: true,
}
});
const utcOffset = dayjs().tz(user.timeZone).utcOffset();
let schedules = await prisma.schedule.findMany({
where: {
eventTypeId: parseInt(context.query.type),
},
});
const schedules = [
{
key: 0,
startDate: dayjs.utc().startOf('day').add(user.startTime - utcOffset, 'minutes').format(),
length: user.endTime,
}
];
return {
props: {
user,
eventType,
schedules
if (!schedules.length) {
schedules = await prisma.schedule.findMany({
where: {
userId: user.id,
},
});
if (!schedules.length) {
schedules.push({
days: [ 1, 2, 3, 4, 5, 6, 7 ],
startTime: user.startTime,
length: user.endTime >= 1440 ? 1439 : user.endTime,
});
}
}
return {
props: {
user,
eventType,
schedules
},
}
}

View File

@ -21,7 +21,7 @@ model EventType {
user User? @relation(fields: [userId], references: [id])
userId Int?
bookings Booking[]
availability Interval[]
availability Schedule[]
}
model Credential {
@ -50,7 +50,7 @@ model User {
credentials Credential[]
teams Membership[]
bookings Booking[]
availability Interval[]
availability Schedule[]
@@map(name: "users")
}
@ -124,14 +124,16 @@ model Booking {
updatedAt DateTime?
}
model Interval {
id Int @default(autoincrement()) @id
model Schedule {
id Int @default(autoincrement()) @id
label String?
user User? @relation(fields: [userId], references: [id])
user User? @relation(fields: [userId], references: [id])
userId Int?
eventType EventType? @relation(fields: [eventTypeId], references: [id])
eventType EventType? @relation(fields: [eventTypeId], references: [id])
eventTypeId Int?
startTime DateTime
days Int[]
startTime Int?
startDate DateTime? @db.Timestamptz(3)
length Int
isOverride Boolean @default(false)
isOverride Boolean @default(false)
}