Implemented rescheduling and concurrent usage of all integrations
This commit is contained in:
parent
403823fc62
commit
af08c74c8a
6 changed files with 4124 additions and 130 deletions
3908
package-lock.json
generated
Normal file
3908
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -14,6 +14,7 @@
|
|||
"@jitsu/sdk-js": "^2.0.1",
|
||||
"@prisma/client": "^2.23.0",
|
||||
"@tailwindcss/forms": "^0.2.1",
|
||||
"async": "^3.2.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dayjs": "^1.10.4",
|
||||
"googleapis": "^67.1.1",
|
||||
|
@ -26,7 +27,8 @@
|
|||
"react-dom": "17.0.1",
|
||||
"react-phone-number-input": "^3.1.21",
|
||||
"react-select": "^4.3.0",
|
||||
"react-timezone-select": "^1.0.2"
|
||||
"react-timezone-select": "^1.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.33",
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ClockIcon, CalendarIcon, LocationMarkerIcon } from '@heroicons/react/solid';
|
||||
import {useRouter} from 'next/router';
|
||||
import {CalendarIcon, ClockIcon, LocationMarkerIcon} from '@heroicons/react/solid';
|
||||
import prisma from '../../lib/prisma';
|
||||
import {collectPageParameters, telemetryEventTypes, useTelemetry} from "../../lib/telemetry";
|
||||
import { useEffect, useState } from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import 'react-phone-number-input/style.css';
|
||||
import PhoneInput from 'react-phone-number-input';
|
||||
import { LocationType } from '../../lib/location';
|
||||
import {LocationType} from '../../lib/location';
|
||||
import Avatar from '../../components/Avatar';
|
||||
import Button from '../../components/ui/Button';
|
||||
|
||||
|
@ -117,13 +117,13 @@ export default function Book(props) {
|
|||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Your name</label>
|
||||
<div className="mt-1">
|
||||
<input type="text" name="name" id="name" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="John Doe" defaultValue={props.booking.attendees[0].name} />
|
||||
<input type="text" name="name" id="name" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="John Doe" defaultValue={props.booking && props.booking.attendees[0].name} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
|
||||
<div className="mt-1">
|
||||
<input type="email" name="email" id="email" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com" defaultValue={props.booking.attendees[0].email} />
|
||||
<input type="email" name="email" id="email" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com" defaultValue={props.booking && props.booking.attendees[0].email} />
|
||||
</div>
|
||||
</div>
|
||||
{locations.length > 1 && (
|
||||
|
@ -145,7 +145,7 @@ export default function Book(props) {
|
|||
</div>)}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">Additional notes</label>
|
||||
<textarea name="notes" id="notes" rows={3} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Please share anything that will help prepare for our meeting." defaultValue={props.booking.description}></textarea>
|
||||
<textarea name="notes" id="notes" rows={3} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Please share anything that will help prepare for our meeting." defaultValue={props.booking && props.booking.description}></textarea>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<Button type="submit" className="btn btn-primary">{rescheduleUid ? 'Reschedule' : 'Confirm'}</Button>
|
||||
|
@ -191,7 +191,7 @@ export async function getServerSideProps(context) {
|
|||
}
|
||||
});
|
||||
|
||||
let booking = undefined;
|
||||
let booking = null;
|
||||
|
||||
if(context.query.rescheduleUid) {
|
||||
booking = await prisma.booking.findFirst({
|
||||
|
|
|
@ -1,83 +1,155 @@
|
|||
import type {NextApiRequest, NextApiResponse} from 'next';
|
||||
import prisma from '../../../lib/prisma';
|
||||
import {createEvent, CalendarEvent} from '../../../lib/calendarClient';
|
||||
import {CalendarEvent, createEvent, updateEvent} from '../../../lib/calendarClient';
|
||||
import createConfirmBookedEmail from "../../../lib/emails/confirm-booked";
|
||||
import sha256 from "../../../lib/sha256";
|
||||
import async from 'async';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {user} = req.query;
|
||||
const {user} = req.query;
|
||||
|
||||
const currentUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: user,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
}
|
||||
});
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: req.body.eventName,
|
||||
title: req.body.eventName + ' with ' + req.body.name,
|
||||
description: req.body.notes,
|
||||
startTime: req.body.start,
|
||||
endTime: req.body.end,
|
||||
location: req.body.location,
|
||||
organizer: {email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone},
|
||||
attendees: [
|
||||
{email: req.body.email, name: req.body.name, timeZone: req.body.timeZone}
|
||||
]
|
||||
};
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
title: evt.type
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
});
|
||||
|
||||
const result = await createEvent(currentUser.credentials[0], evt);
|
||||
|
||||
const hashUID = sha256(JSON.stringify(evt));
|
||||
const referencesToCreate = currentUser.credentials.length == 0 ? [] : [
|
||||
{
|
||||
type: currentUser.credentials[0].type,
|
||||
uid: result.id
|
||||
}
|
||||
];
|
||||
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
uid: hashUID,
|
||||
userId: currentUser.id,
|
||||
references: {
|
||||
create: referencesToCreate
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
|
||||
title: evt.title,
|
||||
description: evt.description,
|
||||
startTime: evt.startTime,
|
||||
endTime: evt.endTime,
|
||||
|
||||
attendees: {
|
||||
create: evt.attendees
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.disableConfirmationEmail) {
|
||||
await createConfirmBookedEmail(
|
||||
evt, hashUID
|
||||
);
|
||||
const currentUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: user,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json(result);
|
||||
const rescheduleUid = req.body.rescheduleUid;
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: req.body.eventName,
|
||||
title: req.body.eventName + ' with ' + req.body.name,
|
||||
description: req.body.notes,
|
||||
startTime: req.body.start,
|
||||
endTime: req.body.end,
|
||||
location: req.body.location,
|
||||
organizer: {email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone},
|
||||
attendees: [
|
||||
{email: req.body.email, name: req.body.name, timeZone: req.body.timeZone}
|
||||
]
|
||||
};
|
||||
|
||||
// TODO: Use UUID algorithm to shorten this
|
||||
const hashUID = sha256(JSON.stringify(evt));
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
title: evt.type
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
});
|
||||
|
||||
let results = undefined;
|
||||
let referencesToCreate = undefined;
|
||||
|
||||
if (rescheduleUid) {
|
||||
// Reschedule event
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: rescheduleUid
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
references: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
uid: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Use all integrations
|
||||
results = await async.mapLimit(currentUser.credentials, 5, async (credential) => {
|
||||
const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||
return await updateEvent(credential, bookingRefUid, evt)
|
||||
});
|
||||
|
||||
// Clone elements
|
||||
referencesToCreate = [...booking.references];
|
||||
|
||||
// Now we can delete the old booking and its references.
|
||||
let bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id
|
||||
}
|
||||
});
|
||||
let attendeeDeletes = prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id
|
||||
}
|
||||
});
|
||||
let bookingDeletes = prisma.booking.delete({
|
||||
where: {
|
||||
uid: rescheduleUid
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
bookingReferenceDeletes,
|
||||
attendeeDeletes,
|
||||
bookingDeletes
|
||||
]);
|
||||
} else {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Schedule event
|
||||
results = await async.mapLimit(currentUser.credentials, 5, async (credential) => {
|
||||
const response = await createEvent(credential, evt);
|
||||
return {
|
||||
type: credential.type,
|
||||
response
|
||||
};
|
||||
});
|
||||
|
||||
referencesToCreate = results.map((result => {
|
||||
return {
|
||||
type: result.type,
|
||||
uid: result.response.id
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
uid: hashUID,
|
||||
userId: currentUser.id,
|
||||
references: {
|
||||
create: referencesToCreate
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
|
||||
title: evt.title,
|
||||
description: evt.description,
|
||||
startTime: evt.startTime,
|
||||
endTime: evt.endTime,
|
||||
|
||||
attendees: {
|
||||
create: evt.attendees
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If one of the integrations allows email confirmations, send it.
|
||||
if (!results.every((result) => result.disableConfirmationEmail)) {
|
||||
await createConfirmBookedEmail(
|
||||
evt, hashUID
|
||||
);
|
||||
}
|
||||
|
||||
res.status(200).json(results);
|
||||
}
|
||||
|
|
|
@ -1,56 +1,63 @@
|
|||
import prisma from '../../lib/prisma';
|
||||
import {createEvent, deleteEvent} from "../../lib/calendarClient";
|
||||
import {deleteEvent} from "../../lib/calendarClient";
|
||||
import async from 'async';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == "POST") {
|
||||
const uid = req.body.uid;
|
||||
if (req.method == "POST") {
|
||||
const uid = req.body.uid;
|
||||
|
||||
const bookingToDelete = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: uid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
user: {
|
||||
select: {
|
||||
credentials: true
|
||||
}
|
||||
},
|
||||
attendees: true,
|
||||
references: {
|
||||
select: {
|
||||
uid: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const bookingToDelete = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: uid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
user: {
|
||||
select: {
|
||||
credentials: true
|
||||
}
|
||||
},
|
||||
attendees: true,
|
||||
references: {
|
||||
select: {
|
||||
uid: true,
|
||||
type: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const credentials = bookingToDelete.user.credentials[0];
|
||||
//TODO Delete from multiple references later
|
||||
const refUid = bookingToDelete.references[0].uid;
|
||||
const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => {
|
||||
const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||
return await deleteEvent(credential, bookingRefUid);
|
||||
});
|
||||
const attendeeDeletes = prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: bookingToDelete.id
|
||||
}
|
||||
});
|
||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: bookingToDelete.id
|
||||
}
|
||||
});
|
||||
const bookingDeletes = prisma.booking.delete({
|
||||
where: {
|
||||
id: bookingToDelete.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: bookingToDelete.id
|
||||
}
|
||||
});
|
||||
await Promise.all([
|
||||
apiDeletes,
|
||||
attendeeDeletes,
|
||||
bookingReferenceDeletes,
|
||||
bookingDeletes
|
||||
]);
|
||||
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: bookingToDelete.id
|
||||
}
|
||||
});
|
||||
//TODO Perhaps send emails to user and client to tell about the cancellation
|
||||
|
||||
//TODO Perhaps send emails to user and client to tell about the cancellation
|
||||
|
||||
const deleteBooking = await prisma.booking.delete({
|
||||
where: {
|
||||
id: bookingToDelete.id,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteEvent(credentials, refUid);
|
||||
|
||||
res.status(200).json({message: 'Booking deleted successfully'});
|
||||
}
|
||||
res.status(200).json({message: 'Booking successfully deleted.'});
|
||||
} else {
|
||||
res.status(405).json({message: 'This endpoint only accepts POST requests.'});
|
||||
}
|
||||
}
|
|
@ -457,6 +457,11 @@ ast-types@0.13.2:
|
|||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
|
||||
integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==
|
||||
|
||||
async@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
at-least-node@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
|
@ -3275,7 +3280,7 @@ uuid@^3.3.3:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
uuid@^8.0.0:
|
||||
uuid@^8.0.0, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
|
Loading…
Reference in a new issue