feat: add POST end point to mark calls as no-shows (#15690)
* feat: add POST end point to mark calls as no-shows * fix: type * feat: add v2 endpoint * wip * chore: type err * chore: add permission * chore: remove packae * chore: remove v1 API endpoint * fix: endpoint * feat: add new version * chore: change route * chore: error msg * fix: message * fix: type err * chore: output * fix: type err * chore: bump libraries to 0.0.17 * chore: add noShowHost --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
co-authored by
Joe Au-Yeung
Morgan
Morgan Vernay
parent
1263519973
commit
8ee27d3997
@@ -26,10 +26,11 @@
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-libraries-0.0.13": "npm:@calcom/platform-libraries@0.0.13",
|
||||
"@calcom/platform-libraries-0.0.14": "npm:@calcom/platform-libraries@0.0.14",
|
||||
"@calcom/platform-libraries-0.0.15": "npm:@calcom/platform-libraries@0.0.15",
|
||||
"@calcom/platform-libraries-0.0.17": "npm:@calcom/platform-libraries@0.0.17",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-libraries-0.0.4": "npm:@calcom/platform-libraries@0.0.4",
|
||||
"@calcom/platform-libraries-0.0.14": "npm:@calcom/platform-libraries@0.0.14",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
||||
import { MarkNoShowInput } from "@/ee/bookings/inputs/mark-no-show.input";
|
||||
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||
import { MarkNoShowOutput } from "@/ee/bookings/outputs/mark-no-show.output";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
@@ -32,7 +34,7 @@ import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
getBookingInfo,
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries-0.0.2";
|
||||
import { handleMarkNoShow } from "@calcom/platform-libraries-0.0.17";
|
||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
@@ -194,6 +197,27 @@ export class BookingsController {
|
||||
throw new InternalServerErrorException("Could not cancel booking.");
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/mark-no-show")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async markNoShow(
|
||||
@Body() body: MarkNoShowInput,
|
||||
@Param("bookingUid") bookingUid: string
|
||||
): Promise<MarkNoShowOutput> {
|
||||
try {
|
||||
const markNoShowResponse = await handleMarkNoShow({
|
||||
bookingUid: bookingUid,
|
||||
attendees: body.attendees,
|
||||
noShowHost: body.noShowHost,
|
||||
});
|
||||
|
||||
return { status: SUCCESS_STATUS, data: markNoShowResponse };
|
||||
} catch (err) {
|
||||
this.handleBookingErrors(err, "no-show");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not mark no show.");
|
||||
}
|
||||
|
||||
@Post("/recurring")
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@@ -287,8 +311,14 @@ export class BookingsController {
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
private handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
|
||||
private handleBookingErrors(
|
||||
err: Error | HttpError | unknown,
|
||||
type?: "recurring" | `instant` | "no-show"
|
||||
): void {
|
||||
const errMsg =
|
||||
type === "no-show"
|
||||
? `Error while marking no-show.`
|
||||
: `Error while creating ${type ? type + " " : ""}booking.`;
|
||||
if (err instanceof HttpError) {
|
||||
const httpError = err as HttpError;
|
||||
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Type } from "class-transformer";
|
||||
import { IsOptional, IsArray, IsEmail, IsBoolean, ValidateNested } from "class-validator";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
export class MarkNoShowInput {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsString, IsEnum, IsOptional, ValidateNested, IsArray, IsEmail, IsBoolean } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
class HandleMarkNoShowData {
|
||||
@IsString()
|
||||
message!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
messageKey?: string;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
|
||||
export class MarkNoShowOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: HandleMarkNoShowData,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => HandleMarkNoShowData)
|
||||
data!: HandleMarkNoShowData;
|
||||
}
|
||||
@@ -692,7 +692,10 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {
|
||||
|
||||
const noShowMutation = trpc.viewer.public.noShow.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
showToast(t(data.message, { x: name || email }), "success");
|
||||
showToast(
|
||||
t("messageKey" in data && data.messageKey ? data.messageKey : data.message, { x: name || email }),
|
||||
"success"
|
||||
);
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
@@ -800,7 +803,7 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
|
||||
const { t } = useLocale();
|
||||
const noShowMutation = trpc.viewer.public.noShow.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
showToast(t(data.message), "success");
|
||||
showToast(t("messageKey" in data && data.messageKey ? data.messageKey : data.message), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function Success(props: PageProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (noShow) {
|
||||
noShowMutation.mutate({ bookingUid: bookingInfo.uid });
|
||||
noShowMutation.mutate({ bookingUid: bookingInfo.uid, noShowHost: true });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -85,3 +85,7 @@ export type BookingResponse = Awaited<
|
||||
export type InstantBookingResponse = Awaited<
|
||||
ReturnType<typeof import("@calcom/features/instant-meeting/handleInstantMeeting").default>
|
||||
>;
|
||||
|
||||
export type MarkNoShowResponse = Awaited<
|
||||
ReturnType<typeof import("@calcom/features/handleMarkNoShow").default>
|
||||
>;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { WebhookService } from "@calcom/features/webhooks/lib/WebhookService";
|
||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/client";
|
||||
import type { TNoShowInputSchema } from "@calcom/trpc/server/routers/publicViewer/noShow.schema";
|
||||
|
||||
const getResultPayload = async (attendees: { email: string; noShow: boolean }[]) => {
|
||||
if (attendees.length === 1) {
|
||||
const [attendee] = attendees;
|
||||
return {
|
||||
messageKey: attendee.noShow ? "x_marked_as_no_show" : "x_unmarked_as_no_show",
|
||||
attendees: [attendee],
|
||||
};
|
||||
}
|
||||
return {
|
||||
messageKey: "no_show_updated",
|
||||
attendees: attendees,
|
||||
};
|
||||
};
|
||||
|
||||
type ResponsePayload = {
|
||||
attendees: { email: string; noShow: boolean }[];
|
||||
noShowHost: boolean;
|
||||
message: string;
|
||||
messageKey: string | undefined;
|
||||
};
|
||||
|
||||
const logFailedResults = (results: PromiseSettledResult<any>[]) => {
|
||||
const failed = results.filter((x) => x.status === "rejected") as PromiseRejectedResult[];
|
||||
if (failed.length < 1) return;
|
||||
const failedMessage = failed.map((r) => r.reason);
|
||||
console.error("Failed to update no-show status", failedMessage.join(","));
|
||||
};
|
||||
|
||||
const handleMarkNoShow = async ({ bookingUid, attendees, noShowHost }: TNoShowInputSchema) => {
|
||||
const responsePayload: ResponsePayload = {
|
||||
attendees: [],
|
||||
noShowHost: false,
|
||||
message: "Failed to update no-show status",
|
||||
messageKey: undefined,
|
||||
};
|
||||
try {
|
||||
const attendeeEmails = attendees?.map((attendee) => attendee.email) || [];
|
||||
if (attendees && attendeeEmails.length > 0) {
|
||||
const allAttendees = await prisma.attendee.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
booking: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
email: {
|
||||
in: attendeeEmails,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const allAttendeesMap = allAttendees.reduce((acc, attendee) => {
|
||||
acc[attendee.email] = attendee;
|
||||
return acc;
|
||||
}, {} as Record<string, { id: number; email: string }>);
|
||||
|
||||
const updatePromises = attendees.map((attendee) => {
|
||||
const attendeeToUpdate = allAttendeesMap[attendee.email];
|
||||
if (!attendeeToUpdate) return;
|
||||
return prisma.attendee.update({
|
||||
where: { id: attendeeToUpdate.id },
|
||||
data: { noShow: attendee.noShow },
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(updatePromises);
|
||||
logFailedResults(results);
|
||||
|
||||
const _attendees = results
|
||||
.filter((x) => x.status === "fulfilled")
|
||||
.map((x) => (x as PromiseFulfilledResult<{ noShow: boolean; email: string }>).value)
|
||||
.map((x) => ({ email: x.email, noShow: x.noShow }));
|
||||
|
||||
const payload = await getResultPayload(_attendees);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { uid: bookingUid },
|
||||
select: {
|
||||
id: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const orgId = await getOrgIdFromMemberOrTeamId({
|
||||
memberId: booking?.eventType?.userId,
|
||||
teamId: booking?.eventType?.teamId,
|
||||
});
|
||||
const webhooks = await new WebhookService({
|
||||
teamId: booking?.eventType?.teamId,
|
||||
userId: booking?.eventType?.userId,
|
||||
eventTypeId: booking?.eventType?.id,
|
||||
orgId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
|
||||
});
|
||||
|
||||
const t = await getTranslation("en", "common");
|
||||
const message = t(payload.messageKey, { x: payload.attendees[0]?.email || "User" });
|
||||
|
||||
await webhooks.sendPayload({
|
||||
...payload,
|
||||
/** We send webhook message pre-translated, on client we already handle this */
|
||||
// @ts-expect-error payload is too booking specific, we need to refactor this
|
||||
message,
|
||||
bookingUid,
|
||||
bookingId: booking?.id,
|
||||
});
|
||||
|
||||
responsePayload["attendees"] = payload.attendees;
|
||||
responsePayload["message"] = message;
|
||||
responsePayload["messageKey"] = payload.messageKey;
|
||||
}
|
||||
|
||||
if (noShowHost) {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
data: {
|
||||
noShowHost: true,
|
||||
},
|
||||
});
|
||||
|
||||
responsePayload["noShowHost"] = true;
|
||||
responsePayload["message"] = "No-show status updated";
|
||||
}
|
||||
|
||||
return responsePayload;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
return { message: "Failed to update no-show status" };
|
||||
}
|
||||
};
|
||||
|
||||
export default handleMarkNoShow;
|
||||
@@ -4,6 +4,7 @@ import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
|
||||
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
import * as newBookingMethods from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import handleMarkNoShow from "@calcom/features/handleMarkNoShow";
|
||||
import * as instantMeetingMethods from "@calcom/features/instant-meeting/handleInstantMeeting";
|
||||
import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings";
|
||||
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
@@ -41,6 +42,8 @@ export { handleNewBooking };
|
||||
const handleInstantMeeting = instantMeetingMethods.default;
|
||||
export { handleInstantMeeting };
|
||||
|
||||
export { handleMarkNoShow };
|
||||
|
||||
export { getAvailableSlots };
|
||||
export type AvailableSlotsType = Awaited<ReturnType<typeof getAvailableSlots>>;
|
||||
export { handleNewRecurringBooking } from "@calcom/features/bookings/lib/handleNewRecurringBooking";
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { WebhookService } from "@calcom/features/webhooks/lib/WebhookService";
|
||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/client";
|
||||
import handleMarkNoShow from "@calcom/features/handleMarkNoShow";
|
||||
|
||||
import type { TNoShowInputSchema } from "./noShow.schema";
|
||||
|
||||
@@ -11,118 +6,10 @@ type NoShowOptions = {
|
||||
input: TNoShowInputSchema;
|
||||
};
|
||||
|
||||
const getResultPayload = async (attendees: { email: string; noShow: boolean }[]) => {
|
||||
if (attendees.length === 1) {
|
||||
const [attendee] = attendees;
|
||||
return {
|
||||
message: attendee.noShow ? "x_marked_as_no_show" : "x_unmarked_as_no_show",
|
||||
attendees: [attendee],
|
||||
};
|
||||
}
|
||||
return { message: "no_show_updated", attendees: attendees };
|
||||
};
|
||||
|
||||
const logFailedResults = (results: PromiseSettledResult<any>[]) => {
|
||||
const failed = results.filter((x) => x.status === "rejected") as PromiseRejectedResult[];
|
||||
if (failed.length < 1) return;
|
||||
const failedMessage = failed.map((r) => r.reason);
|
||||
console.error("Failed to update no-show status", failedMessage.join(","));
|
||||
};
|
||||
|
||||
export const noShowHandler = async ({ input }: NoShowOptions) => {
|
||||
const { bookingUid, attendees } = input;
|
||||
|
||||
try {
|
||||
const attendeeEmails = attendees?.map((attendee) => attendee.email) || [];
|
||||
if (attendees && attendeeEmails.length > 0) {
|
||||
const allAttendees = await prisma.attendee.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
booking: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
email: {
|
||||
in: attendeeEmails,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
const allAttendeesMap = allAttendees.reduce((acc, attendee) => {
|
||||
acc[attendee.email] = attendee;
|
||||
return acc;
|
||||
}, {} as Record<string, { id: number; email: string }>);
|
||||
const updatePromises = attendees.map((attendee) => {
|
||||
const attendeeToUpdate = allAttendeesMap[attendee.email];
|
||||
if (!attendeeToUpdate) return;
|
||||
return prisma.attendee.update({
|
||||
where: { id: attendeeToUpdate.id },
|
||||
data: { noShow: attendee.noShow },
|
||||
});
|
||||
});
|
||||
const results = await Promise.allSettled(updatePromises);
|
||||
logFailedResults(results);
|
||||
const _attendees = results
|
||||
.filter((x) => x.status === "fulfilled")
|
||||
.map((x) => (x as PromiseFulfilledResult<{ noShow: boolean; email: string }>).value)
|
||||
.map((x) => ({ email: x.email, noShow: x.noShow }));
|
||||
const payload = await getResultPayload(_attendees);
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { uid: bookingUid },
|
||||
select: {
|
||||
id: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const orgId = await getOrgIdFromMemberOrTeamId({
|
||||
memberId: booking?.eventType?.userId,
|
||||
teamId: booking?.eventType?.teamId,
|
||||
});
|
||||
const webhooks = await new WebhookService({
|
||||
teamId: booking?.eventType?.teamId,
|
||||
userId: booking?.eventType?.userId,
|
||||
eventTypeId: booking?.eventType?.id,
|
||||
orgId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
|
||||
});
|
||||
|
||||
const t = await getTranslation("en", "common");
|
||||
await webhooks.sendPayload({
|
||||
...payload,
|
||||
/** We send webhook message pre-translated, on client we already handle this */
|
||||
// @ts-expect-error payload is too booking specific, we need to refactor this
|
||||
message: t(payload.message, { x: payload.attendees[0]?.email || "User" }),
|
||||
bookingUid,
|
||||
bookingId: booking?.id,
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
data: {
|
||||
noShowHost: true,
|
||||
},
|
||||
});
|
||||
return { message: "No-show status updated", noShowHost: true };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
return { message: "Failed to update no-show status" };
|
||||
}
|
||||
return handleMarkNoShow({ bookingUid, attendees });
|
||||
};
|
||||
|
||||
export default noShowHandler;
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZNoShowInputSchema = z.object({
|
||||
bookingUid: z.string(),
|
||||
attendees: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
noShow: z.boolean(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
export const ZNoShowInputSchema = z
|
||||
.object({
|
||||
bookingUid: z.string(),
|
||||
attendees: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
noShow: z.boolean(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
noShowHost: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
return (data.attendees && data.attendees.length > 0) || data.noShowHost !== undefined;
|
||||
},
|
||||
{
|
||||
message: "At least one of 'attendees' or 'noShowHost' must be provided",
|
||||
path: ["attendees", "noShowHost"],
|
||||
}
|
||||
);
|
||||
|
||||
export type TNoShowInputSchema = z.infer<typeof ZNoShowInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user