Implemented rescheduling and concurrent usage of all integrations

pull/253/head
nicolas 2021-06-09 21:46:41 +02:00
parent 403823fc62
commit af08c74c8a
6 changed files with 4124 additions and 130 deletions

3908
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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({

View File

@ -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);
} }

View File

@ -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'});
}
} }

View File

@ -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==