feat: virtual queues tab in insights (#18260)
Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: sean-brydon <sean@cal.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
co-authored by
CarinaWolli
sean-brydon
sean-brydon
Peer Richelsen
Omar López
parent
5fe4e09975
commit
cfb09a1b21
@@ -0,0 +1,17 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/insights/getServerSideProps";
|
||||
|
||||
import InsightsVirtualQueuesPage from "~/insights/insights-virtual-queues-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("insights"),
|
||||
(t) => t("insights_subtitle")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: InsightsVirtualQueuesPage });
|
||||
@@ -18,6 +18,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
@@ -243,15 +244,20 @@ const OnboardingPage = ({
|
||||
const promises = group.eventTypes
|
||||
.filter((eventType) => eventType.selected)
|
||||
.map((value: TEventType) => {
|
||||
// Prevent two payment apps to be enabled
|
||||
// Ok to cast type here because this metadata will be updated as the event type metadata
|
||||
if (
|
||||
checkForMultiplePaymentApps(value.metadata as z.infer<typeof EventTypeMetaDataSchema>)
|
||||
)
|
||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
if (value.metadata?.apps?.stripe?.paymentOption === "HOLD" && value.seatsPerTimeSlot) {
|
||||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
if (value.metadata) {
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(value.metadata);
|
||||
// Prevent two payment apps to be enabled
|
||||
// Ok to cast type here because this metadata will be updated as the event type metadata
|
||||
if (checkForMultiplePaymentApps(metadata))
|
||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
if (
|
||||
value.metadata?.apps?.stripe?.paymentOption === "HOLD" &&
|
||||
value.seatsPerTimeSlot
|
||||
) {
|
||||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
}
|
||||
}
|
||||
|
||||
let updateObject: TUpdateObject = { id: value.id };
|
||||
if (isConferencing) {
|
||||
updateObject = {
|
||||
|
||||
@@ -48,7 +48,7 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { bookingMetadataSchema, eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
showToast,
|
||||
EmptyScreen,
|
||||
Icon,
|
||||
HeadSeo,
|
||||
} from "@calcom/ui";
|
||||
import CancelBooking from "@calcom/web/components/booking/CancelBooking";
|
||||
import EventReservationSchema from "@calcom/web/components/schemas/EventReservationSchema";
|
||||
@@ -229,7 +230,13 @@ export default function Success(props: PageProps) {
|
||||
t,
|
||||
};
|
||||
|
||||
const giphyAppData = getEventTypeAppData(eventType, "giphy");
|
||||
const giphyAppData = getEventTypeAppData(
|
||||
{
|
||||
...eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
},
|
||||
"giphy"
|
||||
);
|
||||
const giphyImage = giphyAppData?.thankYouPage;
|
||||
const isRoundRobin = eventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
||||
|
||||
@@ -433,7 +440,10 @@ export default function Success(props: PageProps) {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<BookingPageTagManager eventType={eventType} />
|
||||
<HeadSeo origin={getOrgFullOrigin(orgSlug)} title={title} description={title} />
|
||||
<BookingPageTagManager
|
||||
eventType={{ ...eventType, metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata) }}
|
||||
/>
|
||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { TestForm } from "@calcom/app-store/routing-forms/components/SingleForm";
|
||||
import type { RoutingForm } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Label, Select } from "@calcom/ui";
|
||||
|
||||
import InsightsLayout from "./layout";
|
||||
|
||||
export default function InsightsVirtualQueuesPage() {
|
||||
const { t } = useLocale();
|
||||
const { data: routingForms, isLoading: isRoutingFormsLoading } =
|
||||
trpc.viewer.insights.getUserRelevantTeamRoutingForms.useQuery();
|
||||
|
||||
const [selectedForm, setSelectedForm] = useState<RoutingForm | undefined>(
|
||||
routingForms && routingForms.length > 0 ? routingForms[0] : undefined
|
||||
);
|
||||
|
||||
if (routingForms && !selectedForm && routingForms.length > 0) {
|
||||
setSelectedForm(routingForms[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<InsightsLayout>
|
||||
<Label>{t("routing_form")}</Label>
|
||||
<Select
|
||||
placeholder="Select project"
|
||||
options={routingForms?.map((form) => ({ label: form.name, value: form.id })) ?? []}
|
||||
isLoading={isRoutingFormsLoading}
|
||||
className="w-60"
|
||||
onChange={(e) => {
|
||||
if (e && routingForms) {
|
||||
const form = routingForms.find((form) => form.id === e.value);
|
||||
setSelectedForm(form);
|
||||
}
|
||||
}}
|
||||
value={selectedForm ? { label: selectedForm.name, value: selectedForm.id } : undefined}
|
||||
/>
|
||||
<div className="mt-10">
|
||||
{selectedForm ? <TestForm form={selectedForm} showAllData={false} /> : <></>}
|
||||
</div>
|
||||
</InsightsLayout>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@@ -128,6 +128,9 @@ vi.mock("@calcom/prisma/zod-utils", () => ({
|
||||
EventTypeMetaDataSchema: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
eventTypeMetaDataSchemaWithTypedApps: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
bookingMetadataSchema: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import type Prisma from "@prisma/client";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { test, todo } from "./lib/fixtures";
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
@@ -51,7 +51,7 @@ test.describe("Stripe integration skip true", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
|
||||
|
||||
const stripeAppMetadata = metadata?.apps?.stripe;
|
||||
|
||||
@@ -93,7 +93,7 @@ test.describe("Stripe integration skip true", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
|
||||
|
||||
const stripeAppMetadata = metadata?.apps?.stripe;
|
||||
|
||||
@@ -291,7 +291,7 @@ test.describe("Stripe integration with the new app install flow skip false", ()
|
||||
});
|
||||
|
||||
for (const eventTypeMetadata of eventTypeMetadatas) {
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
|
||||
const stripeAppMetadata = metadata?.apps?.stripe;
|
||||
expect(stripeAppMetadata).toHaveProperty("credentialId");
|
||||
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
|
||||
@@ -328,7 +328,7 @@ test.describe("Stripe integration with the new app install flow skip false", ()
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
|
||||
|
||||
const stripeAppMetadata = metadata?.apps?.stripe;
|
||||
|
||||
|
||||
@@ -2895,5 +2895,12 @@
|
||||
"reassigned": "Reassigned",
|
||||
"rerouted": "Rerouted",
|
||||
"salesforce_assigned": "Salesforce assignment",
|
||||
"router_position": "Router Position",
|
||||
"show_matching_hosts": "Show matching hosts",
|
||||
"no_active_queues": "No active queues for this input",
|
||||
"weight": "Weight",
|
||||
"calibration": "Calibration",
|
||||
"shortfall": "Shortfall",
|
||||
"routing_form": "Routing Form",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export type EventTypeApps = NonNullable<NonNullable<z.infer<typeof EventTypeMetaDataSchema>>["apps"]>;
|
||||
export type EventTypeApps = NonNullable<
|
||||
NonNullable<z.infer<typeof eventTypeMetaDataSchemaWithTypedApps>>["apps"]
|
||||
>;
|
||||
export type EventTypeAppsList = keyof EventTypeApps;
|
||||
|
||||
export const getEventTypeAppData = <T extends EventTypeAppsList>(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type z from "zod";
|
||||
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { appDataSchemas } from "../../apps.schemas.generated";
|
||||
|
||||
@@ -11,7 +11,12 @@ import type { appDataSchemas } from "../../apps.schemas.generated";
|
||||
* @returns boolean
|
||||
*/
|
||||
const checkForMultiplePaymentApps = (
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>,
|
||||
metadata:
|
||||
| {
|
||||
apps?: z.infer<typeof eventTypeAppMetadataOptionalSchema>;
|
||||
}
|
||||
| null
|
||||
| undefined,
|
||||
inclusive = false
|
||||
) => {
|
||||
let enabledPaymentApps = 0;
|
||||
|
||||
@@ -255,9 +255,11 @@ type MembersMatchResultType = {
|
||||
const TeamMembersMatchResult = ({
|
||||
membersMatchResult,
|
||||
chosenRouteName,
|
||||
showAllData,
|
||||
}: {
|
||||
membersMatchResult: MembersMatchResultType;
|
||||
chosenRouteName: string;
|
||||
showAllData: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
if (!membersMatchResult) return null;
|
||||
@@ -284,6 +286,7 @@ const TeamMembersMatchResult = ({
|
||||
|
||||
const renderQueue = () => {
|
||||
if (isNoLogicFound(membersMatchResult.teamMembersMatchingAttributeLogic)) {
|
||||
if (!showAllData) return <div className="mt-4">{t("no_active_queues")}asdf</div>;
|
||||
if (membersMatchResult.checkedFallback) {
|
||||
return (
|
||||
<span className="font-semibold">
|
||||
@@ -311,12 +314,14 @@ const TeamMembersMatchResult = ({
|
||||
<thead>
|
||||
<tr className="border-b text-left">
|
||||
<th className="py-2 pr-4">#</th>
|
||||
<th className="py-2 pr-4">Email</th>
|
||||
<th className="py-2 pr-4">Bookings</th>
|
||||
{membersMatchResult.perUserData.weights ? <th className="py-2">Weight</th> : null}
|
||||
{membersMatchResult.perUserData.calibrations ? <th className="py-2">Calibration</th> : null}
|
||||
<th className="py-2 pr-4">{t("email")}</th>
|
||||
<th className="py-2 pr-4">{t("bookings")}</th>
|
||||
{membersMatchResult.perUserData.weights ? <th className="py-2">{t("weight")}</th> : null}
|
||||
{membersMatchResult.perUserData.calibrations ? (
|
||||
<th className="py-2">{t("calibration")}</th>
|
||||
) : null}
|
||||
{membersMatchResult.perUserData.bookingShortfalls ? (
|
||||
<th className="border-l py-2 pl-2">Shortfall</th>
|
||||
<th className="border-l py-2 pl-2">{t("shortfall")}</th>
|
||||
) : null}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -353,41 +358,61 @@ const TeamMembersMatchResult = ({
|
||||
|
||||
return (
|
||||
<div className="text-default mt-2 space-y-2">
|
||||
<div data-testid="chosen-route">
|
||||
{t("chosen_route")}: <span className="font-semibold">{chosenRouteName}</span>
|
||||
</div>
|
||||
<div data-testid="attribute-logic-matched" className={cn(hasMainWarnings && "text-error")}>
|
||||
{t("attribute_logic_matched")}: <span className="font-semibold">{renderMainLogicStatus()}</span>
|
||||
{hasMainWarnings && (
|
||||
<Alert className="mt-2" severity="warning" title={membersMatchResult.mainWarnings?.join(", ")} />
|
||||
)}
|
||||
</div>
|
||||
<div data-testid="attribute-logic-fallback-matched" className={cn(hasFallbackWarnings && "text-error")}>
|
||||
{t("attribute_logic_fallback_matched")}:{" "}
|
||||
<span className="font-semibold">{renderFallbackLogicStatus()}</span>
|
||||
{hasFallbackWarnings && (
|
||||
<Alert
|
||||
className="mt-2"
|
||||
severity="warning"
|
||||
title={membersMatchResult.fallbackWarnings?.join(", ")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showAllData ? (
|
||||
<>
|
||||
<div data-testid="chosen-route">
|
||||
{t("chosen_route")}: <span className="font-semibold">{chosenRouteName}</span>
|
||||
</div>
|
||||
<div data-testid="attribute-logic-matched" className={cn(hasMainWarnings && "text-error")}>
|
||||
{t("attribute_logic_matched")}: <span className="font-semibold">{renderMainLogicStatus()}</span>
|
||||
{hasMainWarnings && (
|
||||
<Alert
|
||||
className="mt-2"
|
||||
severity="warning"
|
||||
title={membersMatchResult.mainWarnings?.join(", ")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
data-testid="attribute-logic-fallback-matched"
|
||||
className={cn(hasFallbackWarnings && "text-error")}>
|
||||
{t("attribute_logic_fallback_matched")}:{" "}
|
||||
<span className="font-semibold">{renderFallbackLogicStatus()}</span>
|
||||
{hasFallbackWarnings && (
|
||||
<Alert
|
||||
className="mt-2"
|
||||
severity="warning"
|
||||
title={membersMatchResult.fallbackWarnings?.join(", ")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
{membersMatchResult.contactOwnerEmail ? (
|
||||
<div data-testid="contact-owner-email">
|
||||
{t("contact_owner")}:{" "}
|
||||
<span className="font-semibold">{membersMatchResult.contactOwnerEmail}</span>
|
||||
</div>
|
||||
) : (
|
||||
) : showAllData ? (
|
||||
<div data-testid="contact-owner-email">
|
||||
{t("contact_owner")}: <span className="font-semibold">Not found</span>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="mt-2" data-testid="matching-members">
|
||||
{membersMatchResult.isUsingAttributeWeights
|
||||
? t("matching_members_queue_using_attribute_weights")
|
||||
: t("matching_members_queue_using_event_assignee_weights")}
|
||||
{showAllData ? (
|
||||
<>
|
||||
{membersMatchResult.isUsingAttributeWeights
|
||||
? t("matching_members_queue_using_attribute_weights")
|
||||
: t("matching_members_queue_using_event_assignee_weights")}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{renderQueue()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -410,14 +435,14 @@ type UptoDateForm = Brand<
|
||||
"UptoDateForm"
|
||||
>;
|
||||
|
||||
export const TestFormDialog = ({
|
||||
export const TestForm = ({
|
||||
form,
|
||||
isTestPreviewOpen,
|
||||
setIsTestPreviewOpen,
|
||||
showAllData = true,
|
||||
renderFooter,
|
||||
}: {
|
||||
form: UptoDateForm;
|
||||
isTestPreviewOpen: boolean;
|
||||
setIsTestPreviewOpen: (value: boolean) => void;
|
||||
form: UptoDateForm | RoutingForm;
|
||||
showAllData?: boolean;
|
||||
renderFooter?: (onClose: () => void) => React.ReactNode;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [response, setResponse] = useState<FormResponse>({});
|
||||
@@ -458,12 +483,15 @@ export const TestFormDialog = ({
|
||||
let eventTypeRedirectUrl: string | null = null;
|
||||
|
||||
if (route?.action?.type === "eventTypeRedirectUrl") {
|
||||
eventTypeRedirectUrl = getAbsoluteEventTypeRedirectUrl({
|
||||
eventTypeRedirectUrl: route.action.value,
|
||||
form,
|
||||
allURLSearchParams: new URLSearchParams(),
|
||||
});
|
||||
setEventTypeUrlWithoutParams(eventTypeRedirectUrl);
|
||||
// only needed in routing form testing (type UptoDateForm)
|
||||
if ("team" in form) {
|
||||
eventTypeRedirectUrl = getAbsoluteEventTypeRedirectUrl({
|
||||
eventTypeRedirectUrl: route.action.value,
|
||||
form,
|
||||
allURLSearchParams: new URLSearchParams(),
|
||||
});
|
||||
setEventTypeUrlWithoutParams(eventTypeRedirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
setChosenRoute(route || null);
|
||||
@@ -481,7 +509,7 @@ export const TestFormDialog = ({
|
||||
}
|
||||
}
|
||||
|
||||
const renderTestResult = () => {
|
||||
const renderTestResult = (showAllData: boolean) => {
|
||||
if (!form.routes || !chosenRoute) return null;
|
||||
|
||||
const chosenRouteIndex = form.routes.findIndex((route) => route.id === chosenRoute.id);
|
||||
@@ -493,6 +521,33 @@ export const TestFormDialog = ({
|
||||
return `Route ${chosenRouteIndex + 1}`;
|
||||
};
|
||||
|
||||
const renderTeamMembersMatchResult = (showAllData: boolean, isPending: boolean) => {
|
||||
if (!isTeamForm) return null;
|
||||
if (isPending) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TeamMembersMatchResult
|
||||
chosenRouteName={chosenRouteName()}
|
||||
membersMatchResult={membersMatchResult}
|
||||
showAllData={showAllData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!showAllData) {
|
||||
if (
|
||||
chosenRoute.action.type !== "customPageMessage" &&
|
||||
chosenRoute.action.type !== "externalRedirectUrl"
|
||||
) {
|
||||
{
|
||||
return renderTeamMembersMatchResult(false, findTeamMembersMatchingAttributeLogicMutation.isPending);
|
||||
}
|
||||
}
|
||||
return <div className="mt-4">{t("no_active_queues")}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-subtle text-default mt-5 rounded-md p-3">
|
||||
<div className="font-bold ">{t("route_to")}:</div>
|
||||
@@ -539,18 +594,10 @@ export const TestFormDialog = ({
|
||||
{chosenRoute.action.value}
|
||||
</a>
|
||||
</span>
|
||||
{isTeamForm ? (
|
||||
!findTeamMembersMatchingAttributeLogicMutation.isPending ? (
|
||||
<div>
|
||||
<TeamMembersMatchResult
|
||||
chosenRouteName={chosenRouteName()}
|
||||
membersMatchResult={membersMatchResult}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>Loading...</div>
|
||||
)
|
||||
) : null}
|
||||
{renderTeamMembersMatchResult(
|
||||
showAllData,
|
||||
findTeamMembersMatchingAttributeLogicMutation.isPending
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -558,36 +605,68 @@ export const TestFormDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setChosenRoute(null);
|
||||
setResponse({});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
resetMembersMatchResult();
|
||||
testRouting();
|
||||
}}>
|
||||
<div className="px-1">
|
||||
{form && <FormInputFields form={form} response={response} setResponse={setResponse} />}
|
||||
</div>
|
||||
{!renderFooter ? (
|
||||
<div className="mt-4">
|
||||
<Button type="submit">{t("show_matching_hosts")}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div>{renderTestResult(showAllData)}</div>
|
||||
{renderFooter?.(onClose)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const TestFormDialog = ({
|
||||
form,
|
||||
isTestPreviewOpen,
|
||||
setIsTestPreviewOpen,
|
||||
}: {
|
||||
form: UptoDateForm;
|
||||
isTestPreviewOpen: boolean;
|
||||
setIsTestPreviewOpen: (value: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Dialog open={isTestPreviewOpen} onOpenChange={setIsTestPreviewOpen}>
|
||||
<DialogContent size="md" enableOverflow>
|
||||
<DialogHeader title={t("test_routing_form")} subtitle={t("test_preview_description")} />
|
||||
<div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
resetMembersMatchResult();
|
||||
testRouting();
|
||||
}}>
|
||||
<div className="px-1">
|
||||
{form && <FormInputFields form={form} response={response} setResponse={setResponse} />}
|
||||
</div>
|
||||
<div>{renderTestResult()}</div>
|
||||
<DialogFooter>
|
||||
<DialogClose
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setIsTestPreviewOpen(false);
|
||||
setChosenRoute(null);
|
||||
setResponse({});
|
||||
}}>
|
||||
{t("close")}
|
||||
</DialogClose>
|
||||
<Button type="submit" data-testid="test-routing">
|
||||
{t("test_routing")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
<TestForm
|
||||
form={form}
|
||||
renderFooter={(onClose) => (
|
||||
<DialogFooter>
|
||||
<DialogClose
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setIsTestPreviewOpen(false);
|
||||
onClose();
|
||||
}}>
|
||||
{t("close")}
|
||||
</DialogClose>
|
||||
<Button type="submit" data-testid="test-routing">
|
||||
{t("test_routing")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -27,10 +27,7 @@ export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
.nativeEnum(SalesforceRecordEnum)
|
||||
.default(SalesforceRecordEnum.CONTACT)
|
||||
.optional(),
|
||||
// Another way to define z.boolean().optional() through z.any()
|
||||
// Required to prevent tsc crash. I have no clue why it started crashing suddenly.
|
||||
// But we need to improve the schemas of all apps that are combined into apps.schemas.generated.ts
|
||||
ifFreeEmailDomainSkipOwnerCheck: optionalBooleanOnlyRunTimeValidation,
|
||||
ifFreeEmailDomainSkipOwnerCheck: z.boolean().optional(),
|
||||
skipContactCreation: z.boolean().optional(),
|
||||
createEventOn: z.nativeEnum(SalesforceRecordEnum).default(SalesforceRecordEnum.CONTACT).optional(),
|
||||
createNewContactUnderAccount: z.boolean().optional(),
|
||||
|
||||
+2
-2
@@ -3,6 +3,7 @@ import type z from "zod";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
@@ -76,8 +77,7 @@ export const getAllCredentials = async (
|
||||
}
|
||||
|
||||
// Only return CRM credentials that are enabled on the event type
|
||||
const eventTypeAppMetadata = eventType?.metadata?.apps;
|
||||
console.log(eventTypeAppMetadata);
|
||||
const eventTypeAppMetadata = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
|
||||
|
||||
// Will be [credentialId]: { enabled: boolean }]
|
||||
const eventTypeCrmCredentials: Record<number, { enabled: boolean }> = {};
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema, eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -84,7 +84,8 @@ export async function handleConfirmation(args: {
|
||||
} = args;
|
||||
const eventType = booking.eventType;
|
||||
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
|
||||
const eventManager = new EventManager(user, eventTypeMetadata?.apps);
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(eventTypeMetadata?.apps);
|
||||
const eventManager = new EventManager(user, apps);
|
||||
const scheduleResult = await eventManager.create(evt);
|
||||
const results = scheduleResult.results;
|
||||
const metadata: AdditionalInformation = {};
|
||||
|
||||
@@ -68,6 +68,10 @@ import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import {
|
||||
eventTypeAppMetadataOptionalSchema,
|
||||
eventTypeMetaDataSchemaWithTypedApps,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
@@ -421,7 +425,10 @@ async function handler(
|
||||
const isTeamEventType =
|
||||
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
|
||||
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData({
|
||||
...eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
});
|
||||
loggerWithEventDetails.info(
|
||||
`Booking eventType ${eventTypeId} started`,
|
||||
safeStringify({
|
||||
@@ -1051,7 +1058,13 @@ async function handler(
|
||||
oAuthClientId: platformClientId,
|
||||
};
|
||||
|
||||
const workflows = await getAllWorkflowsFromEventType(eventType, organizerUser.id);
|
||||
const workflows = await getAllWorkflowsFromEventType(
|
||||
{
|
||||
...eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
if (isTeamEventType) {
|
||||
evt.team = {
|
||||
@@ -1275,8 +1288,9 @@ async function handler(
|
||||
|
||||
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
|
||||
const credentials = await monitorCallbackAsync(refreshCredentials, allCredentials);
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
|
||||
const eventManager = !isDryRun
|
||||
? new EventManager({ ...organizerUser, credentials }, eventType?.metadata?.apps)
|
||||
? new EventManager({ ...organizerUser, credentials }, apps)
|
||||
: buildDryRunEventManager();
|
||||
|
||||
let videoCallUrl;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { handlePayment } from "@calcom/lib/payment/handlePayment";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { findBookingQuery } from "../../handleNewBooking/findBookingQuery";
|
||||
import type { IEventTypePaymentCredentialType } from "../../handleNewBooking/types";
|
||||
@@ -147,7 +148,8 @@ const createNewSeat = async (
|
||||
);
|
||||
}
|
||||
const credentials = await refreshCredentials(allCredentials);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials }, eventType?.metadata?.apps);
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials }, apps);
|
||||
await eventManager.updateCalendarAttendees(evt, seatedBooking);
|
||||
|
||||
const foundBooking = await findBookingQuery(seatedBooking.id);
|
||||
|
||||
@@ -14,7 +14,8 @@ import { bookingMinimalSelect, prisma } from "@calcom/prisma";
|
||||
import { AppCategories, BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { EventTypeAppMetadataSchema, EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema, eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type App = {
|
||||
@@ -151,9 +152,11 @@ const handleDeleteCredential = async ({
|
||||
if (credential.app?.categories.includes(AppCategories.crm)) {
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
||||
const appSlugToDelete = credential.app?.slug;
|
||||
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(metadata?.apps);
|
||||
if (appSlugToDelete) {
|
||||
const appMetadata = removeAppFromEventTypeMetadata(appSlugToDelete, metadata);
|
||||
const appMetadata = removeAppFromEventTypeMetadata(appSlugToDelete, {
|
||||
apps,
|
||||
});
|
||||
|
||||
await prisma.$transaction(async () => {
|
||||
await prisma.eventType.update({
|
||||
@@ -179,7 +182,10 @@ const handleDeleteCredential = async ({
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
||||
const appSlug = credential.app?.slug;
|
||||
if (appSlug) {
|
||||
const appMetadata = removeAppFromEventTypeMetadata(appSlug, metadata);
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(metadata?.apps);
|
||||
const appMetadata = removeAppFromEventTypeMetadata(appSlug, {
|
||||
apps,
|
||||
});
|
||||
|
||||
await prisma.$transaction(async () => {
|
||||
await prisma.eventType.update({
|
||||
@@ -358,7 +364,7 @@ const handleDeleteCredential = async ({
|
||||
} else if (
|
||||
appStoreMetadata[credential.app?.slug as keyof typeof appStoreMetadata]?.extendsFeature === "EventType"
|
||||
) {
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata);
|
||||
const appSlug = credential.app?.slug;
|
||||
if (appSlug) {
|
||||
await prisma.eventType.update({
|
||||
@@ -456,7 +462,9 @@ const handleDeleteCredential = async ({
|
||||
|
||||
const removeAppFromEventTypeMetadata = (
|
||||
appSlugToDelete: string,
|
||||
eventTypeMetadata: z.infer<typeof EventTypeMetaDataSchema>
|
||||
eventTypeMetadata: {
|
||||
apps: z.infer<typeof eventTypeAppMetadataOptionalSchema>;
|
||||
}
|
||||
) => {
|
||||
const appMetadata = eventTypeMetadata?.apps
|
||||
? Object.entries(eventTypeMetadata.apps).reduce((filteredApps, [appName, appData]) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { handlePaymentSuccess } from "@calcom/lib/payment/handlePaymentSuccess";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[paymentWebhook]"] });
|
||||
|
||||
@@ -72,7 +73,8 @@ const handleSetupSuccess = async (event: Stripe.Event) => {
|
||||
},
|
||||
});
|
||||
if (!requiresConfirmation) {
|
||||
const eventManager = new EventManager(user, eventType?.metadata?.apps);
|
||||
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventType?.metadata);
|
||||
const eventManager = new EventManager(user, metadata?.apps);
|
||||
const scheduleResult = await eventManager.create(evt);
|
||||
bookingData.references = { create: scheduleResult.referencesToCreate };
|
||||
bookingData.status = BookingStatus.ACCEPTED;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import type { baseEventTypeSelect } from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { EventTypeModel } from "@calcom/prisma/zod";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import { Badge } from "@calcom/ui";
|
||||
|
||||
export type EventTypeDescriptionProps = {
|
||||
@@ -39,7 +40,10 @@ export const EventTypeDescription = ({
|
||||
[eventType.recurringEvent]
|
||||
);
|
||||
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData({
|
||||
...eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import InstantEventController from "./InstantEventController";
|
||||
|
||||
@@ -7,7 +8,10 @@ export const EventInstantTab = ({
|
||||
eventType,
|
||||
isTeamEvent,
|
||||
}: Pick<EventTypeSetupProps, "eventType"> & { isTeamEvent: boolean }) => {
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData({
|
||||
...eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
});
|
||||
|
||||
const requirePayment = paymentAppData.price > 0;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { RecurringEventControllerProps } from "./RecurringEventController";
|
||||
import RecurringEventController from "./RecurringEventController";
|
||||
@@ -6,7 +7,10 @@ import RecurringEventController from "./RecurringEventController";
|
||||
export type EventRecurringTabProps = Omit<RecurringEventControllerProps, "paymentEnabled">;
|
||||
|
||||
export const EventRecurringTab = ({ eventType, customClassNames }: EventRecurringTabProps) => {
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData({
|
||||
...eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
});
|
||||
|
||||
const requirePayment = paymentAppData.price > 0;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { Team } from "@calcom/prisma/client";
|
||||
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||
import {
|
||||
BookerLayouts,
|
||||
EventTypeMetaDataSchema,
|
||||
eventTypeMetaDataSchemaWithTypedApps,
|
||||
bookerLayoutOptions,
|
||||
bookerLayouts as bookerLayoutsSchema,
|
||||
customInputSchema,
|
||||
@@ -368,7 +368,7 @@ export const getPublicEvent = async (
|
||||
|
||||
if (!event) return null;
|
||||
|
||||
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});
|
||||
const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(event.metadata || {});
|
||||
const teamMetadata = teamMetadataSchema.parse(event.team?.metadata || {});
|
||||
const usersAsHosts = event.hosts.map((host) => host.user);
|
||||
|
||||
@@ -517,7 +517,7 @@ function getProfileFromEvent(event: Event) {
|
||||
|
||||
const username = "username" in profile ? profile.username : team?.slug;
|
||||
const weekStart = hosts?.[0]?.user?.weekStart || owner?.weekStart || "Monday";
|
||||
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});
|
||||
const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(event.metadata || {});
|
||||
const userMetaData = userMetadataSchema.parse(profile.metadata || {});
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ChildrenEventType } from "@calcom/features/eventtypes/components/C
|
||||
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
|
||||
import type { EventTypeTranslation } from "@calcom/prisma/client";
|
||||
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { BookerLayoutSettings, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { BookerLayoutSettings, eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { eventTypeColor } from "@calcom/prisma/zod-utils";
|
||||
@@ -122,7 +122,7 @@ export type FormValues = {
|
||||
beforeEventBuffer: number;
|
||||
afterEventBuffer: number;
|
||||
slotInterval: number | null;
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
metadata: z.infer<typeof eventTypeMetaDataSchemaWithTypedApps>;
|
||||
destinationCalendar: {
|
||||
integration: string;
|
||||
externalId: string;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { EventsInsights } from "./events";
|
||||
import { RoutingEventsInsights } from "./routing-events";
|
||||
import { VirtualQueuesInsights } from "./virtual-queues";
|
||||
|
||||
const UserBelongsToTeamInput = z.object({
|
||||
teamId: z.coerce.number().optional().nullable(),
|
||||
@@ -1796,4 +1797,15 @@ export const insightsRouter = router({
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||||
}
|
||||
}),
|
||||
getUserRelevantTeamRoutingForms: authedProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
const routingForms = await VirtualQueuesInsights.getUserRelevantTeamRoutingForms({
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
|
||||
return routingForms;
|
||||
} catch (e) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { readonlyPrisma as prisma } from "@calcom/prisma";
|
||||
import { getSerializableForm } from "@calcom/routing-forms/lib/getSerializableForm";
|
||||
|
||||
class VirtualQueuesInsights {
|
||||
static async getUserRelevantTeamRoutingForms({ userId }: { userId: number }) {
|
||||
type JsonField = { [key: string]: any };
|
||||
|
||||
type RoutingFormType = {
|
||||
id: string;
|
||||
description: string | null;
|
||||
position: number;
|
||||
routes: JsonField | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
name: string;
|
||||
fields: JsonField | null;
|
||||
userId: number;
|
||||
teamId: number | null;
|
||||
disabled: boolean;
|
||||
settings: JsonField | null;
|
||||
};
|
||||
|
||||
const formsRedirectingToWeightedRR: RoutingFormType[] = await prisma.$queryRaw<RoutingFormType[]>`
|
||||
WITH RECURSIVE json_array_elements_recursive AS (
|
||||
SELECT f.id, f."teamId",
|
||||
jsonb_array_elements(f.routes::jsonb) as route
|
||||
FROM "App_RoutingForms_Form" f
|
||||
WHERE f."teamId" IN (
|
||||
SELECT "teamId"
|
||||
FROM "Membership"
|
||||
WHERE "userId" = ${userId}
|
||||
)
|
||||
)
|
||||
SELECT DISTINCT f.*,
|
||||
jsonb_build_object(
|
||||
'parentId', t."parentId",
|
||||
'parent', CASE WHEN p.id IS NOT NULL THEN jsonb_build_object('slug', p.slug) ELSE NULL END,
|
||||
'metadata', t.metadata
|
||||
) as team,
|
||||
jsonb_build_object(
|
||||
'id', u.id,
|
||||
'username', u.username,
|
||||
'movedToProfileId', u."movedToProfileId"
|
||||
) as "user"
|
||||
FROM "App_RoutingForms_Form" f
|
||||
INNER JOIN json_array_elements_recursive r ON f.id = r.id
|
||||
INNER JOIN "EventType" e ON (r.route->>'action')::jsonb->>'eventTypeId' = e.id::text
|
||||
INNER JOIN "Team" t ON f."teamId" = t.id
|
||||
LEFT JOIN "Team" p ON t."parentId" = p.id
|
||||
INNER JOIN "users" u ON f."userId" = u.id
|
||||
WHERE e."schedulingType" = 'roundRobin'
|
||||
AND e."isRRWeightsEnabled" = true
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM "Host" h
|
||||
WHERE h."eventTypeId" = e.id
|
||||
AND h."userId" = ${userId}
|
||||
);
|
||||
`;
|
||||
|
||||
// Convert the raw forms to serializable format
|
||||
const serializableForms = await Promise.all(
|
||||
formsRedirectingToWeightedRR.map(async (form) => {
|
||||
const serializedForm = await getSerializableForm({ form });
|
||||
return serializedForm;
|
||||
})
|
||||
);
|
||||
|
||||
return serializableForms;
|
||||
}
|
||||
}
|
||||
|
||||
export { VirtualQueuesInsights };
|
||||
@@ -4,7 +4,7 @@ import type { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"
|
||||
import CrmManager from "@calcom/core/crmManager/crmManager";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeAppMetadataOptionalSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { NoShowAttendees } from "../handleMarkNoShow";
|
||||
import { noShowEnabledApps } from "./noShowEnabledApps";
|
||||
@@ -38,7 +38,7 @@ export default async function handleSendingAttendeeNoShowDataToApps(
|
||||
log.error(`Malformed event type metadata for bookingUid ${bookingUid}`);
|
||||
return;
|
||||
}
|
||||
const eventTypeAppMetadata = eventTypeMetadataParse.data?.apps;
|
||||
const eventTypeAppMetadata = eventTypeAppMetadataOptionalSchema.parse(eventTypeMetadataParse.data?.apps);
|
||||
|
||||
for (const slug in eventTypeAppMetadata) {
|
||||
if (noShowEnabledApps.includes(slug)) {
|
||||
|
||||
@@ -116,6 +116,11 @@ const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemTy
|
||||
href: "/insights/routing",
|
||||
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/routing") ?? false,
|
||||
},
|
||||
{
|
||||
name: "router_position",
|
||||
href: "/insights/virtual-queues",
|
||||
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/virtual-queues") ?? false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ import slugify from "@calcom/lib/slugify";
|
||||
import { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { userSelect } from "@calcom/prisma/selects";
|
||||
import type { CustomInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
@@ -132,7 +133,7 @@ export const dynamicEvent = {
|
||||
descriptionAsSafeHTML: "",
|
||||
position: 0,
|
||||
...commons,
|
||||
metadata: EventTypeMetaDataSchema.parse({ multipleDuration: [15, 30, 45, 60, 90] }),
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse({ multipleDuration: [15, 30, 45, 60, 90] }),
|
||||
};
|
||||
|
||||
export const defaultEvents = [dynamicEvent];
|
||||
|
||||
@@ -11,7 +11,7 @@ import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { SchedulingType, MembershipRole } from "@calcom/prisma/enums";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { customInputSchema, eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -58,7 +58,7 @@ export const getEventTypeById = async ({
|
||||
}
|
||||
|
||||
const { locations, metadata, ...restEventType } = rawEventType;
|
||||
const newMetadata = EventTypeMetaDataSchema.parse(metadata || {}) || {};
|
||||
const newMetadata = eventTypeMetaDataSchemaWithTypedApps.parse(metadata || {}) || {};
|
||||
const apps = newMetadata?.apps || {};
|
||||
const eventTypeWithParsedMetadata = { ...rawEventType, metadata: newMetadata };
|
||||
const eventTeamMembershipsWithUserProfile = [];
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeMetaDataSchemaWithoutApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -116,7 +116,7 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo
|
||||
})
|
||||
)
|
||||
),
|
||||
metadata: eventType.metadata ? EventTypeMetaDataSchema.parse(eventType.metadata) : null,
|
||||
metadata: eventType.metadata ? eventTypeMetaDataSchemaWithoutApps.parse(eventType.metadata) : null,
|
||||
children: await Promise.all(
|
||||
(eventType.children || []).map(async (c) => ({
|
||||
...c,
|
||||
|
||||
@@ -5,12 +5,20 @@ import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
|
||||
import type { appDataSchema, paymentOptionEnum } from "@calcom/app-store/stripepayment/zod";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export default function getPaymentAppData(
|
||||
eventType: Pick<BookerEvent, "price" | "currency" | "metadata">,
|
||||
_eventType: Pick<BookerEvent, "price" | "currency"> & {
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
},
|
||||
forcedGet?: boolean
|
||||
) {
|
||||
const metadataApps = eventType?.metadata?.apps as unknown as EventTypeAppsList;
|
||||
const eventType = {
|
||||
..._eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(_eventType.metadata),
|
||||
};
|
||||
const metadataApps = eventType.metadata?.apps;
|
||||
if (!metadataApps) {
|
||||
return { enabled: false, price: 0, currency: "usd", appId: null };
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AppCategories, Prisma } from "@prisma/client";
|
||||
import appStore from "@calcom/app-store";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import type { CompleteEventType } from "@calcom/prisma/zod";
|
||||
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
|
||||
|
||||
@@ -40,15 +41,15 @@ const handlePayment = async (
|
||||
|
||||
const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService;
|
||||
|
||||
const paymentOption =
|
||||
selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(selectedEventType?.metadata?.apps);
|
||||
const paymentOption = apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
|
||||
|
||||
let paymentData;
|
||||
if (paymentOption === "HOLD") {
|
||||
paymentData = await paymentInstance.collectCard(
|
||||
{
|
||||
amount: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].price,
|
||||
currency: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].currency,
|
||||
amount: apps?.[paymentAppCredentials.appId].price,
|
||||
currency: apps?.[paymentAppCredentials.appId].currency,
|
||||
},
|
||||
booking.id,
|
||||
paymentOption,
|
||||
@@ -58,8 +59,8 @@ const handlePayment = async (
|
||||
} else {
|
||||
paymentData = await paymentInstance.create(
|
||||
{
|
||||
amount: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].price,
|
||||
currency: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].currency,
|
||||
amount: apps?.[paymentAppCredentials.appId].price,
|
||||
currency: apps?.[paymentAppCredentials.appId].currency,
|
||||
},
|
||||
booking.id,
|
||||
booking.userId,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||
import { getBooking } from "@calcom/lib/payment/getBooking";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import logger from "../logger";
|
||||
|
||||
@@ -26,7 +27,8 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number)
|
||||
|
||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||
if (isConfirmed) {
|
||||
const eventManager = new EventManager(userWithCredentials, eventType?.metadata?.apps);
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
|
||||
const eventManager = new EventManager(userWithCredentials, apps);
|
||||
const scheduleResult = await eventManager.create(evt);
|
||||
bookingData.references = { create: scheduleResult.referencesToCreate };
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ export function getPiiFreeDestinationCalendar(destinationCalendar: Partial<Desti
|
||||
};
|
||||
}
|
||||
|
||||
export function getPiiFreeEventType(eventType: Partial<Omit<EventType, "recurringEvent">>) {
|
||||
export function getPiiFreeEventType(
|
||||
eventType: Partial<Pick<EventType, "id" | "schedulingType" | "seatsPerTimeSlot">>
|
||||
) {
|
||||
return {
|
||||
id: eventType.id,
|
||||
schedulingType: eventType.schedulingType,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Prisma } from "@prisma/client";
|
||||
|
||||
import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema, EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export class EventTypeService {
|
||||
static async getEventTypeAppDataFromId(eventTypeId: number, appSlug: keyof typeof appDataSchemas) {
|
||||
@@ -26,8 +26,8 @@ export class EventTypeService {
|
||||
if (!parseEventTypeAppMetadata.success || !parseEventTypeAppMetadata.data?.apps) return null;
|
||||
|
||||
const eventTypeAppMetadata = parseEventTypeAppMetadata.data.apps;
|
||||
|
||||
const appMetadata = eventTypeAppMetadata[appSlug];
|
||||
const apps = EventTypeAppMetadataSchema.parse(eventTypeAppMetadata);
|
||||
const appMetadata = apps[appSlug];
|
||||
|
||||
return appMetadata;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeBookingFields as eventTypeBookingFieldsSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type Fields = z.infer<typeof eventTypeBookingFieldsSchema>;
|
||||
@@ -325,8 +324,7 @@ export const useEventTypeForm = ({
|
||||
|
||||
// Prevent two payment apps to be enabled
|
||||
// Ok to cast type here because this metadata will be updated as the event type metadata
|
||||
if (checkForMultiplePaymentApps(metadata as z.infer<typeof EventTypeMetaDataSchema>))
|
||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
if (checkForMultiplePaymentApps(metadata)) throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
|
||||
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
|
||||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
} from "@calcom/features/eventtypes/lib/types";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import type { VerticalTabItemProps } from "@calcom/ui";
|
||||
|
||||
type Props = {
|
||||
@@ -55,7 +56,10 @@ export const useTabsNavigations = ({
|
||||
eventTypeApps?.items.find((app) => app.slug === appId)?.isInstalled && appData.enabled
|
||||
).length;
|
||||
}
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData({
|
||||
...eventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
});
|
||||
|
||||
const requirePayment = paymentAppData.price > 0;
|
||||
|
||||
|
||||
@@ -71,50 +71,64 @@ export type BookerLayoutSettings = z.infer<typeof bookerLayouts>;
|
||||
export const RequiresConfirmationThresholdUnits: z.ZodType<UnitTypeLongPlural> = z.enum(["hours", "minutes"]);
|
||||
|
||||
export const EventTypeAppMetadataSchema = z.object(appDataSchemas).partial();
|
||||
export const eventTypeAppMetadataOptionalSchema = EventTypeAppMetadataSchema.optional();
|
||||
|
||||
export const EventTypeMetaDataSchema = z
|
||||
.object({
|
||||
smartContractAddress: z.string().optional(),
|
||||
blockchainId: z.number().optional(),
|
||||
multipleDuration: z.number().array().optional(),
|
||||
giphyThankYouPage: z.string().optional(),
|
||||
apps: EventTypeAppMetadataSchema.optional(),
|
||||
additionalNotesRequired: z.boolean().optional(),
|
||||
disableSuccessPage: z.boolean().optional(),
|
||||
disableStandardEmails: z
|
||||
.object({
|
||||
all: z
|
||||
.object({
|
||||
host: z.boolean().optional(),
|
||||
attendee: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
confirmation: z
|
||||
.object({
|
||||
host: z.boolean().optional(),
|
||||
attendee: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
managedEventConfig: z
|
||||
.object({
|
||||
unlockedFields: z.custom<{ [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true }>().optional(),
|
||||
})
|
||||
.optional(),
|
||||
requiresConfirmationThreshold: z
|
||||
.object({
|
||||
time: z.number(),
|
||||
unit: RequiresConfirmationThresholdUnits,
|
||||
})
|
||||
.optional(),
|
||||
config: z
|
||||
.object({
|
||||
useHostSchedulesForTeamEvent: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
bookerLayouts: bookerLayouts.optional(),
|
||||
const _eventTypeMetaDataSchemaWithoutApps = z.object({
|
||||
smartContractAddress: z.string().optional(),
|
||||
blockchainId: z.number().optional(),
|
||||
multipleDuration: z.number().array().optional(),
|
||||
giphyThankYouPage: z.string().optional(),
|
||||
additionalNotesRequired: z.boolean().optional(),
|
||||
disableSuccessPage: z.boolean().optional(),
|
||||
disableStandardEmails: z
|
||||
.object({
|
||||
all: z
|
||||
.object({
|
||||
host: z.boolean().optional(),
|
||||
attendee: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
confirmation: z
|
||||
.object({
|
||||
host: z.boolean().optional(),
|
||||
attendee: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
managedEventConfig: z
|
||||
.object({
|
||||
unlockedFields: z.custom<{ [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true }>().optional(),
|
||||
})
|
||||
.optional(),
|
||||
requiresConfirmationThreshold: z
|
||||
.object({
|
||||
time: z.number(),
|
||||
unit: RequiresConfirmationThresholdUnits,
|
||||
})
|
||||
.optional(),
|
||||
config: z
|
||||
.object({
|
||||
useHostSchedulesForTeamEvent: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
bookerLayouts: bookerLayouts.optional(),
|
||||
});
|
||||
|
||||
export const eventTypeMetaDataSchemaWithUntypedApps = _eventTypeMetaDataSchemaWithoutApps.merge(
|
||||
z.object({
|
||||
apps: z.unknown().optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const EventTypeMetaDataSchema = eventTypeMetaDataSchemaWithUntypedApps.nullable();
|
||||
export const eventTypeMetaDataSchemaWithoutApps = _eventTypeMetaDataSchemaWithoutApps.nullable();
|
||||
export const eventTypeMetaDataSchemaWithTypedApps = _eventTypeMetaDataSchemaWithoutApps
|
||||
.merge(
|
||||
z.object({
|
||||
apps: eventTypeAppMetadataOptionalSchema,
|
||||
})
|
||||
)
|
||||
.nullable();
|
||||
|
||||
export type EventTypeMetadata = z.infer<typeof EventTypeMetaDataSchema>;
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
handleCustomInputs,
|
||||
handlePeriodType,
|
||||
} from "./util";
|
||||
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type SessionUser = NonNullable<TrpcSessionUser>;
|
||||
type User = {
|
||||
@@ -382,8 +383,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
}
|
||||
}
|
||||
|
||||
for (const appKey in input.metadata?.apps) {
|
||||
const app = input.metadata?.apps[appKey as keyof typeof appDataSchemas];
|
||||
const apps = eventTypeAppMetadataOptionalSchema.parse(input.metadata?.apps);
|
||||
for (const appKey in apps) {
|
||||
const app = apps[appKey as keyof typeof appDataSchemas];
|
||||
// There should only be one enabled payment app in the metadata
|
||||
if (app.enabled && app.price && app.currency) {
|
||||
data.price = app.price;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isTeamMember } from "@calcom/lib/server/queries";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -40,7 +41,13 @@ export const getAllActiveWorkflowsHandler = async ({ input, ctx }: GetAllActiveW
|
||||
if (!team) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const allActiveWorkflows = await getAllWorkflowsFromEventType(completeEventType, eventType.userId);
|
||||
const allActiveWorkflows = await getAllWorkflowsFromEventType(
|
||||
{
|
||||
...completeEventType,
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
|
||||
},
|
||||
eventType.userId
|
||||
);
|
||||
|
||||
return allActiveWorkflows;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user