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,7 +1,7 @@
|
|||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
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 {collectPageParameters, telemetryEventTypes, useTelemetry} from "../../lib/telemetry";
|
||||
import {useEffect, useState} from "react";
|
||||
|
@ -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,8 +1,9 @@
|
|||
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;
|
||||
|
@ -20,6 +21,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
});
|
||||
|
||||
const rescheduleUid = req.body.rescheduleUid;
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: req.body.eventName,
|
||||
title: req.body.eventName + ' with ' + req.body.name,
|
||||
|
@ -33,6 +36,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
]
|
||||
};
|
||||
|
||||
// TODO: Use UUID algorithm to shorten this
|
||||
const hashUID = sha256(JSON.stringify(evt));
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: currentUser.id,
|
||||
|
@ -43,15 +49,80 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
});
|
||||
|
||||
const result = await createEvent(currentUser.credentials[0], evt);
|
||||
let results = undefined;
|
||||
let referencesToCreate = undefined;
|
||||
|
||||
const hashUID = sha256(JSON.stringify(evt));
|
||||
const referencesToCreate = currentUser.credentials.length == 0 ? [] : [
|
||||
{
|
||||
type: currentUser.credentials[0].type,
|
||||
uid: result.id
|
||||
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: {
|
||||
|
@ -73,11 +144,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
});
|
||||
|
||||
if (!result.disableConfirmationEmail) {
|
||||
// If one of the integrations allows email confirmations, send it.
|
||||
if (!results.every((result) => result.disableConfirmationEmail)) {
|
||||
await createConfirmBookedEmail(
|
||||
evt, hashUID
|
||||
);
|
||||
}
|
||||
|
||||
res.status(200).json(result);
|
||||
res.status(200).json(results);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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") {
|
||||
|
@ -19,38 +20,44 @@ export default async function handler(req, res) {
|
|||
attendees: true,
|
||||
references: {
|
||||
select: {
|
||||
uid: true
|
||||
uid: true,
|
||||
type: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const credentials = bookingToDelete.user.credentials[0];
|
||||
//TODO Delete from multiple references later
|
||||
const refUid = bookingToDelete.references[0].uid;
|
||||
|
||||
await prisma.attendee.deleteMany({
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.bookingReference.deleteMany({
|
||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: bookingToDelete.id
|
||||
}
|
||||
});
|
||||
|
||||
//TODO Perhaps send emails to user and client to tell about the cancellation
|
||||
|
||||
const deleteBooking = await prisma.booking.delete({
|
||||
const bookingDeletes = prisma.booking.delete({
|
||||
where: {
|
||||
id: bookingToDelete.id,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteEvent(credentials, refUid);
|
||||
await Promise.all([
|
||||
apiDeletes,
|
||||
attendeeDeletes,
|
||||
bookingReferenceDeletes,
|
||||
bookingDeletes
|
||||
]);
|
||||
|
||||
res.status(200).json({message: 'Booking deleted successfully'});
|
||||
//TODO Perhaps send emails to user and client to tell about the cancellation
|
||||
|
||||
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