Implemented rescheduling and concurrent usage of all integrations
parent
403823fc62
commit
af08c74c8a
File diff suppressed because it is too large
Load Diff
|
@ -14,6 +14,7 @@
|
||||||
"@jitsu/sdk-js": "^2.0.1",
|
"@jitsu/sdk-js": "^2.0.1",
|
||||||
"@prisma/client": "^2.23.0",
|
"@prisma/client": "^2.23.0",
|
||||||
"@tailwindcss/forms": "^0.2.1",
|
"@tailwindcss/forms": "^0.2.1",
|
||||||
|
"async": "^3.2.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"googleapis": "^67.1.1",
|
"googleapis": "^67.1.1",
|
||||||
|
@ -26,7 +27,8 @@
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-phone-number-input": "^3.1.21",
|
"react-phone-number-input": "^3.1.21",
|
||||||
"react-select": "^4.3.0",
|
"react-select": "^4.3.0",
|
||||||
"react-timezone-select": "^1.0.2"
|
"react-timezone-select": "^1.0.2",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^14.14.33",
|
"@types/node": "^14.14.33",
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import {useRouter} from 'next/router';
|
||||||
import { ClockIcon, CalendarIcon, LocationMarkerIcon } from '@heroicons/react/solid';
|
import {CalendarIcon, ClockIcon, LocationMarkerIcon} from '@heroicons/react/solid';
|
||||||
import prisma from '../../lib/prisma';
|
import prisma from '../../lib/prisma';
|
||||||
import {collectPageParameters, telemetryEventTypes, useTelemetry} from "../../lib/telemetry";
|
import {collectPageParameters, telemetryEventTypes, useTelemetry} from "../../lib/telemetry";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
import PhoneInput from 'react-phone-number-input';
|
import PhoneInput from 'react-phone-number-input';
|
||||||
import { LocationType } from '../../lib/location';
|
import {LocationType} from '../../lib/location';
|
||||||
import Avatar from '../../components/Avatar';
|
import Avatar from '../../components/Avatar';
|
||||||
import Button from '../../components/ui/Button';
|
import Button from '../../components/ui/Button';
|
||||||
|
|
||||||
|
@ -117,13 +117,13 @@ export default function Book(props) {
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Your name</label>
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Your name</label>
|
||||||
<div className="mt-1">
|
<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>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
|
||||||
<div className="mt-1">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{locations.length > 1 && (
|
{locations.length > 1 && (
|
||||||
|
@ -145,7 +145,7 @@ export default function Book(props) {
|
||||||
</div>)}
|
</div>)}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">Additional notes</label>
|
<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>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<Button type="submit" className="btn btn-primary">{rescheduleUid ? 'Reschedule' : 'Confirm'}</Button>
|
<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) {
|
if(context.query.rescheduleUid) {
|
||||||
booking = await prisma.booking.findFirst({
|
booking = await prisma.booking.findFirst({
|
||||||
|
|
|
@ -1,83 +1,155 @@
|
||||||
import type {NextApiRequest, NextApiResponse} from 'next';
|
import type {NextApiRequest, NextApiResponse} from 'next';
|
||||||
import prisma from '../../../lib/prisma';
|
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 createConfirmBookedEmail from "../../../lib/emails/confirm-booked";
|
||||||
import sha256 from "../../../lib/sha256";
|
import sha256 from "../../../lib/sha256";
|
||||||
|
import async from 'async';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const {user} = req.query;
|
const {user} = req.query;
|
||||||
|
|
||||||
const currentUser = await prisma.user.findFirst({
|
const currentUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username: user,
|
username: user,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
email: true,
|
email: true,
|
||||||
name: 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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
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 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) {
|
export default async function handler(req, res) {
|
||||||
if (req.method == "POST") {
|
if (req.method == "POST") {
|
||||||
const uid = req.body.uid;
|
const uid = req.body.uid;
|
||||||
|
|
||||||
const bookingToDelete = await prisma.booking.findFirst({
|
const bookingToDelete = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
uid: uid,
|
uid: uid,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
credentials: true
|
credentials: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
attendees: true,
|
attendees: true,
|
||||||
references: {
|
references: {
|
||||||
select: {
|
select: {
|
||||||
uid: true
|
uid: true,
|
||||||
}
|
type: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const credentials = bookingToDelete.user.credentials[0];
|
const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => {
|
||||||
//TODO Delete from multiple references later
|
const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0].uid;
|
||||||
const refUid = bookingToDelete.references[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({
|
await Promise.all([
|
||||||
where: {
|
apiDeletes,
|
||||||
bookingId: bookingToDelete.id
|
attendeeDeletes,
|
||||||
}
|
bookingReferenceDeletes,
|
||||||
});
|
bookingDeletes
|
||||||
|
]);
|
||||||
|
|
||||||
await prisma.bookingReference.deleteMany({
|
//TODO Perhaps send emails to user and client to tell about the cancellation
|
||||||
where: {
|
|
||||||
bookingId: bookingToDelete.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//TODO Perhaps send emails to user and client to tell about the cancellation
|
res.status(200).json({message: 'Booking successfully deleted.'});
|
||||||
|
} else {
|
||||||
const deleteBooking = await prisma.booking.delete({
|
res.status(405).json({message: 'This endpoint only accepts POST requests.'});
|
||||||
where: {
|
}
|
||||||
id: bookingToDelete.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await deleteEvent(credentials, refUid);
|
|
||||||
|
|
||||||
res.status(200).json({message: 'Booking deleted successfully'});
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -457,6 +457,11 @@ ast-types@0.13.2:
|
||||||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
|
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
|
||||||
integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==
|
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:
|
at-least-node@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
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"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
uuid@^8.0.0:
|
uuid@^8.0.0, uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
Loading…
Reference in New Issue