From 26e458be6d66da3221ec0c19ce3436e97cb5add9 Mon Sep 17 00:00:00 2001 From: Monto <138862352+monto7926@users.noreply.github.com> Date: Wed, 19 Jul 2023 17:30:37 +0300 Subject: [PATCH] feat: dynamic time format rendering in workflows based on user settings (#10229) --- .../bookings/lib/handleCancelBooking.ts | 5 +++ .../features/bookings/lib/handleNewBooking.ts | 4 +- packages/features/ee/payments/api/webhook.ts | 5 +++ .../workflows/api/scheduleEmailReminders.ts | 11 ++++- .../ee/workflows/api/scheduleSMSReminders.ts | 5 ++- .../api/scheduleWhatsappReminders.ts | 2 + .../components/WorkflowDetailsPage.tsx | 7 ++- .../components/WorkflowStepContainer.tsx | 45 +++++++++++++------ .../ee/workflows/lib/actionHelperFunctions.ts | 10 +++-- .../lib/reminders/emailReminderManager.ts | 11 ++++- .../lib/reminders/smsReminderManager.ts | 15 ++++++- .../lib/reminders/templates/customTemplate.ts | 10 +++-- .../templates/emailReminderTemplate.ts | 18 +++++--- .../templates/smsReminderTemplate.ts | 8 +++- .../whatsappEventCancelledTemplate.ts | 11 +++-- .../whatsappEventCompletedTemplate.ts | 11 +++-- .../whatsapp/whatsappEventReminderTemplate.ts | 11 +++-- .../whatsappEventRescheduledTemplate.ts | 11 +++-- .../lib/reminders/whatsappReminderManager.ts | 30 ++++++++++--- .../features/ee/workflows/pages/workflow.tsx | 8 +++- packages/lib/timeFormat.ts | 4 ++ .../viewer/bookings/confirm.handler.ts | 2 + .../workflows/activateEventType.handler.ts | 2 + .../viewer/workflows/create.handler.ts | 11 ++++- .../viewer/workflows/update.handler.ts | 4 ++ 25 files changed, 205 insertions(+), 56 deletions(-) diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 4a3ac35ab9..e81806a57f 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -22,6 +22,7 @@ import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import { handleRefundError } from "@calcom/lib/payment/handleRefundError"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import { BookingStatus, MembershipRole, WorkflowMethods, WebhookTriggerEvents } from "@calcom/prisma/enums"; import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils"; @@ -44,6 +45,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine credentials: true, // Not leaking at the moment, be careful with email: true, timeZone: true, + timeFormat: true, name: true, destinationCalendar: true, }, @@ -154,6 +156,7 @@ async function handler(req: CustomRequest) { name: true, email: true, timeZone: true, + timeFormat: true, locale: true, }, }); @@ -207,6 +210,7 @@ async function handler(req: CustomRequest) { email: organizer.email, name: organizer.name ?? "Nameless", timeZone: organizer.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat), language: { translate: tOrganizer, locale: organizer.locale ?? "en" }, }, attendees: attendeesList, @@ -540,6 +544,7 @@ async function handler(req: CustomRequest) { email: bookingToDelete.user?.email ?? "dev@calendso.com", name: bookingToDelete.user?.name ?? "no user", timeZone: bookingToDelete.user?.timeZone ?? "", + timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat), language: { translate: tOrganizer, locale: organizer.locale ?? "en" }, }, attendees: attendeesList, diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index b07e0f3ad1..f73c5ac747 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -60,7 +60,7 @@ import { getBookerUrl } from "@calcom/lib/server/getBookerUrl"; import { getTranslation } from "@calcom/lib/server/i18n"; import { slugify } from "@calcom/lib/slugify"; import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager"; -import { TimeFormat } from "@calcom/lib/timeFormat"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma, { userSelect } from "@calcom/prisma"; import type { BookingReference } from "@calcom/prisma/client"; import { BookingStatus, SchedulingType, WebhookTriggerEvents, WorkflowMethods } from "@calcom/prisma/enums"; @@ -1027,7 +1027,7 @@ async function handler( username: organizerUser.username || undefined, timeZone: organizerUser.timeZone, language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" }, - timeFormat: organizerUser.timeFormat === 24 ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR, + timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat), }, responses: "calEventResponses" in reqBody ? reqBody.calEventResponses : null, userFieldsResponses: calEventUserFieldsResponses, diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index a7534efe09..5962868854 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -14,6 +14,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import { HttpError as HttpCode } from "@calcom/lib/http-error"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma, bookingMinimalSelect } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; @@ -59,6 +60,7 @@ async function getBooking(bookingId: number) { id: true, username: true, timeZone: true, + timeFormat: true, email: true, name: true, locale: true, @@ -108,6 +110,7 @@ async function getBooking(bookingId: number) { email: user.email, name: user.name!, timeZone: user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(user.timeFormat), language: { translate: t, locale: user.locale ?? "en" }, id: user.id, }, @@ -163,6 +166,7 @@ async function handlePaymentSuccess(event: Stripe.Event) { username: true, credentials: true, timeZone: true, + timeFormat: true, email: true, name: true, locale: true, @@ -216,6 +220,7 @@ async function handlePaymentSuccess(event: Stripe.Event) { email: user.email, name: user.name!, timeZone: user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(user.timeFormat), language: { translate: t, locale: user.locale ?? "en" }, }, attendees: attendeesList, diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 2fbfbd654b..228141e549 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -6,6 +6,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { defaultHandler } from "@calcom/lib/server"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -196,6 +197,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { reminder.workflowStep.emailSubject || "", variables, emailLocale, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), !!reminder.booking.user?.hideBranding ).text; emailContent.emailSubject = emailSubject; @@ -203,15 +205,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { reminder.workflowStep.reminderBody || "", variables, emailLocale, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), !!reminder.booking.user?.hideBranding ).html; emailBodyEmpty = - customTemplate(reminder.workflowStep.reminderBody || "", variables, emailLocale).text.length === 0; + customTemplate( + reminder.workflowStep.reminderBody || "", + variables, + emailLocale, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat) + ).text.length === 0; } else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) { emailContent = emailReminderTemplate( false, reminder.workflowStep.action, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), reminder.booking.startTime.toISOString() || "", reminder.booking.endTime.toISOString() || "", reminder.booking.eventType?.title || "", diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index 1cca96fa19..fe88358c64 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -4,6 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { defaultHandler } from "@calcom/lib/server"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -116,13 +117,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const customMessage = customTemplate( reminder.workflowStep.reminderBody || "", variables, - locale || "en" + locale || "en", + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat) ); message = customMessage.text; } else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) { message = smsReminderTemplate( false, reminder.workflowStep.action, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), reminder.booking?.startTime.toISOString() || "", reminder.booking?.eventType?.title || "", timeZone || "", diff --git a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts index 60ead69a1d..276163a305 100644 --- a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts +++ b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts @@ -4,6 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import dayjs from "@calcom/dayjs"; import { defaultHandler } from "@calcom/lib/server"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import { getWhatsappTemplateFunction } from "../lib/actionHelperFunctions"; @@ -81,6 +82,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const message = templateFunction( false, reminder.workflowStep.action, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), reminder.booking?.startTime.toISOString() || "", reminder.booking?.eventType?.title || "", timeZone || "", diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index f986732b77..f475526844 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -9,6 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { WorkflowTemplates } from "@calcom/prisma/enums"; import type { WorkflowActions } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; +import type { RouterOutputs } from "@calcom/trpc/react"; import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui"; import { Button, Label, MultiSelectCheckboxes, TextField } from "@calcom/ui"; import { ArrowDown, Trash2 } from "@calcom/ui/components/icon"; @@ -19,12 +20,15 @@ import { AddActionDialog } from "./AddActionDialog"; import { DeleteDialog } from "./DeleteDialog"; import WorkflowStepContainer from "./WorkflowStepContainer"; +type User = RouterOutputs["viewer"]["me"]; + interface Props { form: UseFormReturn; workflowId: number; selectedEventTypes: Option[]; setSelectedEventTypes: Dispatch>; teamId?: number; + user: User; isMixedEventType: boolean; readOnly: boolean; } @@ -158,7 +162,7 @@ export default function WorkflowDetailsPage(props: Props) {
{form.getValues("trigger") && (
- +
)} {form.getValues("steps") && ( @@ -168,6 +172,7 @@ export default function WorkflowDetailsPage(props: Props) { ; + user: User; reload?: boolean; setReload?: Dispatch>; teamId?: number; @@ -72,6 +77,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { { teamId }, { enabled: !!teamId } ); + + const timeFormat = getTimeFormatStringFromUserTimeFormat(props.user.timeFormat); + const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber) || []; const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false); @@ -118,23 +126,30 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) { const action = form.getValues(`steps.${step.stepNumber - 1}.action`); if (isSMSAction(action)) { - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, smsReminderTemplate(true, action)); + form.setValue( + `steps.${step.stepNumber - 1}.reminderBody`, + smsReminderTemplate(true, action, timeFormat) + ); } else if (isWhatsappAction(action)) { - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, whatsappReminderTemplate(true, action)); + form.setValue( + `steps.${step.stepNumber - 1}.reminderBody`, + whatsappReminderTemplate(true, action, timeFormat) + ); } else { - const reminderBodyTemplate = emailReminderTemplate(true, action).emailBody; + const reminderBodyTemplate = emailReminderTemplate(true, action, timeFormat).emailBody; form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, reminderBodyTemplate); } } if (!form.getValues(`steps.${step.stepNumber - 1}.emailSubject`)) { const subjectTemplate = emailReminderTemplate( true, - form.getValues(`steps.${step.stepNumber - 1}.action`) + form.getValues(`steps.${step.stepNumber - 1}.action`), + timeFormat ).emailSubject; form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate); } } else if (step && isWhatsappAction(step.action)) { - const templateBody = getWhatsappTemplateForAction(step.action, step.template); + const templateBody = getWhatsappTemplateForAction(step.action, step.template, timeFormat); form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, templateBody); } @@ -463,15 +478,19 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { if (isSMSAction(val.value)) { form.setValue( `steps.${step.stepNumber - 1}.reminderBody`, - smsReminderTemplate(true, val.value) + smsReminderTemplate(true, val.value, timeFormat) ); } else if (isWhatsappAction(val.value)) { form.setValue( `steps.${step.stepNumber - 1}.reminderBody`, - whatsappReminderTemplate(true, val.value) + whatsappReminderTemplate(true, val.value, timeFormat) ); } else { - const emailReminderBody = emailReminderTemplate(true, val.value); + const emailReminderBody = emailReminderTemplate( + true, + val.value, + timeFormat + ); form.setValue( `steps.${step.stepNumber - 1}.reminderBody`, emailReminderBody.emailBody @@ -680,28 +699,28 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { if (isWhatsappAction(action)) { form.setValue( `steps.${step.stepNumber - 1}.reminderBody`, - whatsappReminderTemplate(true, action) + whatsappReminderTemplate(true, action, timeFormat) ); } else if (isSMSAction(action)) { form.setValue( `steps.${step.stepNumber - 1}.reminderBody`, - smsReminderTemplate(true, action) + smsReminderTemplate(true, action, timeFormat) ); } else { form.setValue( `steps.${step.stepNumber - 1}.reminderBody`, - emailReminderTemplate(true, action).emailBody + emailReminderTemplate(true, action, timeFormat).emailBody ); form.setValue( `steps.${step.stepNumber - 1}.emailSubject`, - emailReminderTemplate(true, action).emailSubject + emailReminderTemplate(true, action, timeFormat).emailSubject ); } } else { if (isWhatsappAction(action)) { form.setValue( `steps.${step.stepNumber - 1}.reminderBody`, - getWhatsappTemplateForAction(action, val.value) + getWhatsappTemplateForAction(action, val.value, timeFormat) ); } else { form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); diff --git a/packages/features/ee/workflows/lib/actionHelperFunctions.ts b/packages/features/ee/workflows/lib/actionHelperFunctions.ts index 965a0f8c52..52b072cc4c 100644 --- a/packages/features/ee/workflows/lib/actionHelperFunctions.ts +++ b/packages/features/ee/workflows/lib/actionHelperFunctions.ts @@ -1,4 +1,7 @@ -import { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@prisma/client"; +import type { WorkflowTriggerEvents } from "@prisma/client"; +import { WorkflowActions, WorkflowTemplates } from "@prisma/client"; + +import type { TimeFormat } from "@calcom/lib/timeFormat"; import { whatsappEventCancelledTemplate, @@ -61,8 +64,9 @@ export function getWhatsappTemplateFunction(template: WorkflowTemplates): typeof export function getWhatsappTemplateForAction( action: WorkflowActions, - template: WorkflowTemplates + template: WorkflowTemplates, + timeFormat: TimeFormat ): string | null { const templateFunction = getWhatsappTemplateFunction(template); - return templateFunction(true, action); + return templateFunction(true, action, timeFormat); } diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 90cff2a1a8..e20f8a719c 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -104,13 +104,20 @@ export const scheduleEmailReminder = async ( ? evt.attendees[0].language?.locale : evt.organizer.language.locale; - const emailSubjectTemplate = customTemplate(emailSubject, variables, locale); + const emailSubjectTemplate = customTemplate(emailSubject, variables, locale, evt.organizer.timeFormat); emailContent.emailSubject = emailSubjectTemplate.text; - emailContent.emailBody = customTemplate(emailBody, variables, locale, hideBranding).html; + emailContent.emailBody = customTemplate( + emailBody, + variables, + locale, + evt.organizer.timeFormat, + hideBranding + ).html; } else if (template === WorkflowTemplates.REMINDER) { emailContent = emailReminderTemplate( false, action, + evt.organizer.timeFormat, startTime, endTime, evt.title, diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 45c9792a25..b9d8aa3ada 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -1,5 +1,6 @@ import dayjs from "@calcom/dayjs"; import logger from "@calcom/lib/logger"; +import type { TimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import type { TimeUnit } from "@calcom/prisma/enums"; @@ -29,6 +30,7 @@ export type BookingInfo = { name: string; email: string; timeZone: string; + timeFormat?: TimeFormat; username?: string; }; eventType: { @@ -115,11 +117,20 @@ export const scheduleSMSReminder = async ( cancelLink: `/booking/${evt.uid}?cancel=true`, rescheduleLink: `/${evt.organizer.username}/${evt.eventType.slug}?rescheduleUid=${evt.uid}`, }; - const customMessage = customTemplate(message, variables, locale); + const customMessage = customTemplate(message, variables, locale, evt.organizer.timeFormat); message = customMessage.text; } else if (template === WorkflowTemplates.REMINDER) { message = - smsReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message; + smsReminderTemplate( + false, + action, + evt.organizer.timeFormat, + evt.startTime, + evt.title, + timeZone, + attendeeName, + name + ) || message; } // Allows debugging generated email content without waiting for sendgrid to send emails diff --git a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts index 72e13ef513..43e388a619 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts @@ -2,6 +2,7 @@ import { guessEventLocationType } from "@calcom/app-store/locations"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; +import { TimeFormat } from "@calcom/lib/timeFormat"; import type { CalEventResponses } from "@calcom/types/Calendar"; export type VariablesType = { @@ -24,6 +25,7 @@ const customTemplate = ( text: string, variables: VariablesType, locale: string, + timeFormat?: TimeFormat, isBrandingDisabled?: boolean ) => { const translatedDate = new Intl.DateTimeFormat(locale, { @@ -42,6 +44,8 @@ const customTemplate = ( const cancelLink = variables.cancelLink ? `${WEBAPP_URL}${variables.cancelLink}` : ""; const rescheduleLink = variables.rescheduleLink ? `${WEBAPP_URL}${variables.rescheduleLink}` : ""; + const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; + let dynamicText = text .replaceAll("{EVENT_NAME}", variables.eventName || "") .replaceAll("{ORGANIZER}", variables.organizerName || "") @@ -49,9 +53,9 @@ const customTemplate = ( .replaceAll("{ORGANIZER_NAME}", variables.organizerName || "") //old variable names .replaceAll("{ATTENDEE_NAME}", variables.attendeeName || "") //old variable names .replaceAll("{EVENT_DATE}", translatedDate) - .replaceAll("{EVENT_TIME}", variables.eventDate?.format("H:mmA") || "") - .replaceAll("{START_TIME}", variables.eventDate?.format("H:mmA") || "") - .replaceAll("{EVENT_END_TIME}", variables.eventEndTime?.format("H:mmA") || "") + .replaceAll("{EVENT_TIME}", variables.eventDate?.format(currentTimeFormat) || "") + .replaceAll("{START_TIME}", variables.eventDate?.format(currentTimeFormat) || "") + .replaceAll("{EVENT_END_TIME}", variables.eventEndTime?.format(currentTimeFormat) || "") .replaceAll("{LOCATION}", locationString) .replaceAll("{ADDITIONAL_NOTES}", variables.additionalNotes || "") .replaceAll("{ATTENDEE_EMAIL}", variables.attendeeEmail || "") diff --git a/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts index a7774bb49e..ec8a676228 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts @@ -1,10 +1,12 @@ import dayjs from "@calcom/dayjs"; import { APP_NAME } from "@calcom/lib/constants"; +import { TimeFormat } from "@calcom/lib/timeFormat"; import { WorkflowActions } from "@calcom/prisma/enums"; const emailReminderTemplate = ( isEditingMode: boolean, action?: WorkflowActions, + timeFormat?: TimeFormat, startTime?: string, endTime?: string, eventName?: string, @@ -13,6 +15,9 @@ const emailReminderTemplate = ( name?: string, isBrandingDisabled?: boolean ) => { + const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; + const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`; + let eventDate = ""; if (isEditingMode) { @@ -21,23 +26,24 @@ const emailReminderTemplate = ( timeZone = "{TIMEZONE}"; otherPerson = action === WorkflowActions.EMAIL_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}"; name = action === WorkflowActions.EMAIL_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}"; - eventDate = "{EVENT_DATE_ddd, MMM D, YYYY H:mmA}"; + eventDate = `{EVENT_DATE_${dateTimeFormat}}`; } else { - eventDate = dayjs(startTime).tz(timeZone).format("ddd, MMM D, YYYY H:mmA"); + eventDate = dayjs(startTime).tz(timeZone).format(dateTimeFormat); - endTime = dayjs(endTime).tz(timeZone).format("H:mmA"); + endTime = dayjs(endTime).tz(timeZone).format(currentTimeFormat); } + const emailSubject = `Reminder: ${eventName} - ${eventDate}`; const introHtml = `Hi${ name ? " " + name : "" },

This is a reminder about your upcoming event.

`; - const eventHtml = `
Event:
${eventName}

`; + const eventHtml = `
Event:
${eventName}

`; - const dateTimeHtml = `
Date & Time:
${eventDate} - ${endTime} (${timeZone})

`; + const dateTimeHtml = `
Date & Time:
${eventDate} - ${endTime} (${timeZone})

`; - const attendeeHtml = `
Attendees:
You & ${otherPerson}

`; + const attendeeHtml = `
Attendees:
You & ${otherPerson}

`; const branding = !isBrandingDisabled && !isEditingMode ? `

_

Scheduling by ${APP_NAME}` : ""; diff --git a/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts index da11b5583d..c2974d2949 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts @@ -1,27 +1,31 @@ import dayjs from "@calcom/dayjs"; +import { TimeFormat } from "@calcom/lib/timeFormat"; import { WorkflowActions } from "@calcom/prisma/enums"; const smsReminderTemplate = ( isEditingMode: boolean, action?: WorkflowActions, + timeFormat?: TimeFormat, startTime?: string, eventName?: string, timeZone?: string, attendee?: string, name?: string ) => { + const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; + let eventDate; if (isEditingMode) { eventName = "{EVENT_NAME}"; timeZone = "{TIMEZONE}"; - startTime = "{EVENT_TIME_h:mmA}"; + startTime = `{EVENT_TIME_${currentTimeFormat}}`; eventDate = "{EVENT_DATE_YYYY MMM D}"; attendee = action === WorkflowActions.SMS_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}"; name = action === WorkflowActions.SMS_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}"; } else { eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D"); - startTime = dayjs(startTime).tz(timeZone).format("h:mmA"); + startTime = dayjs(startTime).tz(timeZone).format(currentTimeFormat); } const templateOne = `Hi${ diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts index 291874d478..8a34c56196 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts @@ -1,28 +1,33 @@ import { WorkflowActions } from "@prisma/client"; import dayjs from "@calcom/dayjs"; +import { TimeFormat } from "@calcom/lib/timeFormat"; export const whatsappEventCancelledTemplate = ( isEditingMode: boolean, action?: WorkflowActions, + timeFormat?: TimeFormat, startTime?: string, eventName?: string, timeZone?: string, attendee?: string, name?: string ) => { + const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; + const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`; + let eventDate; if (isEditingMode) { eventName = "{EVENT_NAME}"; timeZone = "{TIMEZONE}"; - startTime = "{START_TIME_h:mmA}"; + startTime = `{START_TIME_${currentTimeFormat}}`; - eventDate = "{EVENT_DATE_YYYY MMM D}"; + eventDate = `{EVENT_DATE_${dateTimeFormat}}`; attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}"; name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}"; } else { eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D"); - startTime = dayjs(startTime).tz(timeZone).format("h:mmA"); + startTime = dayjs(startTime).tz(timeZone).format(currentTimeFormat); } const templateOne = `Hi${ diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts index 4dc23ad93d..a1cdf807ae 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts @@ -1,28 +1,33 @@ import { WorkflowActions } from "@prisma/client"; import dayjs from "@calcom/dayjs"; +import { TimeFormat } from "@calcom/lib/timeFormat"; export const whatsappEventCompletedTemplate = ( isEditingMode: boolean, action?: WorkflowActions, + timeFormat?: TimeFormat, startTime?: string, eventName?: string, timeZone?: string, attendee?: string, name?: string ) => { + const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; + const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`; + let eventDate; if (isEditingMode) { eventName = "{EVENT_NAME}"; timeZone = "{TIMEZONE}"; - startTime = "{START_TIME_h:mmA}"; + startTime = `{START_TIME_${currentTimeFormat}}`; - eventDate = "{EVENT_DATE_YYYY MMM D}"; + eventDate = `{EVENT_DATE_${dateTimeFormat}}`; attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}"; name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}"; } else { eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D"); - startTime = dayjs(startTime).tz(timeZone).format("h:mmA"); + startTime = dayjs(startTime).tz(timeZone).format(currentTimeFormat); } const templateOne = `Hi${ diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts index afa548dfec..f5f1b22411 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts @@ -1,28 +1,33 @@ import { WorkflowActions } from "@prisma/client"; import dayjs from "@calcom/dayjs"; +import { TimeFormat } from "@calcom/lib/timeFormat"; export const whatsappReminderTemplate = ( isEditingMode: boolean, action?: WorkflowActions, + timeFormat?: TimeFormat, startTime?: string, eventName?: string, timeZone?: string, attendee?: string, name?: string ) => { + const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; + const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`; + let eventDate; if (isEditingMode) { eventName = "{EVENT_NAME}"; timeZone = "{TIMEZONE}"; - startTime = "{START_TIME_h:mmA}"; + startTime = `{START_TIME_${currentTimeFormat}}`; - eventDate = "{EVENT_DATE_YYYY MMM D}"; + eventDate = `{EVENT_DATE_${dateTimeFormat}}`; attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}"; name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}"; } else { eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D"); - startTime = dayjs(startTime).tz(timeZone).format("h:mmA"); + startTime = dayjs(startTime).tz(timeZone).format(currentTimeFormat); } const templateOne = `Hi${ diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts index 813560c9a8..1e7ff1ce75 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts @@ -1,28 +1,33 @@ import { WorkflowActions } from "@prisma/client"; import dayjs from "@calcom/dayjs"; +import { TimeFormat } from "@calcom/lib/timeFormat"; export const whatsappEventRescheduledTemplate = ( isEditingMode: boolean, action?: WorkflowActions, + timeFormat?: TimeFormat, startTime?: string, eventName?: string, timeZone?: string, attendee?: string, name?: string ) => { + const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; + const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`; + let eventDate; if (isEditingMode) { eventName = "{EVENT_NAME}"; timeZone = "{TIMEZONE}"; - startTime = "{START_TIME_h:mmA}"; + startTime = `{START_TIME_${currentTimeFormat}}`; - eventDate = "{EVENT_DATE_YYYY MMM D}"; + eventDate = `{EVENT_DATE_${dateTimeFormat}}`; attendee = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ORGANIZER}" : "{ATTENDEE}"; name = action === WorkflowActions.WHATSAPP_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}"; } else { eventDate = dayjs(startTime).tz(timeZone).format("YYYY MMM D"); - startTime = dayjs(startTime).tz(timeZone).format("h:mmA"); + startTime = dayjs(startTime).tz(timeZone).format(currentTimeFormat); } const templateOne = `Hi${ diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts index 77dc2ce07a..ac88303932 100644 --- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts @@ -6,7 +6,8 @@ import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import * as twilio from "./smsProviders/twilioProvider"; -import { BookingInfo, deleteScheduledSMSReminder, timeUnitLowerCase } from "./smsReminderManager"; +import type { BookingInfo, timeUnitLowerCase } from "./smsReminderManager"; +import { deleteScheduledSMSReminder } from "./smsReminderManager"; import { whatsappEventCancelledTemplate, whatsappEventCompletedTemplate, @@ -68,14 +69,23 @@ export const scheduleWhatsappReminder = async ( switch (template) { case WorkflowTemplates.REMINDER: message = - whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || - message; + whatsappReminderTemplate( + false, + action, + evt.organizer.timeFormat, + evt.startTime, + evt.title, + timeZone, + attendeeName, + name + ) || message; break; case WorkflowTemplates.CANCELLED: message = whatsappEventCancelledTemplate( false, action, + evt.organizer.timeFormat, evt.startTime, evt.title, timeZone, @@ -88,6 +98,7 @@ export const scheduleWhatsappReminder = async ( whatsappEventRescheduledTemplate( false, action, + evt.organizer.timeFormat, evt.startTime, evt.title, timeZone, @@ -100,6 +111,7 @@ export const scheduleWhatsappReminder = async ( whatsappEventCompletedTemplate( false, action, + evt.organizer.timeFormat, evt.startTime, evt.title, timeZone, @@ -109,8 +121,16 @@ export const scheduleWhatsappReminder = async ( break; default: message = - whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || - message; + whatsappReminderTemplate( + false, + action, + evt.organizer.timeFormat, + evt.startTime, + evt.title, + timeZone, + attendeeName, + name + ) || message; } // Allows debugging generated whatsapp content without waiting for twilio to send whatsapp messages diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx index 836f676770..37b2782592 100644 --- a/packages/features/ee/workflows/pages/workflow.tsx +++ b/packages/features/ee/workflows/pages/workflow.tsx @@ -19,6 +19,8 @@ import { trpc } from "@calcom/trpc/react"; import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui"; import { Alert, Button, Form, showToast, Badge } from "@calcom/ui"; +import useMeQuery from "@lib/hooks/useMeQuery"; + import LicenseRequired from "../../common/components/LicenseRequired"; import SkeletonLoader from "../components/SkeletonLoaderEdit"; import WorkflowDetailsPage from "../components/WorkflowDetailsPage"; @@ -93,6 +95,9 @@ function WorkflowPage() { const { workflow: workflowId } = router.isReady ? querySchema.parse(router.query) : { workflow: -1 }; const utils = trpc.useContext(); + const userQuery = useMeQuery(); + const user = userQuery.data; + const { data: workflow, isError, @@ -285,11 +290,12 @@ function WorkflowPage() { {!isError ? ( <> - {isAllDataLoaded ? ( + {isAllDataLoaded && user ? ( <> { return is24hFromLocalstorage === "true"; }; +export const getTimeFormatStringFromUserTimeFormat = (timeFormat: number | null | undefined) => { + return timeFormat === 24 ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR; +}; + /** * Retrieves the browsers time format preference, checking local storage first * for a user set preference. If no preference is found, it will use the browser diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index 1ce4f63571..1f571717f0 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -8,6 +8,7 @@ import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhoo import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { getTranslation } from "@calcom/lib/server"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { BookingStatus, MembershipRole, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -162,6 +163,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { name: user.name || "Unnamed", username: user.username || undefined, timeZone: user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(user.timeFormat), language: { translate: tOrganizer, locale: user.locale ?? "en" }, }, attendees: attendeesList, diff --git a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts index bc854e889c..a8f8c0e861 100644 --- a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts @@ -11,6 +11,7 @@ import { scheduleWhatsappReminder, } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager"; import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/client"; import { MembershipRole, WorkflowActions, WorkflowMethods } from "@calcom/prisma/enums"; @@ -171,6 +172,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType name: booking.user.name || "", email: booking.user.email, timeZone: booking.user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), language: { locale: booking.user.locale || defaultLocale }, } : { name: "", email: "", timeZone: "", language: { locale: "" } }, diff --git a/packages/trpc/server/routers/viewer/workflows/create.handler.ts b/packages/trpc/server/routers/viewer/workflows/create.handler.ts index 75c71acec1..6159389ffc 100644 --- a/packages/trpc/server/routers/viewer/workflows/create.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/create.handler.ts @@ -2,6 +2,7 @@ import type { Workflow } from "@prisma/client"; import emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate"; import { SENDER_NAME } from "@calcom/lib/constants"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import type { PrismaClient } from "@calcom/prisma/client"; import { @@ -65,13 +66,19 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { }, }); + const renderedEmailTemplate = emailReminderTemplate( + true, + WorkflowActions.EMAIL_ATTENDEE, + getTimeFormatStringFromUserTimeFormat(ctx.user.timeFormat) + ); + await ctx.prisma.workflowStep.create({ data: { stepNumber: 1, action: WorkflowActions.EMAIL_ATTENDEE, template: WorkflowTemplates.REMINDER, - reminderBody: emailReminderTemplate(true, WorkflowActions.EMAIL_ATTENDEE).emailBody, - emailSubject: emailReminderTemplate(true, WorkflowActions.EMAIL_ATTENDEE).emailSubject, + reminderBody: renderedEmailTemplate.emailBody, + emailSubject: renderedEmailTemplate.emailSubject, workflowId: workflow.id, sender: SENDER_NAME, numberVerificationPending: false, diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index 5643f2fbe6..af4f0bb5d2 100644 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -15,6 +15,7 @@ import { } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager"; import { IS_SELF_HOSTED, SENDER_ID, SENDER_NAME } from "@calcom/lib/constants"; import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import type { PrismaClient } from "@calcom/prisma/client"; import { BookingStatus, WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -280,6 +281,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { name: booking.user.name || "", email: booking.user.email, timeZone: booking.user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), @@ -503,6 +505,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { name: booking.user.name || "", email: booking.user.email, timeZone: booking.user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), @@ -649,6 +652,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { name: booking.user.name || "", email: booking.user.email, timeZone: booking.user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), language: { locale: booking.user.locale || defaultLocale }, } : { name: "", email: "", timeZone: "", language: { locale: "" } },