Compare commits

...

14 Commits

Author SHA1 Message Date
Joe Au-Yeung 37148ac335 Add app store doc 2022-03-11 09:19:51 -05:00
Joe Au-Yeung bef07cc833 Merge branch 'main' of https://github.com/calcom/cal.com into feature/twilio-sms-reminders 2022-03-10 17:06:11 -05:00
Joe Au-Yeung 5ea854dd8b Merge branch 'main' of https://github.com/calcom/cal.com into feature/twilio-sms-reminders 2022-03-10 14:41:11 -05:00
Joe Au-Yeung 65d6f06cfe Map to singular time unit 2022-03-10 14:31:15 -05:00
Joe Au-Yeung a0b67bf2e3 Loop through reminders 2022-03-10 14:30:58 -05:00
Joe Au-Yeung c3e019394d Merge branch 'main' of https://github.com/calcom/cal.com into feature/twilio-sms-reminders 2022-03-09 10:37:44 -05:00
Joe Au-Yeung 94acd691bb Add Twilio package 2022-03-09 10:37:11 -05:00
Joe Au-Yeung 46d893768d Save reminderPhone to DB 2022-03-08 17:57:39 -05:00
Joe Au-Yeung 17e025f3c2 Add field for number to send reminders 2022-03-08 16:07:56 -05:00
Joe Au-Yeung 965c242659 Merge branch 'main' of https://github.com/calcom/cal.com into feature/twilio-email-reminders 2022-03-08 09:58:02 -05:00
Joe Au-Yeung 52b9e1b809 Save attendee reminders to db 2022-03-05 23:33:34 -05:00
Joe Au-Yeung ddf9323908 Add, remove, edit attendee reminders 2022-03-05 22:26:03 -05:00
Joe Au-Yeung 1816139295 Add event reminder modal & section 2022-03-05 15:55:10 -05:00
Joe Au-Yeung 74e2e8ba58 Add EventTypeAttendeeReminders to prisma 2022-03-05 15:54:46 -05:00
17 changed files with 736 additions and 907 deletions

View File

@ -0,0 +1,43 @@
---
title: Adding CSS
---
Since Cal.com is open source we encourage developers to create new apps for others to use. This guide is to help you get started.
## Structure
All apps can be found under `packages/app-store`. In this folder is `_example` which shows the general structure of an app.
```
_example
| index.ts
| package.json
| .env.example
|
|---api
| | example.ts
| | index.ts
|
|---lib
| | adaptor.ts
| | index.ts
|
|---static
| | icon.svg
```
## Getting Started
In the `package.json` name your package appropriately and list the dependencies needed for the package.
Next in the `.env.example` specify the environmental variables (ex. auth token, API secrets) that your app will need. In a comment add a link to instructions on how to obtain the credentials. Create a `.env` with your the filled in environmental variables.
In `index.js` fill out the meta data that will be rendered on the app page. Under `packages/app-store/index.ts`, import your app and add it under `appStore`. Your app should now appear in the app store.
Under the `/api` folder, this is where any API calls that are associated with your app will be handled. Since cal.com uses Next.js we use dynamic API routes. In this example if we want to hit `/api/example.ts` the route would be `{BASE_URL}/api/integrations/_example/example`. Export your endpoints in an `index.ts` file under `/api` folder and import them in your main `index.ts` file.
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
The `/static` folder is where your assets live.
If you need any help feel free to join us on Slack: https://cal.com/slack

View File

@ -46,6 +46,7 @@ type BookingFormValues = {
locationType?: LocationType;
guests?: string[];
phone?: string;
reminderPhone?: string;
customInputs?: {
[key: string]: string;
};
@ -259,6 +260,7 @@ const BookingPage = (props: BookingPageProps) => {
label: props.eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label,
value: booking.customInputs![inputId],
})),
reminderPhone: booking.reminderPhone,
});
};
@ -468,6 +470,24 @@ const BookingPage = (props: BookingPageProps) => {
)}
</div>
))}
{props.eventType.customInputs && (
<div className="mb-4">
<label
htmlFor="reminderPhone"
className="block text-sm font-medium text-gray-700 dark:text-white">
{t("send_reminders_number")}
</label>
<div className="mt-1">
<PhoneInput
// @ts-expect-error
control={bookingForm.control}
name="reminderPhone"
placeholder={t("enter_phone_number")}
id="reminderPhone"
/>
</div>
</div>
)}
{!props.eventType.disableGuests && (
<div className="mb-4">
{!guestToggle && (

View File

@ -0,0 +1,117 @@
import {
EventTypeAttendeeReminder,
EventTypeAttendeeReminderMethod,
EventTypeAttendeeReminderTimeUnit,
} from "@prisma/client";
import React, { FC } from "react";
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
import Select from "react-select";
import { useLocale } from "@lib/hooks/useLocale";
import Button from "@components/ui/Button";
interface OptionTypeBase {
label: string;
value: EventTypeAttendeeReminderMethod | EventTypeAttendeeReminderTimeUnit;
}
interface Props {
onSubmit: SubmitHandler<IFormInput>;
onCancel: () => void;
selectedAttendeeReminder?: EventTypeAttendeeReminder;
}
type IFormInput = EventTypeAttendeeReminder;
const AttendeeReminderTypeForm: FC<Props> = (props) => {
const { t } = useLocale();
const methodOptions: OptionTypeBase[] = [
{ value: EventTypeAttendeeReminderMethod.EMAIL, label: t("email") },
{ value: EventTypeAttendeeReminderMethod.SMS, label: t("SMS") },
];
const timeUnitOptions: OptionTypeBase[] = [
{ value: EventTypeAttendeeReminderTimeUnit.MINUTE, label: t("minutes") },
{ value: EventTypeAttendeeReminderTimeUnit.HOUR, label: t("hours") },
{ value: EventTypeAttendeeReminderTimeUnit.DAY, label: t("days") },
];
const { selectedAttendeeReminder } = props;
const defaultValues = selectedAttendeeReminder || { type: methodOptions[0].value };
const { register, control, handleSubmit } = useForm<IFormInput>({
defaultValues,
});
const selectedMethod = useWatch({ name: "method", control });
const selectedMethodOption = methodOptions.find((e) => selectedMethod === e.value)!;
const selectedTimeUnit = useWatch({ name: "timeUnit", control });
const selectedTimeUnitOption = timeUnitOptions.find((e) => selectedTimeUnit === e.value)!;
const onCancel = () => {
props.onCancel();
};
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<div className="mb-2">
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("communication_method")}
</label>
<Controller
name="method"
control={control}
render={({ field }) => (
<Select
id="type"
defaultValue={selectedMethodOption}
options={methodOptions}
isSearchable={false}
className="focus:border-primary-500 focus:ring-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
onChange={(option) => option && field.onChange(option.value)}
value={selectedMethodOption}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</div>
{/* TODO Align these vertically */}
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("when_to_send")}
</label>
<div className="middle mb-2 flex items-center justify-center text-sm">
<input
type="number"
className="focus:border-primary-500 focus:ring-primary-500 block w-12 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
placeholder="30"
defaultValue={selectedAttendeeReminder?.time}
{...register("time", { required: true, valueAsNumber: true })}
/>
<Controller
name="timeUnit"
control={control}
render={({ field }) => (
<Select
id="timeUnit"
defaultValue={selectedTimeUnitOption}
options={timeUnitOptions}
isSearchable={false}
className="focus:border-primary-500 focus:ring-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
onChange={(option) => option && field.onChange(option.value)}
value={selectedTimeUnitOption}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</div>
<div className="mt-5 flex space-x-2 sm:mt-4">
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
{t("cancel")}
</Button>
<Button type="submit">{t("save")}</Button>
</div>
</form>
);
};
export default AttendeeReminderTypeForm;

View File

@ -55,6 +55,7 @@ export interface CalendarEvent {
destinationCalendar?: DestinationCalendar | null;
cancellationReason?: string | null;
rejectionReason?: string | null;
reminderPhone?: string;
}
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {

View File

@ -0,0 +1,48 @@
import { EventTypeAttendeeReminder } from "@prisma/client/";
import dayjs from "dayjs";
import twilio from "twilio";
import prisma from "@lib/prisma";
const accountSid = process.env.TWILIO_SID;
const token = process.env.TWILIO_TOKEN;
const senderNumber = process.env.TWILIO_PHONE_NUMBER;
const client = twilio(accountSid, token);
export const scheduleSMSAttendeeReminders = async (
uid: string,
reminderPhone: string,
startTime: string,
attendeeReminder: EventTypeAttendeeReminder
) => {
// console.log("🚀 ~ file: reminder-manager.ts ~ line 8 ~ startTime", startTime);
console.log(
"🚀 ~ file: reminder-manager.ts ~ line 8 ~ scheduleAttendeeReminders ~ eventType",
attendeeReminder
);
// console.log(
// "🚀 ~ file: reminder-manager.ts ~ line 8 ~ scheduleAttendeeReminders ~ bookings",
// reminderPhone
// );
// await client.messages.create({
// body: "This is a test",
// from: senderNumber,
// to: reminderPhone,
// });
await prisma.attendeeReminder.create({
data: {
booking: {
connect: {
uid: uid,
},
},
method: "SMS",
referenceId: "123",
scheduledFor: dayjs().toISOString(),
scheduled: true,
},
});
};

View File

@ -26,6 +26,7 @@ export type BookingCreateBody = {
metadata: {
[key: string]: string;
};
reminderPhone?: string;
};
export type BookingResponse = Booking & {

View File

@ -102,6 +102,7 @@
"superjson": "1.8.1",
"tsdav": "2.0.0",
"tslog": "^3.2.1",
"twilio": "^3.75.0",
"uuid": "^8.3.2",
"web3": "^1.6.1",
"zod": "^3.8.2"

View File

@ -55,6 +55,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
length: true,
locations: true,
customInputs: true,
attendeeReminders: true,
periodType: true,
periodDays: true,
periodStartDate: true,

View File

@ -50,6 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
const reqBody = req.body as BookingConfirmBody;
console.log("🚀 ~ file: confirm.ts ~ line 53 ~ handler ~ reqBody", reqBody);
const bookingId = reqBody.id;
if (!bookingId) {

View File

@ -28,6 +28,7 @@ import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office36
import logger from "@lib/logger";
import notEmpty from "@lib/notEmpty";
import prisma from "@lib/prisma";
import { scheduleSMSAttendeeReminders } from "@lib/reminders/reminder-manager";
import { BookingCreateBody } from "@lib/types/booking";
import { getBusyVideoTimes } from "@lib/videoClient";
import sendPayload from "@lib/webhooks/sendPayload";
@ -232,6 +233,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
currency: true,
metadata: true,
destinationCalendar: true,
attendeeReminders: true,
},
});
@ -342,6 +344,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
location: reqBody.location, // Will be processed by the EventManager later.
/** For team events, we will need to handle each member destinationCalendar eventually */
destinationCalendar: eventType.destinationCalendar || users[0].destinationCalendar,
reminderPhone: reqBody.reminderPhone,
};
if (eventType.schedulingType === SchedulingType.COLLECTIVE) {
@ -406,6 +409,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
connect: { id: evt.destinationCalendar.id },
}
: undefined,
reminderPhone: evt.reminderPhone,
},
});
}
@ -582,6 +586,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
metadata.entryPoints = results[0].createdEvent?.entryPoints;
}
await sendScheduledEmails({ ...evt, additionInformation: metadata });
// Loops through attendee reminders to schedule them
for (const reminder of eventType.attendeeReminders) {
if (reminder.method === "SMS") {
await scheduleSMSAttendeeReminders(evt.uid, evt.reminderPhone!, evt.startTime, reminder);
}
}
}
}

View File

@ -13,7 +13,14 @@ import {
UsersIcon,
} from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import { Availability, EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
import {
Availability,
EventTypeCustomInput,
EventTypeAttendeeReminder,
PeriodType,
Prisma,
SchedulingType,
} from "@prisma/client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import * as RadioGroup from "@radix-ui/react-radio-group";
import dayjs from "dayjs";
@ -48,6 +55,7 @@ import Loader from "@components/Loader";
import Shell from "@components/Shell";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import { Form } from "@components/form/fields";
import AttendeeReminderTypeForm from "@components/pages/eventtypes/AttendeeReminderTypeForm";
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
import Button from "@components/ui/Button";
import InfoBadge from "@components/ui/InfoBadge";
@ -173,6 +181,13 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
// prettier-ignore
const [selectedAttendeeReminder, setSelectedAttendeeReminder] = useState<EventTypeAttendeeReminder | undefined>(undefined);
const [selectedAttendeeReminderModalOpen, setSelectedAttendeeReminderModalOpen] = useState(false);
const [attendeeReminders, setAttendeeReminders] = useState<EventTypeAttendeeReminder[]>(
eventType.attendeeReminders.sort((a, b) => a.id - b.id) || []
);
const [tokensList, setTokensList] = useState<Array<Token>>([]);
const periodType =
@ -298,6 +313,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
setCustomInputs([...customInputs]);
};
const removeReminder = (index: number) => {
formMethods.getValues("attendeeReminders").splice(index, 1);
attendeeReminders.splice(index, 1);
setAttendeeReminders([...attendeeReminders]);
};
const schedulingTypeOptions: {
value: SchedulingType;
label: string;
@ -371,6 +392,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
integration: string;
externalId: string;
};
attendeeReminders: EventTypeAttendeeReminder[];
}>({
defaultValues: {
locations: eventType.locations || [],
@ -1334,6 +1356,84 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</div>
</div>
<hr className="border-neutral-200" />
<div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label
htmlFor="attendeeReminders"
className="flex text-sm font-medium text-neutral-700">
{t("attendee_reminders")}
</label>
</div>
<div className="w-full">
<Controller
name="attendeeReminders"
control={formMethods.control}
render={() => (
<div className="w-full">
{/* TODO sort reminders based on time from event */}
<ul className="mt-1">
{attendeeReminders.map(
(attendeeReminder: EventTypeAttendeeReminder, idx: number) => (
<li key={idx} className="bg-secondary-50 mb-2 border p-2">
<div className="flex justify-between">
<div className="w-0 flex-1">
<div className="truncate">
<span
className="text-sm ltr:ml-2 rtl:mr-2"
title={`${t("communication_method")}: ${
attendeeReminder.method
}`}>
{t("communication_method")}: {attendeeReminder.method}
</span>
</div>
{attendeeReminder.time && attendeeReminder.timeUnit && (
<div className="truncate">
<span
className="text-sm ltr:ml-2 rtl:mr-2"
title={`${t("when")}: ${attendeeReminder.time}`}>
{t("when")}: {attendeeReminder.time}{" "}
{attendeeReminder.timeUnit} {t("before_event")}
</span>
</div>
)}
</div>
<div className="flex">
<Button
onClick={() => {
setSelectedAttendeeReminder(attendeeReminder);
setSelectedAttendeeReminderModalOpen(true);
}}
color="minimal"
type="button">
{t("edit")}
</Button>
<button type="button" onClick={() => removeReminder(idx)}>
<XIcon className="h-6 w-6 border-l-2 pl-1 hover:text-red-500 " />
</button>
</div>
</div>
</li>
)
)}
</ul>
<Button
onClick={() => {
setSelectedAttendeeReminder(undefined);
setSelectedAttendeeReminderModalOpen(true);
}}
color="secondary"
type="button"
StartIcon={PlusIcon}>
{t("add_input")}
</Button>
</div>
)}
/>
</div>
</div>
{hasPaymentIntegration && (
<>
<hr className="border-neutral-200" />
@ -1634,6 +1734,62 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</Dialog>
)}
/>
<Controller
name="attendeeReminders"
control={formMethods.control}
defaultValue={eventType.attendeeReminders.sort((a, b) => a.id - b.id) || []}
render={() => (
<Dialog
open={selectedAttendeeReminderModalOpen}
onOpenChange={setSelectedAttendeeReminderModalOpen}>
<DialogContent asChild>
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<PlusIcon className="text-primary-600 h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("add_new_attendee_reminder")}
</h3>
<div>
<p className="text-sm text-gray-400">{t("attendee_reminder_description")}</p>
</div>
</div>
</div>
<AttendeeReminderTypeForm
selectedAttendeeReminder={selectedAttendeeReminder}
onSubmit={(values) => {
const attendeeReminder: EventTypeAttendeeReminder = {
id: -1,
eventTypeId: -1,
method: values.method,
timeUnit: values.timeUnit,
time: values.time,
};
if (selectedAttendeeReminder) {
selectedAttendeeReminder.method = attendeeReminder.method;
selectedAttendeeReminder.timeUnit = attendeeReminder.timeUnit;
selectedAttendeeReminder.time = attendeeReminder.time;
} else {
setAttendeeReminders(attendeeReminders.concat(attendeeReminder));
formMethods.setValue(
"attendeeReminders",
formMethods.getValues("attendeeReminders").concat(attendeeReminder)
);
}
setSelectedAttendeeReminderModalOpen(false);
}}
onCancel={() => {
setSelectedAttendeeReminderModalOpen(false);
}}
/>
</div>
</DialogContent>
</Dialog>
)}
/>
{isAdmin && (
<WebhookListContainer
title={t("team_webhooks")}
@ -1748,6 +1904,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
price: true,
currency: true,
destinationCalendar: true,
attendeeReminders: true,
},
});

View File

@ -672,5 +672,13 @@
"example_name": "John Doe",
"time_format": "Time format",
"12_hour": "12 hour",
"24_hour": "24 hour"
"24_hour": "24 hour",
"attendee_reminders": "Attendee Reminders",
"sms": "SMS",
"add_new_attendee_reminder": "Add new attendee reminder",
"attendee_reminder_description": "A reminder will be sent to attendees before the event",
"communication_method": "Reminder Method",
"when_to_send": "When to send the reminder",
"days": "Days",
"send_reminders_number": "Number to send SMS reminders"
}

View File

@ -1,9 +1,16 @@
import { EventTypeCustomInput, MembershipRole, PeriodType, Prisma } from "@prisma/client";
import {
EventTypeCustomInput,
MembershipRole,
EventTypeAttendeeReminder,
PeriodType,
Prisma,
} from "@prisma/client";
import { z } from "zod";
import {
_AvailabilityModel,
_DestinationCalendarModel,
_EventTypeAttendeeReminderModel,
_EventTypeCustomInputModel,
_EventTypeModel,
} from "@calcom/prisma/zod";
@ -63,6 +70,42 @@ function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: n
};
}
function handleAttendeeReminders(attendeeReminders: EventTypeAttendeeReminder[], eventTypeId: number) {
const remindersToDelete = attendeeReminders.filter((input) => input.id > 0).map((e) => e.id);
const remindersToCreate = attendeeReminders
.filter((input) => input.id < 0)
.map((input) => ({
method: input.method,
timeUnit: input.timeUnit,
time: input.time,
}));
const remindersToUpdate = attendeeReminders
.filter((input) => input.id > 0)
.map((input) => ({
data: {
method: input.method,
timeUnit: input.timeUnit,
time: input.time,
},
where: {
id: input.id,
},
}));
return {
deleteMany: {
eventTypeId,
NOT: {
id: { in: remindersToDelete },
},
},
createMany: {
data: remindersToCreate,
},
update: remindersToUpdate,
};
}
const AvailabilityInput = _AvailabilityModel.pick({
days: true,
startTime: true,
@ -84,6 +127,7 @@ const EventTypeUpdateInput = _EventTypeModel
externalId: true,
}),
users: z.array(stringOrNumber).optional(),
attendeeReminders: z.array(_EventTypeAttendeeReminderModel).optional(),
})
.partial()
.merge(
@ -190,8 +234,17 @@ export const eventTypesRouter = createProtectedRouter()
.mutation("update", {
input: EventTypeUpdateInput.strict(),
async resolve({ ctx, input }) {
const { availability, periodType, locations, destinationCalendar, customInputs, users, id, ...rest } =
input;
const {
availability,
periodType,
locations,
destinationCalendar,
customInputs,
attendeeReminders,
users,
id,
...rest
} = input;
const data: Prisma.EventTypeUpdateInput = rest;
data.locations = locations ?? undefined;
@ -211,6 +264,10 @@ export const eventTypesRouter = createProtectedRouter()
data.customInputs = handleCustomInputs(customInputs, id);
}
if (attendeeReminders) {
data.attendeeReminders = handleAttendeeReminders(attendeeReminders, id);
}
if (users) {
data.users = {
set: [],

View File

@ -0,0 +1,57 @@
-- CreateEnum
CREATE TYPE "EventTypeAttendeeReminderMethod" AS ENUM ('email', 'sms');
-- CreateTable
CREATE TABLE "EventTypeAttendeeReminder" (
"id" SERIAL NOT NULL,
"eventTypeId" INTEGER NOT NULL,
"method" TEXT NOT NULL,
"secondsBeforeEvent" INTEGER NOT NULL,
CONSTRAINT "EventTypeAttendeeReminder_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "EventTypeAttendeeReminder" ADD CONSTRAINT "EventTypeAttendeeReminder_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex
ALTER INDEX "Booking.uid_unique" RENAME TO "Booking_uid_key";
-- RenameIndex
ALTER INDEX "DailyEventReference_bookingId_unique" RENAME TO "DailyEventReference_bookingId_key";
-- RenameIndex
ALTER INDEX "DestinationCalendar.bookingId_unique" RENAME TO "DestinationCalendar_bookingId_key";
-- RenameIndex
ALTER INDEX "DestinationCalendar.eventTypeId_unique" RENAME TO "DestinationCalendar_eventTypeId_key";
-- RenameIndex
ALTER INDEX "DestinationCalendar.userId_unique" RENAME TO "DestinationCalendar_userId_key";
-- RenameIndex
ALTER INDEX "EventType.userId_slug_unique" RENAME TO "EventType_userId_slug_key";
-- RenameIndex
ALTER INDEX "Payment.externalId_unique" RENAME TO "Payment_externalId_key";
-- RenameIndex
ALTER INDEX "Payment.uid_unique" RENAME TO "Payment_uid_key";
-- RenameIndex
ALTER INDEX "Team.slug_unique" RENAME TO "Team_slug_key";
-- RenameIndex
ALTER INDEX "VerificationRequest.identifier_token_unique" RENAME TO "VerificationRequest_identifier_token_key";
-- RenameIndex
ALTER INDEX "VerificationRequest.token_unique" RENAME TO "VerificationRequest_token_key";
-- RenameIndex
ALTER INDEX "Webhook.id_unique" RENAME TO "Webhook_id_key";
-- RenameIndex
ALTER INDEX "users.email_unique" RENAME TO "users_email_key";
-- RenameIndex
ALTER INDEX "users.username_unique" RENAME TO "users_username_key";

View File

@ -3,7 +3,7 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
url = "postgresql://postgres:@localhost:5450/calendso"
}
generator client {
@ -29,20 +29,20 @@ enum PeriodType {
}
model EventType {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
/// @zod.nonempty()
title String
/// @zod.custom(imports.eventTypeSlug)
slug String
description String?
position Int @default(0)
position Int @default(0)
/// @zod.custom(imports.eventTypeLocations)
locations Json?
length Int
hidden Boolean @default(false)
users User[] @relation("user_eventtype")
hidden Boolean @default(false)
users User[] @relation("user_eventtype")
userId Int?
team Team? @relation(fields: [teamId], references: [id])
team Team? @relation(fields: [teamId], references: [id])
teamId Int?
bookings Booking[]
availability Availability[]
@ -51,21 +51,22 @@ model EventType {
eventName String?
customInputs EventTypeCustomInput[]
timeZone String?
periodType PeriodType @default(UNLIMITED)
periodType PeriodType @default(UNLIMITED)
periodStartDate DateTime?
periodEndDate DateTime?
periodDays Int?
periodCountCalendarDays Boolean?
requiresConfirmation Boolean @default(false)
disableGuests Boolean @default(false)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
requiresConfirmation Boolean @default(false)
disableGuests Boolean @default(false)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
schedulingType SchedulingType?
Schedule Schedule[]
price Int @default(0)
currency String @default("usd")
price Int @default(0)
currency String @default("usd")
slotInterval Int?
attendeeReminders EventTypeAttendeeReminder[]
metadata Json?
@@unique([userId, slug])
@ -252,6 +253,8 @@ model Booking {
destinationCalendar DestinationCalendar?
cancellationReason String?
rejectionReason String?
reminderPhone String?
attendeeReminder AttendeeReminder?
}
model Schedule {
@ -303,6 +306,36 @@ model EventTypeCustomInput {
placeholder String @default("")
}
enum EventTypeAttendeeReminderMethod {
EMAIL @map("email")
SMS @map("sms")
}
enum EventTypeAttendeeReminderTimeUnit {
MINUTE @map("minute")
HOUR @map("hour")
DAY @map("day")
}
model EventTypeAttendeeReminder {
id Int @id @default(autoincrement())
eventTypeId Int
eventType EventType @relation(fields: [eventTypeId], references: [id])
method EventTypeAttendeeReminderMethod
timeUnit EventTypeAttendeeReminderTimeUnit
time Int
}
model AttendeeReminder {
id Int @id @default(autoincrement())
bookingUid String @unique
booking Booking @relation(fields: [bookingUid], references: [uid])
method String
referenceId String
scheduledFor DateTime
scheduled Boolean
}
model ResetPasswordRequest {
id String @id @default(cuid())
createdAt DateTime @default(now())

1049
yarn.lock

File diff suppressed because it is too large Load Diff