Compare commits
14 Commits
main
...
docs/contr
Author | SHA1 | Date |
---|---|---|
Joe Au-Yeung | 37148ac335 | |
Joe Au-Yeung | bef07cc833 | |
Joe Au-Yeung | 5ea854dd8b | |
Joe Au-Yeung | 65d6f06cfe | |
Joe Au-Yeung | a0b67bf2e3 | |
Joe Au-Yeung | c3e019394d | |
Joe Au-Yeung | 94acd691bb | |
Joe Au-Yeung | 46d893768d | |
Joe Au-Yeung | 17e025f3c2 | |
Joe Au-Yeung | 965c242659 | |
Joe Au-Yeung | 52b9e1b809 | |
Joe Au-Yeung | ddf9323908 | |
Joe Au-Yeung | 1816139295 | |
Joe Au-Yeung | 74e2e8ba58 |
|
@ -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
|
|
@ -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 && (
|
||||
|
|
|
@ -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;
|
|
@ -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"> {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -26,6 +26,7 @@ export type BookingCreateBody = {
|
|||
metadata: {
|
||||
[key: string]: string;
|
||||
};
|
||||
reminderPhone?: string;
|
||||
};
|
||||
|
||||
export type BookingResponse = Booking & {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -55,6 +55,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
length: true,
|
||||
locations: true,
|
||||
customInputs: true,
|
||||
attendeeReminders: true,
|
||||
periodType: true,
|
||||
periodDays: true,
|
||||
periodStartDate: true,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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";
|
|
@ -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())
|
||||
|
|
Loading…
Reference in New Issue