feat: added Rating Template to Workflows (#14215)
Co-authored-by: alishaz-polymath <alishahbaz7@gmail.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
co-authored by
alishaz-polymath
Syed Ali Shahbaz
parent
844076badb
commit
9bd3a362a2
File diff suppressed because it is too large
Load Diff
@@ -1247,6 +1247,7 @@
|
||||
"reminder": "Reminder",
|
||||
"rescheduled": "Rescheduled",
|
||||
"completed": "Completed",
|
||||
"rating": "Rating",
|
||||
"reminder_email": "Reminder: {{eventType}} with {{name}} at {{date}}",
|
||||
"not_triggering_existing_bookings": "Won't trigger for already existing bookings as user will be asked for phone number when booking the event.",
|
||||
"minute_one": "{{count}} minute",
|
||||
@@ -2364,8 +2365,15 @@
|
||||
"lock_org_users_eventtypes_description": "Prevent members from creating their own event types.",
|
||||
"cookie_consent_checkbox": "I consent to our privacy policy and cookie usage",
|
||||
"make_a_call": "Make a Call",
|
||||
"submit_feedback": "Submit Feedback",
|
||||
"host_no_show": "Your host didn't show up",
|
||||
"no_show_description": "You can reschedule another meeting with them",
|
||||
"how_can_we_improve": "How can we improve our service?",
|
||||
"most_liked": "What did you like the most?",
|
||||
"review": "Review",
|
||||
"reviewed": "Reviewed",
|
||||
"unreviewed": "Unreviewed",
|
||||
"rating_url_info":"The URL for Rating Feedback Form",
|
||||
"no_show_url_info":"The URL for No Show Feedback",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
--cal-border-booker: #e5e7eb;
|
||||
--cal-border-muted: hsla(220, 14%, 96%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
--cal-border-focus: hsla(0, 0%, 10%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: hsla(217, 19%, 27%, 1);
|
||||
@@ -71,6 +72,7 @@
|
||||
--cal-border-booker: hsla(0, 0%, 22%, 1);
|
||||
--cal-border-muted: hsla(0, 0%, 18%, 1);
|
||||
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||
--cal-border-focus: hsla(0, 0%, 100%, 1);
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: hsla(240, 20%, 99%, 1);
|
||||
|
||||
@@ -66,6 +66,7 @@ module.exports = {
|
||||
muted: "var(--cal-border-muted, #F3F4F6)",
|
||||
booker: `var(--cal-border-booker, ${subtleColor})`,
|
||||
error: "var(--cal-border-error, #AA2E26)",
|
||||
focus: "var(--cal-border-focus, #1A1A1A)",
|
||||
},
|
||||
textColor: {
|
||||
emphasis: "var(--cal-text-emphasis, #111827)",
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "../lib/reminders/providers/sendgridProvider";
|
||||
import type { VariablesType } from "../lib/reminders/templates/customTemplate";
|
||||
import customTemplate from "../lib/reminders/templates/customTemplate";
|
||||
import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate";
|
||||
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -188,6 +189,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
|
||||
cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true`,
|
||||
rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}`,
|
||||
ratingUrl: `${bookerUrl}/booking/${reminder.booking.uid}?rating`,
|
||||
noShowUrl: `${bookerUrl}/booking/${reminder.booking.uid}?noShow=true`,
|
||||
};
|
||||
const emailLocale = locale || "en";
|
||||
const emailSubject = customTemplate(
|
||||
@@ -226,6 +229,30 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
name || "",
|
||||
!!reminder.booking.user?.hideBranding
|
||||
);
|
||||
} else if (reminder.workflowStep.template === WorkflowTemplates.RATING) {
|
||||
const organizerOrganizationProfile = await prisma.profile.findFirst({
|
||||
where: {
|
||||
userId: reminder.booking.user?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const organizerOrganizationId = organizerOrganizationProfile?.organizationId;
|
||||
const bookerUrl = await getBookerBaseUrl(
|
||||
reminder.booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null
|
||||
);
|
||||
emailContent = emailRatingTemplate({
|
||||
isEditingMode: true,
|
||||
action: reminder.workflowStep.action || WorkflowActions.EMAIL_ADDRESS,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
||||
startTime: reminder.booking.startTime.toISOString() || "",
|
||||
endTime: reminder.booking.endTime.toISOString() || "",
|
||||
eventName: reminder.booking.eventType?.title || "",
|
||||
timeZone: timeZone || "",
|
||||
organizer: reminder.booking.user?.name || "",
|
||||
name: name || "",
|
||||
ratingUrl: `${bookerUrl}/booking/${reminder.booking.uid}?rating` || "",
|
||||
noShowUrl: `${bookerUrl}/booking/${reminder.booking.uid}?noShow=true` || "",
|
||||
});
|
||||
}
|
||||
|
||||
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from "../lib/actionHelperFunctions";
|
||||
import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants";
|
||||
import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions";
|
||||
import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate";
|
||||
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
|
||||
import smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate";
|
||||
import { whatsappReminderTemplate } from "../lib/reminders/templates/whatsapp";
|
||||
@@ -739,6 +740,15 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
emailReminderTemplate(true, action, timeFormat).emailSubject
|
||||
);
|
||||
}
|
||||
} else if (val.value === WorkflowTemplates.RATING) {
|
||||
form.setValue(
|
||||
`steps.${step.stepNumber - 1}.reminderBody`,
|
||||
emailRatingTemplate({ isEditingMode: true, action, timeFormat }).emailBody
|
||||
);
|
||||
form.setValue(
|
||||
`steps.${step.stepNumber - 1}.emailSubject`,
|
||||
emailRatingTemplate({ isEditingMode: true, action, timeFormat }).emailSubject
|
||||
);
|
||||
} else {
|
||||
if (isWhatsappAction(action)) {
|
||||
form.setValue(
|
||||
|
||||
@@ -38,6 +38,10 @@ export function isAttendeeAction(action: WorkflowActions) {
|
||||
);
|
||||
}
|
||||
|
||||
export function isEmailToAttendeeAction(action: WorkflowActions) {
|
||||
return action === WorkflowActions.EMAIL_ATTENDEE;
|
||||
}
|
||||
|
||||
export function isTextMessageToAttendeeAction(action?: WorkflowActions) {
|
||||
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export const TIME_UNIT = [TimeUnit.DAY, TimeUnit.HOUR, TimeUnit.MINUTE] as const
|
||||
export const WORKFLOW_TEMPLATES = [
|
||||
WorkflowTemplates.CUSTOM,
|
||||
WorkflowTemplates.REMINDER,
|
||||
WorkflowTemplates.RATING,
|
||||
WorkflowTemplates.CANCELLED,
|
||||
WorkflowTemplates.COMPLETED,
|
||||
WorkflowTemplates.RESCHEDULED,
|
||||
@@ -30,6 +31,12 @@ export const WORKFLOW_TEMPLATES = [
|
||||
|
||||
export const BASIC_WORKFLOW_TEMPLATES = [WorkflowTemplates.CUSTOM, WorkflowTemplates.REMINDER] as const;
|
||||
|
||||
export const ATTENDEE_WORKFLOW_TEMPLATES = [
|
||||
WorkflowTemplates.CUSTOM,
|
||||
WorkflowTemplates.REMINDER,
|
||||
WorkflowTemplates.RATING,
|
||||
] as const;
|
||||
|
||||
export const WHATSAPP_WORKFLOW_TEMPLATES = [
|
||||
WorkflowTemplates.REMINDER,
|
||||
WorkflowTemplates.COMPLETED,
|
||||
@@ -53,6 +60,8 @@ export const DYNAMIC_TEXT_VARIABLES = [
|
||||
"meeting_url",
|
||||
"cancel_url",
|
||||
"reschedule_url",
|
||||
"rating_url",
|
||||
"no_show_url",
|
||||
];
|
||||
|
||||
export const FORMATTED_DYNAMIC_TEXT_VARIABLES = ["event_date_", "event_time_", "event_end_time_"];
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isTextMessageToAttendeeAction,
|
||||
isSMSOrWhatsappAction,
|
||||
isWhatsappAction,
|
||||
isEmailToAttendeeAction,
|
||||
} from "./actionHelperFunctions";
|
||||
import {
|
||||
TIME_UNIT,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
WORKFLOW_ACTIONS,
|
||||
BASIC_WORKFLOW_TEMPLATES,
|
||||
WORKFLOW_TRIGGER_EVENTS,
|
||||
ATTENDEE_WORKFLOW_TEMPLATES,
|
||||
} from "./constants";
|
||||
|
||||
export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean, isOrgsPlan?: boolean) {
|
||||
@@ -46,7 +48,11 @@ export function getWorkflowTimeUnitOptions(t: TFunction) {
|
||||
|
||||
export function getWorkflowTemplateOptions(t: TFunction, action: WorkflowActions | undefined) {
|
||||
const TEMPLATES =
|
||||
action && isWhatsappAction(action) ? WHATSAPP_WORKFLOW_TEMPLATES : BASIC_WORKFLOW_TEMPLATES;
|
||||
action && isWhatsappAction(action)
|
||||
? WHATSAPP_WORKFLOW_TEMPLATES
|
||||
: action && isEmailToAttendeeAction(action)
|
||||
? ATTENDEE_WORKFLOW_TEMPLATES
|
||||
: BASIC_WORKFLOW_TEMPLATES;
|
||||
return TEMPLATES.map((template) => {
|
||||
return { label: t(`${template.toLowerCase()}`), value: template };
|
||||
}) as { label: string; value: any }[];
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getBatchId, sendSendgridMail } from "./providers/sendgridProvider";
|
||||
import type { AttendeeInBookingInfo, BookingInfo, timeUnitLowerCase } from "./smsReminderManager";
|
||||
import type { VariablesType } from "./templates/customTemplate";
|
||||
import customTemplate from "./templates/customTemplate";
|
||||
import emailRatingTemplate from "./templates/emailRatingTemplate";
|
||||
import emailReminderTemplate from "./templates/emailReminderTemplate";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[emailReminderManager]"] });
|
||||
@@ -200,6 +201,8 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
||||
meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl,
|
||||
cancelLink: `${evt.bookerUrl}/booking/${evt.uid}?cancel=true`,
|
||||
rescheduleLink: `${evt.bookerUrl}/reschedule/${evt.uid}`,
|
||||
ratingUrl: `${evt.bookerUrl}/booking/${evt.uid}?rating`,
|
||||
noShowUrl: `${evt.bookerUrl}/booking/${evt.uid}?noShow=true`,
|
||||
};
|
||||
|
||||
const locale =
|
||||
@@ -228,6 +231,20 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
||||
attendeeName,
|
||||
name
|
||||
);
|
||||
} else if (template === WorkflowTemplates.RATING) {
|
||||
emailContent = emailRatingTemplate({
|
||||
isEditingMode: true,
|
||||
action,
|
||||
timeFormat: evt.organizer.timeFormat,
|
||||
startTime,
|
||||
endTime,
|
||||
eventName: evt.title,
|
||||
timeZone,
|
||||
organizer: evt.organizer.name,
|
||||
name,
|
||||
ratingUrl: `${evt.bookerUrl}/booking/${evt.uid}?rating`,
|
||||
noShowUrl: `${evt.bookerUrl}/booking/${evt.uid}?noShow=true`,
|
||||
});
|
||||
}
|
||||
|
||||
// Allows debugging generated email content without waiting for sendgrid to send emails
|
||||
|
||||
@@ -21,6 +21,8 @@ export type VariablesType = {
|
||||
meetingUrl?: string;
|
||||
cancelLink?: string;
|
||||
rescheduleLink?: string;
|
||||
ratingUrl?: string;
|
||||
noShowUrl?: string;
|
||||
};
|
||||
|
||||
const customTemplate = (
|
||||
@@ -79,7 +81,9 @@ const customTemplate = (
|
||||
.replaceAll("{TIMEZONE}", variables.timeZone || "")
|
||||
.replaceAll("{CANCEL_URL}", cancelLink)
|
||||
.replaceAll("{RESCHEDULE_URL}", rescheduleLink)
|
||||
.replaceAll("{MEETING_URL}", variables.meetingUrl || "");
|
||||
.replaceAll("{MEETING_URL}", variables.meetingUrl || "")
|
||||
.replaceAll("{RATING_URL}", variables.ratingUrl || "")
|
||||
.replaceAll("{NO_SHOW_URL}", variables.noShowUrl || "");
|
||||
|
||||
const customInputvariables = dynamicText.match(/\{(.+?)}/g)?.map((variable) => {
|
||||
return variable.replace("{", "").replace("}", "");
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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 emailRatingTemplate = ({
|
||||
isEditingMode,
|
||||
action,
|
||||
timeFormat,
|
||||
startTime,
|
||||
endTime,
|
||||
eventName,
|
||||
timeZone,
|
||||
organizer,
|
||||
name,
|
||||
isBrandingDisabled,
|
||||
ratingUrl,
|
||||
noShowUrl,
|
||||
}: {
|
||||
isEditingMode: boolean;
|
||||
action: WorkflowActions;
|
||||
timeFormat?: TimeFormat;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
eventName?: string;
|
||||
timeZone?: string;
|
||||
organizer?: string;
|
||||
name?: string;
|
||||
isBrandingDisabled?: boolean;
|
||||
ratingUrl?: string;
|
||||
noShowUrl?: string;
|
||||
}) => {
|
||||
const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR;
|
||||
const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`;
|
||||
|
||||
let eventDate = "";
|
||||
|
||||
if (isEditingMode) {
|
||||
endTime = "{EVENT_END_TIME}";
|
||||
eventName = "{EVENT_NAME}";
|
||||
timeZone = "{TIMEZONE}";
|
||||
organizer = "{ORGANIZER}";
|
||||
name = action === WorkflowActions.EMAIL_ATTENDEE ? "{ATTENDEE}" : "{ORGANIZER}";
|
||||
eventDate = `{EVENT_DATE_${dateTimeFormat}}`;
|
||||
ratingUrl = "{RATING_URL}";
|
||||
noShowUrl = "{NO_SHOW_URL}";
|
||||
} else {
|
||||
eventDate = dayjs(startTime).tz(timeZone).format(dateTimeFormat);
|
||||
|
||||
endTime = dayjs(endTime).tz(timeZone).format(currentTimeFormat);
|
||||
}
|
||||
|
||||
const emailSubject = `How was your recent experience?: ${eventName}`;
|
||||
|
||||
const introHtml = `<body>Hi${
|
||||
name ? ` ${name}` : ""
|
||||
},<br><br>We're always looking to improve our customer's experience. How satisfied were you with your recent meeting?<br><br>`;
|
||||
|
||||
const ratingHtml = `<a href="${ratingUrl}=1">😠</a> <a href="${ratingUrl}=2">🙁</a> <a href="${ratingUrl}=3">😐</a> <a href="${ratingUrl}=4">😄</a> <a href="${ratingUrl}=5">😍</a><br><br>`;
|
||||
|
||||
const noShowHtml = `<div><a href="${noShowUrl}">${organizer} didn't join the meeting</a></div><br><br>`;
|
||||
|
||||
const eventHtml = `<div><strong class="editor-text-bold">Event: </strong></div>${eventName}<br><br>`;
|
||||
|
||||
const dateTimeHtml = `<div><strong class="editor-text-bold">Date & Time: </strong></div>${eventDate} - ${endTime} (${timeZone})<br><br>`;
|
||||
|
||||
const attendeeHtml = `<div><strong class="editor-text-bold">Attendees: </strong></div>You & ${organizer}<br><br>`;
|
||||
|
||||
const branding = !isBrandingDisabled && !isEditingMode ? `<br><br>_<br><br>Scheduling by ${APP_NAME}` : "";
|
||||
|
||||
const endingHtml = `This survey was triggered by a Workflow in Cal.${branding}</body>`;
|
||||
|
||||
const emailBody =
|
||||
introHtml + ratingHtml + noShowHtml + eventHtml + dateTimeHtml + attendeeHtml + endingHtml;
|
||||
|
||||
return { emailSubject, emailBody };
|
||||
};
|
||||
|
||||
export default emailRatingTemplate;
|
||||
@@ -65,6 +65,9 @@ export const buildBooking = (booking?: Partial<Booking>): Booking => {
|
||||
isRecorded: false,
|
||||
iCalUID: getICalUID({ uid }),
|
||||
iCalSequence: 0,
|
||||
rating: null,
|
||||
noShowHost: null,
|
||||
ratingFeedback: null,
|
||||
...booking,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "WorkflowTemplates" ADD VALUE 'RATING';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Booking" ADD COLUMN "noShowHost" BOOLEAN,
|
||||
ADD COLUMN "rating" INTEGER,
|
||||
ADD COLUMN "ratingFeedback" TEXT;
|
||||
@@ -547,6 +547,9 @@ model Booking {
|
||||
iCalUID String? @default("")
|
||||
iCalSequence Int @default(0)
|
||||
instantMeetingToken InstantMeetingToken?
|
||||
rating Int?
|
||||
ratingFeedback String?
|
||||
noShowHost Boolean?
|
||||
|
||||
@@index([eventTypeId])
|
||||
@@index([userId])
|
||||
@@ -994,6 +997,7 @@ enum WorkflowTemplates {
|
||||
CANCELLED
|
||||
RESCHEDULED
|
||||
COMPLETED
|
||||
RATING
|
||||
}
|
||||
|
||||
enum WorkflowMethods {
|
||||
|
||||
@@ -2,10 +2,12 @@ import publicProcedure from "../../procedures/publicProcedure";
|
||||
import { importHandler, router } from "../../trpc";
|
||||
import { slotsRouter } from "../viewer/slots/_router";
|
||||
import { i18nInputSchema } from "./i18n.schema";
|
||||
import { ZNoShowInputSchema } from "./noShow.schema";
|
||||
import { event } from "./procedures/event";
|
||||
import { session } from "./procedures/session";
|
||||
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
|
||||
import { ZSubmitRatingInputSchema } from "./submitRating.schema";
|
||||
|
||||
const NAMESPACE = "publicViewer";
|
||||
|
||||
@@ -22,6 +24,14 @@ export const publicViewerRouter = router({
|
||||
const handler = await importHandler(namespaced("countryCode"), () => import("./countryCode.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
submitRating: publicProcedure.input(ZSubmitRatingInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("submitRating"), () => import("./submitRating.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
noShow: publicProcedure.input(ZNoShowInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("noShow"), () => import("./noShow.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("samlTenantProduct"),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import type { TNoShowInputSchema } from "./noShow.schema";
|
||||
|
||||
type NoShowOptions = {
|
||||
input: TNoShowInputSchema;
|
||||
};
|
||||
|
||||
export const noShowHandler = async ({ input }: NoShowOptions) => {
|
||||
const { bookingUid } = input;
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
data: {
|
||||
noShowHost: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default noShowHandler;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZNoShowInputSchema = z.object({
|
||||
bookingUid: z.string(),
|
||||
});
|
||||
|
||||
export type TNoShowInputSchema = z.infer<typeof ZNoShowInputSchema>;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import type { TSubmitRatingInputSchema } from "./submitRating.schema";
|
||||
|
||||
type SubmitRatingOptions = {
|
||||
input: TSubmitRatingInputSchema;
|
||||
};
|
||||
|
||||
export const submitRatingHandler = async ({ input }: SubmitRatingOptions) => {
|
||||
const { bookingUid, rating, comment } = input;
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
data: {
|
||||
rating: rating,
|
||||
ratingFeedback: comment,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default submitRatingHandler;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZSubmitRatingInputSchema = z.object({
|
||||
bookingUid: z.string(),
|
||||
rating: z.number(),
|
||||
comment: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TSubmitRatingInputSchema = z.infer<typeof ZSubmitRatingInputSchema>;
|
||||
@@ -18,6 +18,8 @@ export function EmptyScreen({
|
||||
border = true,
|
||||
dashedBorder = true,
|
||||
className,
|
||||
iconClassName,
|
||||
iconWrapperClassName,
|
||||
}: {
|
||||
Icon?: IconName;
|
||||
avatar?: React.ReactElement;
|
||||
@@ -28,6 +30,8 @@ export function EmptyScreen({
|
||||
buttonRaw?: ReactNode; // Used incase you want to provide your own button.
|
||||
border?: boolean;
|
||||
dashedBorder?: boolean;
|
||||
iconWrapperClassName?: string;
|
||||
iconClassName?: string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<>
|
||||
@@ -42,9 +46,17 @@ export function EmptyScreen({
|
||||
{!avatar ? null : (
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full">{avatar}</div>
|
||||
)}
|
||||
|
||||
{!icon ? null : (
|
||||
<div className="bg-emphasis flex h-[72px] w-[72px] items-center justify-center rounded-full ">
|
||||
<Icon name={icon} className="text-default inline-block h-10 w-10 stroke-[1.3px]" />
|
||||
<div
|
||||
className={classNames(
|
||||
"bg-emphasis flex h-[72px] w-[72px] items-center justify-center rounded-full ",
|
||||
iconWrapperClassName
|
||||
)}>
|
||||
<Icon
|
||||
name={icon}
|
||||
className={classNames("text-default inline-block h-10 w-10 stroke-[1.3px]", iconClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[420px] flex-col items-center">
|
||||
|
||||
Reference in New Issue
Block a user