Merge branch 'main' into refactor/handle-seats
This commit is contained in:
commit
6e30d9ad65
|
@ -28,7 +28,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
organization-name: calcom
|
organization-name: calcom
|
||||||
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||||
apply-labels-from-issue:
|
apply-labels-from-issue:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ jobs:
|
||||||
body="${body//'%'/'%25'}"
|
body="${body//'%'/'%25'}"
|
||||||
body="${body//$'\n'/'%0A'}"
|
body="${body//$'\n'/'%0A'}"
|
||||||
body="${body//$'\r'/'%0D'}"
|
body="${body//$'\r'/'%0D'}"
|
||||||
echo "{body}={$body}" >> $GITHUB_OUTPUT
|
echo "{body}=${body}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
uses: peter-evans/find-comment@v2
|
uses: peter-evans/find-comment@v2
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* description: Delete all remaining bookings
|
* description: Delete all remaining bookings
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: reason
|
* name: cancellationReason
|
||||||
* required: false
|
* required: false
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
|
|
|
@ -69,7 +69,12 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
||||||
export async function patchHandler(req: NextApiRequest) {
|
export async function patchHandler(req: NextApiRequest) {
|
||||||
const { prisma, query, userId, isAdmin } = req;
|
const { prisma, query, userId, isAdmin } = req;
|
||||||
const { id } = schemaQueryIdAsString.parse(query);
|
const { id } = schemaQueryIdAsString.parse(query);
|
||||||
const { eventTypeId, userId: bodyUserId, ...data } = schemaWebhookEditBodyParams.parse(req.body);
|
const {
|
||||||
|
eventTypeId,
|
||||||
|
userId: bodyUserId,
|
||||||
|
eventTriggers,
|
||||||
|
...data
|
||||||
|
} = schemaWebhookEditBodyParams.parse(req.body);
|
||||||
const args: Prisma.WebhookUpdateArgs = { where: { id }, data };
|
const args: Prisma.WebhookUpdateArgs = { where: { id }, data };
|
||||||
|
|
||||||
if (eventTypeId) {
|
if (eventTypeId) {
|
||||||
|
@ -87,6 +92,11 @@ export async function patchHandler(req: NextApiRequest) {
|
||||||
args.data.userId = bodyUserId;
|
args.data.userId = bodyUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.data.eventTriggers) {
|
||||||
|
const eventTriggersSet = new Set(eventTriggers);
|
||||||
|
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await prisma.webhook.update(args);
|
const result = await prisma.webhook.update(args);
|
||||||
return { webhook: schemaWebhookReadPublic.parse(result) };
|
return { webhook: schemaWebhookReadPublic.parse(result) };
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,12 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
|
||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, prisma } = req;
|
const { userId, isAdmin, prisma } = req;
|
||||||
const { eventTypeId, userId: bodyUserId, ...body } = schemaWebhookCreateBodyParams.parse(req.body);
|
const {
|
||||||
|
eventTypeId,
|
||||||
|
userId: bodyUserId,
|
||||||
|
eventTriggers,
|
||||||
|
...body
|
||||||
|
} = schemaWebhookCreateBodyParams.parse(req.body);
|
||||||
const args: Prisma.WebhookCreateArgs = { data: { id: uuidv4(), ...body } };
|
const args: Prisma.WebhookCreateArgs = { data: { id: uuidv4(), ...body } };
|
||||||
|
|
||||||
// If no event type, we assume is for the current user. If admin we run more checks below...
|
// If no event type, we assume is for the current user. If admin we run more checks below...
|
||||||
|
@ -87,6 +92,11 @@ async function postHandler(req: NextApiRequest) {
|
||||||
args.data.userId = bodyUserId;
|
args.data.userId = bodyUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.data.eventTriggers) {
|
||||||
|
const eventTriggersSet = new Set(eventTriggers);
|
||||||
|
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await prisma.webhook.create(args);
|
const data = await prisma.webhook.create(args);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||||
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export const withAppDir =
|
||||||
|
(getServerSideProps: GetServerSideProps) => async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssrResponse = await getServerSideProps(context);
|
||||||
|
|
||||||
|
if ("redirect" in ssrResponse) {
|
||||||
|
redirect(ssrResponse.redirect.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("notFound" in ssrResponse) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssrResponse.props;
|
||||||
|
};
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { isNotFoundError } from "next/dist/client/components/not-found";
|
||||||
|
import { getURLFromRedirectError, isRedirectError } from "next/dist/client/components/redirect";
|
||||||
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
export type EmbedProps = {
|
||||||
|
isEmbed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function withEmbedSsrAppDir<T extends Record<string, any>>(
|
||||||
|
getData: (context: GetServerSidePropsContext) => Promise<T>
|
||||||
|
) {
|
||||||
|
return async (context: GetServerSidePropsContext): Promise<T> => {
|
||||||
|
const { embed, layout } = context.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const props = await getData(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
isEmbed: true,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (isRedirectError(e)) {
|
||||||
|
const destinationUrl = getURLFromRedirectError(e);
|
||||||
|
let urlPrefix = "";
|
||||||
|
|
||||||
|
// Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
|
||||||
|
const destinationUrlObj = new URL(destinationUrl, WEBAPP_URL);
|
||||||
|
|
||||||
|
// If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
|
||||||
|
if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
|
||||||
|
urlPrefix = destinationUrlObj.origin;
|
||||||
|
} else {
|
||||||
|
// Don't use any prefix for relative URLs to ensure we stay on the same domain
|
||||||
|
urlPrefix = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const destinationQueryStr = destinationUrlObj.searchParams.toString();
|
||||||
|
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||||
|
const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
|
||||||
|
destinationQueryStr ? `${destinationQueryStr}&` : ""
|
||||||
|
}layout=${layout}&embed=${embed}`;
|
||||||
|
|
||||||
|
redirect(newDestinationUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotFoundError(e)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import OldPage from "@pages/booking/[uid]";
|
||||||
|
import withEmbedSsrAppDir from "app/WithEmbedSSR";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getData } from "../page";
|
||||||
|
|
||||||
|
const getEmbedData = withEmbedSsrAppDir(getData);
|
||||||
|
|
||||||
|
// @ts-expect-error Type '(context: GetServerSidePropsContext) => Promise<any>' is not assignable to type '(arg: {
|
||||||
|
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: OldPage });
|
|
@ -0,0 +1,204 @@
|
||||||
|
import OldPage from "@pages/booking/[uid]";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||||
|
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } from "@lib/booking";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const stringToBoolean = z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => val === "true");
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
uid: z.string(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
eventTypeSlug: z.string().optional(),
|
||||||
|
cancel: stringToBoolean,
|
||||||
|
allRemainingBookings: stringToBoolean,
|
||||||
|
changes: stringToBoolean,
|
||||||
|
reschedule: stringToBoolean,
|
||||||
|
isSuccessBookingPage: stringToBoolean,
|
||||||
|
formerTime: z.string().optional(),
|
||||||
|
seatReferenceUid: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "",
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getData = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
const session = await getServerSession(context);
|
||||||
|
let tz: string | null = null;
|
||||||
|
let userTimeFormat: number | null = null;
|
||||||
|
let requiresLoginToUpdate = false;
|
||||||
|
if (session) {
|
||||||
|
const user = await ssr.viewer.me.fetch();
|
||||||
|
tz = user.timeZone;
|
||||||
|
userTimeFormat = user.timeFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedQuery = querySchema.safeParse(context.query);
|
||||||
|
|
||||||
|
if (!parsedQuery.success) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
||||||
|
|
||||||
|
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
||||||
|
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||||
|
where: {
|
||||||
|
uid: maybeUid,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
id: true,
|
||||||
|
uid: true,
|
||||||
|
description: true,
|
||||||
|
customInputs: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
recurringEventId: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
location: true,
|
||||||
|
status: true,
|
||||||
|
metadata: true,
|
||||||
|
cancellationReason: true,
|
||||||
|
responses: true,
|
||||||
|
rejectionReason: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
timeZone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
timeZone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eventTypeId: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
eventName: true,
|
||||||
|
slug: true,
|
||||||
|
timeZone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seatsReferences: {
|
||||||
|
select: {
|
||||||
|
referenceUid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bookingInfoRaw) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
||||||
|
? getDefaultEvent(eventTypeSlug || "")
|
||||||
|
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
||||||
|
if (!eventTypeRaw) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
||||||
|
requiresLoginToUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||||
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
|
// probably fixable with json.stringify -> json.parse
|
||||||
|
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
|
||||||
|
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
|
||||||
|
? eventTypeRaw.hosts.map((host) => host.user)
|
||||||
|
: eventTypeRaw.users;
|
||||||
|
|
||||||
|
if (!eventTypeRaw.users.length) {
|
||||||
|
if (!eventTypeRaw.owner) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTypeRaw.users.push({
|
||||||
|
...eventTypeRaw.owner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventType = {
|
||||||
|
...eventTypeRaw,
|
||||||
|
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
|
||||||
|
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
||||||
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
||||||
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
|
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||||
|
};
|
||||||
|
|
||||||
|
const profile = {
|
||||||
|
name: eventType.team?.name || eventType.users[0]?.name || null,
|
||||||
|
email: eventType.team ? null : eventType.users[0].email || null,
|
||||||
|
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
|
||||||
|
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
|
||||||
|
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
|
||||||
|
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
|
||||||
|
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment = await prisma.payment.findFirst({
|
||||||
|
where: {
|
||||||
|
bookingId: bookingInfo.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
success: true,
|
||||||
|
refunded: true,
|
||||||
|
currency: true,
|
||||||
|
amount: true,
|
||||||
|
paymentOption: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||||
|
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||||
|
profile,
|
||||||
|
eventType,
|
||||||
|
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
||||||
|
dehydratedState: ssr.dehydrate(),
|
||||||
|
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
||||||
|
bookingInfo,
|
||||||
|
paymentStatus: payment,
|
||||||
|
...(tz && { tz }),
|
||||||
|
userTimeFormat,
|
||||||
|
requiresLoginToUpdate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Argument of type '{ req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
|
||||||
|
export default WithLayout({ getLayout: null, getData, Page: OldPage });
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout })<"L">;
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
import EnterprisePage from "@components/EnterprisePage";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("create_your_org"),
|
||||||
|
(t) => t("create_your_org_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EnterprisePage;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import LegacyPage from "@pages/insights/index";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||||
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "Insights",
|
||||||
|
(t) => t("insights_subtitle")
|
||||||
|
);
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||||
|
const flags = await getFeatureFlagMap(prisma);
|
||||||
|
|
||||||
|
if (flags.insights === false) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout, getData, Page: LegacyPage });
|
|
@ -0,0 +1,13 @@
|
||||||
|
import LegacyPage from "@pages/maintenance";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => `${t("under_maintenance")} | ${APP_NAME}`,
|
||||||
|
(t) => t("under_maintenance_description", { appName: APP_NAME })
|
||||||
|
);
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
|
|
@ -0,0 +1,4 @@
|
||||||
|
import Page from "@pages/more";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout: null, Page })<"P">;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { getServerSideProps } from "@pages/reschedule/[uid]";
|
||||||
|
import { withAppDir } from "app/AppDirSSRHOC";
|
||||||
|
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
|
||||||
|
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
type PageProps = Readonly<{
|
||||||
|
params: Params;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const Page = async ({ params }: PageProps) => {
|
||||||
|
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
|
||||||
|
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }'
|
||||||
|
await withAppDir(withEmbedSsr(getServerSideProps))(legacyCtx);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import OldPage, { getServerSideProps as _getServerSideProps } from "@pages/reschedule/[uid]";
|
||||||
|
import { withAppDir } from "app/AppDirSSRHOC";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
|
||||||
|
import { headers, cookies } from "next/headers";
|
||||||
|
|
||||||
|
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "",
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
|
||||||
|
type PageProps = Readonly<{
|
||||||
|
params: Params;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const getData = withAppDir(_getServerSideProps);
|
||||||
|
|
||||||
|
const Page = async ({ params }: PageProps) => {
|
||||||
|
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
|
||||||
|
|
||||||
|
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }'
|
||||||
|
await getData(legacyCtx);
|
||||||
|
|
||||||
|
return <OldPage />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/my-account/appearance";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("appearance"),
|
||||||
|
(t) => t("appearance_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/my-account/calendars";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("calendars"),
|
||||||
|
(t) => t("calendars_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/my-account/conferencing";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("conferencing"),
|
||||||
|
(t) => t("conferencing_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/my-account/general";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("general"),
|
||||||
|
(t) => t("general_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/my-account/profile";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("profile"),
|
||||||
|
(t) => t("profile_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/security/impersonation";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("impersonation"),
|
||||||
|
(t) => t("impersonation_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/security/password";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("password"),
|
||||||
|
(t) => t("password_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
import Page from "@calcom/features/ee/sso/page/user-sso-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("sso_configuration"),
|
||||||
|
(t) => t("sso_configuration_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout });
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Page from "@pages/settings/security/two-factor-auth";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("two_factor_auth"),
|
||||||
|
(t) => t("add_an_extra_layer_of_security")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
import { type GetServerSidePropsContext } from "next";
|
||||||
|
import { headers, cookies } from "next/headers";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import LegacyPage from "@calcom/features/ee/workflows/pages/workflow";
|
||||||
|
|
||||||
|
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
workflow: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
const { workflow } = await getProps(
|
||||||
|
buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext
|
||||||
|
);
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => workflow ?? "Untitled",
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getProps(context: GetServerSidePropsContext) {
|
||||||
|
const safeParams = querySchema.safeParse(context.params);
|
||||||
|
|
||||||
|
console.log("Built workflow page:", safeParams);
|
||||||
|
if (!safeParams.success) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
return { workflow: safeParams.data.workflow };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateStaticParams = () => [];
|
||||||
|
|
||||||
|
// @ts-expect-error getData arg
|
||||||
|
export default WithLayout({ getLayout: null, getData: getProps, Page: LegacyPage })<"P">;
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
// generate segments on demand
|
||||||
|
export const dynamicParams = true;
|
||||||
|
export const revalidate = 10;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||||
|
import LegacyPage from "@calcom/features/ee/workflows/pages/index";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("workflows"),
|
||||||
|
(t) => t("workflows_to_automate_notifications")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default WithLayout({ getLayout, Page: LegacyPage })<"P">;
|
|
@ -7,7 +7,7 @@ import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
type WithLayoutParams<T extends Record<string, any>> = {
|
type WithLayoutParams<T extends Record<string, any>> = {
|
||||||
getLayout: ((page: React.ReactElement) => React.ReactNode) | null;
|
getLayout: ((page: React.ReactElement) => React.ReactNode) | null;
|
||||||
Page?: (props: T) => React.ReactElement;
|
Page?: (props: T) => React.ReactElement | null;
|
||||||
getData?: (arg: ReturnType<typeof buildLegacyCtx>) => Promise<T>;
|
getData?: (arg: ReturnType<typeof buildLegacyCtx>) => Promise<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
|
import { UpgradeTip } from "@calcom/features/tips";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button, ButtonGroup } from "@calcom/ui";
|
||||||
|
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
export default function EnterprisePage() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: <Globe className="h-5 w-5 text-red-500" />,
|
||||||
|
title: t("branded_subdomain"),
|
||||||
|
description: t("branded_subdomain_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BarChart className="h-5 w-5 text-blue-500" />,
|
||||||
|
title: t("org_insights"),
|
||||||
|
description: t("org_insights_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
|
||||||
|
title: t("extensive_whitelabeling"),
|
||||||
|
description: t("extensive_whitelabeling_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Users className="h-5 w-5 text-orange-500" />,
|
||||||
|
title: t("unlimited_teams"),
|
||||||
|
description: t("unlimited_teams_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <CreditCard className="h-5 w-5 text-green-500" />,
|
||||||
|
title: t("unified_billing"),
|
||||||
|
description: t("unified_billing_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Lock className="h-5 w-5 text-purple-500" />,
|
||||||
|
title: t("advanced_managed_events"),
|
||||||
|
description: t("advanced_managed_events_description"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ShellMain heading="Enterprise" subtitle={t("enterprise_description")}>
|
||||||
|
<UpgradeTip
|
||||||
|
plan="enterprise"
|
||||||
|
title={t("create_your_org")}
|
||||||
|
description={t("create_your_org_description")}
|
||||||
|
features={features}
|
||||||
|
background="/tips/enterprise"
|
||||||
|
buttons={
|
||||||
|
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button color="primary" href="https://i.cal.com/sales/enterprise?duration=25" target="_blank">
|
||||||
|
{t("contact_sales")}
|
||||||
|
</Button>
|
||||||
|
<Button color="minimal" href="https://cal.com/enterprise" target="_blank">
|
||||||
|
{t("learn_more")}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<>Create Org</>
|
||||||
|
</UpgradeTip>
|
||||||
|
</ShellMain>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -116,7 +116,7 @@ function getNavigation(props: {
|
||||||
{
|
{
|
||||||
name: "workflows",
|
name: "workflows",
|
||||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||||
icon: PhoneCall,
|
icon: Zap,
|
||||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -219,7 +219,7 @@ function EventTypeSingleLayout({
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "instant_tab_title",
|
name: "instant_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=instant`,
|
href: `/event-types/${eventType.id}?tabName=instant`,
|
||||||
icon: Zap,
|
icon: PhoneCall,
|
||||||
info: `instant_event_tab_description`,
|
info: `instant_event_tab_description`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
.custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button {
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media query for screens larger than 768px */
|
||||||
|
@media (max-width: 639) {
|
||||||
|
.custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharts-cartesian-grid-horizontal line{
|
||||||
|
@apply stroke-emphasis
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-button button{
|
||||||
|
@apply !h-9 !max-h-9 border-default hover:border-emphasis
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarButton,
|
||||||
|
.tremor-DateRangePicker-dropdownButton {
|
||||||
|
@apply border-subtle bg-default focus-within:ring-emphasis hover:border-subtle dark:focus-within:ring-emphasis hover:bg-subtle text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-dropdownModal{
|
||||||
|
@apply divide-none
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DropdownItem-root{
|
||||||
|
@apply !h-9 !max-h-9 bg-default hover:bg-subtle text-default hover:text-emphasis
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarButtonText,
|
||||||
|
.tremor-DateRangePicker-dropdownButtonText {
|
||||||
|
@apply text-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarHeaderText{
|
||||||
|
@apply !text-default
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarHeader svg{
|
||||||
|
@apply text-default
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarHeader button{
|
||||||
|
@apply hover:bg-emphasis shadow-none focus:ring-0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarHeader button:hover svg{
|
||||||
|
@apply text-emphasis
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarButtonIcon{
|
||||||
|
@apply text-default
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarModal,
|
||||||
|
.tremor-DateRangePicker-dropdownModal {
|
||||||
|
@apply bg-default border-subtle shadow-dropdown
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarBodyDate button{
|
||||||
|
@apply text-default hover:bg-emphasis
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarBodyDate button:disabled,
|
||||||
|
.tremor-DateRangePicker-calendarBodyDate button[disabled]{
|
||||||
|
@apply opacity-25
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarHeader button{
|
||||||
|
@apply border-default text-default
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarBodyDate .bg-gray-100{
|
||||||
|
@apply bg-subtle
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarBodyDate .bg-gray-500{
|
||||||
|
@apply !bg-brand-default text-inverted
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tremor-Card-root {
|
||||||
|
@apply p-5 bg-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-TableCell-root {
|
||||||
|
@apply pl-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharts-responsive-container {
|
||||||
|
@apply -mx-4;
|
||||||
|
}
|
||||||
|
.tremor-Card-root > p {
|
||||||
|
@apply mb-2 text-base font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-Legend-legendItem {
|
||||||
|
@apply ml-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tremor-TableBody-root {
|
||||||
|
@apply divide-subtle;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { BookingRedirectForm } from "@pages/settings/my-account/out-of-office";
|
||||||
|
import { DateRangePicker } from "@tremor/react";
|
||||||
|
import type { UseFormSetValue } from "react-hook-form";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
import "./DateSelect.css";
|
||||||
|
|
||||||
|
interface IOutOfOfficeDateRangeSelectProps {
|
||||||
|
dateRange: [Date | null, Date | null, null];
|
||||||
|
setDateRange: React.Dispatch<React.SetStateAction<[Date | null, Date | null, null]>>;
|
||||||
|
setValue: UseFormSetValue<BookingRedirectForm>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OutOfOfficeDateRangePicker = (props: IOutOfOfficeDateRangeSelectProps) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { dateRange, setDateRange, setValue } = props;
|
||||||
|
return (
|
||||||
|
<div className="custom-date">
|
||||||
|
<DateRangePicker
|
||||||
|
value={dateRange}
|
||||||
|
defaultValue={dateRange}
|
||||||
|
onValueChange={(datesArray) => {
|
||||||
|
const [start, end] = datesArray;
|
||||||
|
|
||||||
|
if (start) {
|
||||||
|
setDateRange([start, end as Date | null, null]);
|
||||||
|
}
|
||||||
|
if (start && end) {
|
||||||
|
setValue("startDate", start.toISOString());
|
||||||
|
setValue("endDate", end.toISOString());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="gray"
|
||||||
|
options={undefined}
|
||||||
|
enableDropdown={false}
|
||||||
|
placeholder={t("select_date_range")}
|
||||||
|
enableYearPagination={true}
|
||||||
|
minDate={dayjs().startOf("d").toDate()}
|
||||||
|
maxDate={dayjs().add(2, "y").endOf("d").toDate()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { OutOfOfficeDateRangePicker };
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
export const getEventTypesFromDB = async (id: number) => {
|
||||||
|
const userSelect = {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
username: true,
|
||||||
|
hideBranding: true,
|
||||||
|
theme: true,
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
email: true,
|
||||||
|
timeZone: true,
|
||||||
|
};
|
||||||
|
const eventType = await prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
length: true,
|
||||||
|
eventName: true,
|
||||||
|
recurringEvent: true,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
userId: true,
|
||||||
|
successRedirectUrl: true,
|
||||||
|
customInputs: true,
|
||||||
|
locations: true,
|
||||||
|
price: true,
|
||||||
|
currency: true,
|
||||||
|
bookingFields: true,
|
||||||
|
disableGuests: true,
|
||||||
|
timeZone: true,
|
||||||
|
owner: {
|
||||||
|
select: userSelect,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
select: userSelect,
|
||||||
|
},
|
||||||
|
hosts: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: userSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
hideBranding: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workflows: {
|
||||||
|
select: {
|
||||||
|
workflow: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
steps: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: true,
|
||||||
|
seatsPerTimeSlot: true,
|
||||||
|
seatsShowAttendees: true,
|
||||||
|
seatsShowAvailabilityCount: true,
|
||||||
|
periodStartDate: true,
|
||||||
|
periodEndDate: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!eventType) {
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDynamic: false,
|
||||||
|
...eventType,
|
||||||
|
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleSeatsEventTypeOnBooking = async (
|
||||||
|
eventType: {
|
||||||
|
seatsPerTimeSlot?: number | null;
|
||||||
|
seatsShowAttendees: boolean | null;
|
||||||
|
seatsShowAvailabilityCount: boolean | null;
|
||||||
|
[x: string | number | symbol]: unknown;
|
||||||
|
},
|
||||||
|
bookingInfo: Partial<
|
||||||
|
Prisma.BookingGetPayload<{
|
||||||
|
include: {
|
||||||
|
attendees: { select: { name: true; email: true } };
|
||||||
|
seatsReferences: { select: { referenceUid: true } };
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
name: true;
|
||||||
|
email: true;
|
||||||
|
username: true;
|
||||||
|
timeZone: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>
|
||||||
|
>,
|
||||||
|
seatReferenceUid?: string,
|
||||||
|
userId?: number
|
||||||
|
) => {
|
||||||
|
if (eventType?.seatsPerTimeSlot !== null) {
|
||||||
|
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
|
||||||
|
delete bookingInfo.description;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// @TODO: If handling teams, we need to do more check ups for this.
|
||||||
|
if (bookingInfo?.user?.id === userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eventType.seatsShowAttendees) {
|
||||||
|
const seatAttendee = await prisma.bookingSeat.findFirst({
|
||||||
|
where: {
|
||||||
|
referenceUid: seatReferenceUid,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
attendee: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (seatAttendee) {
|
||||||
|
const attendee = bookingInfo?.attendees?.find((a) => {
|
||||||
|
return a.email === seatAttendee.attendee?.email;
|
||||||
|
});
|
||||||
|
bookingInfo["attendees"] = attendee ? [attendee] : [];
|
||||||
|
} else {
|
||||||
|
bookingInfo["attendees"] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bookingInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getRecurringBookings(recurringEventId: string | null) {
|
||||||
|
if (!recurringEventId) return null;
|
||||||
|
const recurringBookings = await prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
recurringEventId,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
startTime: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return recurringBookings.map((obj) => obj.startTime.toString());
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||||
|
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const stringToBoolean = z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => val === "true");
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
uid: z.string(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
eventTypeSlug: z.string().optional(),
|
||||||
|
cancel: stringToBoolean,
|
||||||
|
allRemainingBookings: stringToBoolean,
|
||||||
|
changes: stringToBoolean,
|
||||||
|
reschedule: stringToBoolean,
|
||||||
|
isSuccessBookingPage: stringToBoolean,
|
||||||
|
formerTime: z.string().optional(),
|
||||||
|
seatReferenceUid: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
// this is needed to prevent bundling of lib/booking to the client bundle
|
||||||
|
// usually functions that are used in getServerSideProps are tree shaken from client bundle
|
||||||
|
// but not in case when they are exported. So we have to dynamically load them, or to copy paste them to the /future/page.
|
||||||
|
|
||||||
|
const { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } = await import(
|
||||||
|
"@lib/booking"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
const session = await getServerSession(context);
|
||||||
|
let tz: string | null = null;
|
||||||
|
let userTimeFormat: number | null = null;
|
||||||
|
let requiresLoginToUpdate = false;
|
||||||
|
if (session) {
|
||||||
|
const user = await ssr.viewer.me.fetch();
|
||||||
|
tz = user.timeZone;
|
||||||
|
userTimeFormat = user.timeFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedQuery = querySchema.safeParse(context.query);
|
||||||
|
|
||||||
|
if (!parsedQuery.success) return { notFound: true } as const;
|
||||||
|
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
||||||
|
|
||||||
|
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
||||||
|
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||||
|
where: {
|
||||||
|
uid: maybeUid,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
id: true,
|
||||||
|
uid: true,
|
||||||
|
description: true,
|
||||||
|
customInputs: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
recurringEventId: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
location: true,
|
||||||
|
status: true,
|
||||||
|
metadata: true,
|
||||||
|
cancellationReason: true,
|
||||||
|
responses: true,
|
||||||
|
rejectionReason: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
timeZone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
timeZone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eventTypeId: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
eventName: true,
|
||||||
|
slug: true,
|
||||||
|
timeZone: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seatsReferences: {
|
||||||
|
select: {
|
||||||
|
referenceUid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!bookingInfoRaw) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
||||||
|
? getDefaultEvent(eventTypeSlug || "")
|
||||||
|
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
||||||
|
if (!eventTypeRaw) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
||||||
|
requiresLoginToUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||||
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
|
// probably fixable with json.stringify -> json.parse
|
||||||
|
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
|
||||||
|
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
|
||||||
|
? eventTypeRaw.hosts.map((host) => host.user)
|
||||||
|
: eventTypeRaw.users;
|
||||||
|
|
||||||
|
if (!eventTypeRaw.users.length) {
|
||||||
|
if (!eventTypeRaw.owner)
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
} as const;
|
||||||
|
eventTypeRaw.users.push({
|
||||||
|
...eventTypeRaw.owner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventType = {
|
||||||
|
...eventTypeRaw,
|
||||||
|
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
|
||||||
|
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
||||||
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
||||||
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
|
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||||
|
};
|
||||||
|
|
||||||
|
const profile = {
|
||||||
|
name: eventType.team?.name || eventType.users[0]?.name || null,
|
||||||
|
email: eventType.team ? null : eventType.users[0].email || null,
|
||||||
|
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
|
||||||
|
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
|
||||||
|
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
|
||||||
|
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
|
||||||
|
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment = await prisma.payment.findFirst({
|
||||||
|
where: {
|
||||||
|
bookingId: bookingInfo.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
success: true,
|
||||||
|
refunded: true,
|
||||||
|
currency: true,
|
||||||
|
amount: true,
|
||||||
|
paymentOption: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||||
|
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||||
|
profile,
|
||||||
|
eventType,
|
||||||
|
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
||||||
|
bookingInfo,
|
||||||
|
paymentStatus: payment,
|
||||||
|
...(tz && { tz }),
|
||||||
|
userTimeFormat,
|
||||||
|
requiresLoginToUpdate,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -39,7 +39,7 @@
|
||||||
"@calcom/tsconfig": "*",
|
"@calcom/tsconfig": "*",
|
||||||
"@calcom/ui": "*",
|
"@calcom/ui": "*",
|
||||||
"@daily-co/daily-js": "^0.37.0",
|
"@daily-co/daily-js": "^0.37.0",
|
||||||
"@formkit/auto-animate": "^0.8.1",
|
"@formkit/auto-animate": "1.0.0-beta.5",
|
||||||
"@glidejs/glide": "^3.5.2",
|
"@glidejs/glide": "^3.5.2",
|
||||||
"@hookform/error-message": "^2.0.0",
|
"@hookform/error-message": "^2.0.0",
|
||||||
"@hookform/resolvers": "^2.9.7",
|
"@hookform/resolvers": "^2.9.7",
|
||||||
|
|
|
@ -2,6 +2,8 @@ import type { DehydratedState } from "@tanstack/react-query";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { encode } from "querystring";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
@ -11,10 +13,12 @@ import {
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-user";
|
||||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||||
|
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||||
|
@ -40,6 +44,7 @@ import { getTemporaryOrgRedirect } from "../lib/getTemporaryOrgRedirect";
|
||||||
|
|
||||||
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
|
@ -59,6 +64,8 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||||
...query
|
...query
|
||||||
} = useRouterQuery();
|
} = useRouterQuery();
|
||||||
|
|
||||||
|
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||||
|
const fromUserNameRedirected = searchParams?.get("username") || "";
|
||||||
/*
|
/*
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -77,6 +84,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEventListEmpty = eventTypes.length === 0;
|
const isEventListEmpty = eventTypes.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
|
@ -100,6 +108,25 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||||
isEmbed ? "border-booker border-booker-width bg-default rounded-md border" : "",
|
isEmbed ? "border-booker border-booker-width bg-default rounded-md border" : "",
|
||||||
"max-w-3xl px-4 py-24"
|
"max-w-3xl px-4 py-24"
|
||||||
)}>
|
)}>
|
||||||
|
{isRedirect && (
|
||||||
|
<div className="mb-8 rounded-md bg-blue-100 p-4 dark:border dark:bg-transparent dark:bg-transparent">
|
||||||
|
<h2 className="text-default mb-2 text-sm font-semibold dark:text-white">
|
||||||
|
{t("user_redirect_title", {
|
||||||
|
username: fromUserNameRedirected,
|
||||||
|
})}{" "}
|
||||||
|
🏝️
|
||||||
|
</h2>
|
||||||
|
<p className="text-default text-sm">
|
||||||
|
{t("user_redirect_description", {
|
||||||
|
profile: {
|
||||||
|
username: user.username,
|
||||||
|
},
|
||||||
|
username: fromUserNameRedirected,
|
||||||
|
})}{" "}
|
||||||
|
😄
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
size="xl"
|
size="xl"
|
||||||
|
@ -290,6 +317,18 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||||
const usernameList = getUsernameList(context.query.user as string);
|
const usernameList = getUsernameList(context.query.user as string);
|
||||||
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
||||||
const dataFetchStart = Date.now();
|
const dataFetchStart = Date.now();
|
||||||
|
let outOfOffice = false;
|
||||||
|
|
||||||
|
if (usernameList.length === 1) {
|
||||||
|
const result = await handleUserRedirection({ username: usernameList[0] });
|
||||||
|
if (result && result.outOfOffice) {
|
||||||
|
outOfOffice = true;
|
||||||
|
}
|
||||||
|
if (result && result.redirect?.destination) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const usersWithoutAvatar = await prisma.user.findMany({
|
const usersWithoutAvatar = await prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
username: {
|
username: {
|
||||||
|
@ -374,9 +413,9 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||||
name: user.name || user.username || "",
|
name: user.name || user.username || "",
|
||||||
image: user.avatar,
|
image: user.avatar,
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
brandColor: user.brandColor,
|
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||||
avatarUrl: user.avatarUrl,
|
avatarUrl: user.avatarUrl,
|
||||||
darkBrandColor: user.darkBrandColor,
|
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
organization: {
|
organization: {
|
||||||
|
@ -400,11 +439,16 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// if profile only has one public event-type, redirect to it
|
// if profile only has one public event-type, redirect to it
|
||||||
if (eventTypes.length === 1 && context.query.redirect !== "false") {
|
if (eventTypes.length === 1 && context.query.redirect !== "false" && !outOfOffice) {
|
||||||
|
// Redirect but don't change the URL
|
||||||
|
const urlDestination = `/${user.username}/${eventTypes[0].slug}`;
|
||||||
|
const { query } = context;
|
||||||
|
const urlQuery = new URLSearchParams(encode(query));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: `/${user.username}/${eventTypes[0].slug}`,
|
destination: `${urlDestination}?${urlQuery}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -421,7 +465,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||||
username: user.username,
|
username: user.username,
|
||||||
bio: user.bio,
|
bio: user.bio,
|
||||||
avatarUrl: user.avatarUrl,
|
avatarUrl: user.avatarUrl,
|
||||||
away: user.away,
|
away: usernameList.length === 1 ? outOfOffice : user.away,
|
||||||
verified: user.verified,
|
verified: user.verified,
|
||||||
})),
|
})),
|
||||||
entity: {
|
entity: {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { z } from "zod";
|
||||||
|
|
||||||
import { Booker } from "@calcom/atoms";
|
import { Booker } from "@calcom/atoms";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { handleTypeRedirection } from "@calcom/features/booking-redirect/handle-type";
|
||||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
@ -164,7 +165,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
const username = usernames[0];
|
const username = usernames[0];
|
||||||
const { rescheduleUid, bookingUid } = context.query;
|
const { rescheduleUid, bookingUid } = context.query;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
|
let outOfOffice = false;
|
||||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||||
|
|
||||||
if (!isOrgContext) {
|
if (!isOrgContext) {
|
||||||
|
@ -188,7 +189,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
away: true,
|
id: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
allowSEOIndexing: true,
|
allowSEOIndexing: true,
|
||||||
},
|
},
|
||||||
|
@ -199,6 +200,18 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
// If user is found, quickly verify bookingRedirects
|
||||||
|
const result = await handleTypeRedirection({
|
||||||
|
userId: user.id,
|
||||||
|
username,
|
||||||
|
slug,
|
||||||
|
});
|
||||||
|
if (result && result.outOfOffice) {
|
||||||
|
outOfOffice = true;
|
||||||
|
}
|
||||||
|
if (result && result.redirect?.destination) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
|
@ -230,7 +243,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
length: eventData.length,
|
length: eventData.length,
|
||||||
metadata: eventData.metadata,
|
metadata: eventData.metadata,
|
||||||
},
|
},
|
||||||
away: user?.away,
|
away: outOfOffice,
|
||||||
user: username,
|
user: username,
|
||||||
slug,
|
slug,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
|
|
|
@ -55,6 +55,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
const payload: OAuthTokenPayload = {
|
const payload: OAuthTokenPayload = {
|
||||||
userId: decodedRefreshToken.userId,
|
userId: decodedRefreshToken.userId,
|
||||||
|
teamId: decodedRefreshToken.teamId,
|
||||||
scope: decodedRefreshToken.scope,
|
scope: decodedRefreshToken.scope,
|
||||||
token_type: "Access Token",
|
token_type: "Access Token",
|
||||||
clientId: client_id,
|
clientId: client_id,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter, usePathname } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
@ -104,24 +105,32 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border-subtle bg-default mb-16 overflow-hidden rounded-md border">
|
<>
|
||||||
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
|
<div className="border-subtle bg-default overflow-hidden rounded-md border">
|
||||||
{schedules.map((schedule) => (
|
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
|
||||||
<ScheduleListItem
|
{schedules.map((schedule) => (
|
||||||
displayOptions={{
|
<ScheduleListItem
|
||||||
hour12: meQuery.data?.timeFormat ? meQuery.data.timeFormat === 12 : undefined,
|
displayOptions={{
|
||||||
timeZone: meQuery.data?.timeZone,
|
hour12: meQuery.data?.timeFormat ? meQuery.data.timeFormat === 12 : undefined,
|
||||||
}}
|
timeZone: meQuery.data?.timeZone,
|
||||||
key={schedule.id}
|
}}
|
||||||
schedule={schedule}
|
key={schedule.id}
|
||||||
isDeletable={schedules.length !== 1}
|
schedule={schedule}
|
||||||
updateDefault={updateMutation.mutate}
|
isDeletable={schedules.length !== 1}
|
||||||
deleteFunction={deleteMutation.mutate}
|
updateDefault={updateMutation.mutate}
|
||||||
duplicateFunction={duplicateMutation.mutate}
|
deleteFunction={deleteMutation.mutate}
|
||||||
/>
|
duplicateFunction={duplicateMutation.mutate}
|
||||||
))}
|
/>
|
||||||
</ul>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block">
|
||||||
|
{t("temporarily_out_of_office")}{" "}
|
||||||
|
<Link href="settings/my-account/out-of-office" className="underline">
|
||||||
|
{t("add_a_redirect")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { createEvent } from "ics";
|
import { createEvent } from "ics";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
@ -22,35 +23,28 @@ import {
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
||||||
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
||||||
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
|
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||||
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import {
|
import {
|
||||||
formatToLocalizedDate,
|
formatToLocalizedDate,
|
||||||
formatToLocalizedTime,
|
formatToLocalizedTime,
|
||||||
formatToLocalizedTimezone,
|
formatToLocalizedTimezone,
|
||||||
} from "@calcom/lib/date-fns";
|
} from "@calcom/lib/date-fns";
|
||||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
|
||||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
|
||||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import { bookingMetadataSchema, customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Alert, Badge, Button, EmailInput, HeadSeo, useCalcomTheme } from "@calcom/ui";
|
import { Alert, Badge, Button, EmailInput, HeadSeo, useCalcomTheme } from "@calcom/ui";
|
||||||
import { AlertCircle, Calendar, Check, ChevronLeft, ExternalLink, X } from "@calcom/ui/components/icon";
|
import { AlertCircle, Calendar, Check, ChevronLeft, ExternalLink, X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
@ -58,23 +52,7 @@ import PageWrapper from "@components/PageWrapper";
|
||||||
import CancelBooking from "@components/booking/CancelBooking";
|
import CancelBooking from "@components/booking/CancelBooking";
|
||||||
import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
export { getServerSideProps };
|
||||||
|
|
||||||
const useBrandColors = ({
|
|
||||||
brandColor,
|
|
||||||
darkBrandColor,
|
|
||||||
}: {
|
|
||||||
brandColor?: string | null;
|
|
||||||
darkBrandColor?: string | null;
|
|
||||||
}) => {
|
|
||||||
const brandTheme = useGetBrandingColours({
|
|
||||||
lightVal: brandColor,
|
|
||||||
darkVal: darkBrandColor,
|
|
||||||
});
|
|
||||||
useCalcomTheme(brandTheme);
|
|
||||||
};
|
|
||||||
|
|
||||||
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
|
|
||||||
|
|
||||||
const stringToBoolean = z
|
const stringToBoolean = z
|
||||||
.string()
|
.string()
|
||||||
|
@ -94,6 +72,22 @@ const querySchema = z.object({
|
||||||
seatReferenceUid: z.string().optional(),
|
seatReferenceUid: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const useBrandColors = ({
|
||||||
|
brandColor,
|
||||||
|
darkBrandColor,
|
||||||
|
}: {
|
||||||
|
brandColor?: string | null;
|
||||||
|
darkBrandColor?: string | null;
|
||||||
|
}) => {
|
||||||
|
const brandTheme = useGetBrandingColours({
|
||||||
|
lightVal: brandColor,
|
||||||
|
darkVal: darkBrandColor,
|
||||||
|
});
|
||||||
|
useCalcomTheme(brandTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function Success(props: SuccessProps) {
|
export default function Success(props: SuccessProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -925,329 +919,3 @@ export function RecurringBookings({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEventTypesFromDB = async (id: number) => {
|
|
||||||
const userSelect = {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
username: true,
|
|
||||||
hideBranding: true,
|
|
||||||
theme: true,
|
|
||||||
brandColor: true,
|
|
||||||
darkBrandColor: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
};
|
|
||||||
const eventType = await prisma.eventType.findUnique({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
length: true,
|
|
||||||
eventName: true,
|
|
||||||
recurringEvent: true,
|
|
||||||
requiresConfirmation: true,
|
|
||||||
userId: true,
|
|
||||||
successRedirectUrl: true,
|
|
||||||
customInputs: true,
|
|
||||||
locations: true,
|
|
||||||
price: true,
|
|
||||||
currency: true,
|
|
||||||
bookingFields: true,
|
|
||||||
disableGuests: true,
|
|
||||||
timeZone: true,
|
|
||||||
owner: {
|
|
||||||
select: userSelect,
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
select: userSelect,
|
|
||||||
},
|
|
||||||
hosts: {
|
|
||||||
select: {
|
|
||||||
user: {
|
|
||||||
select: userSelect,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
slug: true,
|
|
||||||
name: true,
|
|
||||||
hideBranding: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workflows: {
|
|
||||||
select: {
|
|
||||||
workflow: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
steps: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: true,
|
|
||||||
seatsPerTimeSlot: true,
|
|
||||||
seatsShowAttendees: true,
|
|
||||||
seatsShowAvailabilityCount: true,
|
|
||||||
periodStartDate: true,
|
|
||||||
periodEndDate: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!eventType) {
|
|
||||||
return eventType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDynamic: false,
|
|
||||||
...eventType,
|
|
||||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeatsEventTypeOnBooking = async (
|
|
||||||
eventType: {
|
|
||||||
seatsPerTimeSlot?: number | null;
|
|
||||||
seatsShowAttendees: boolean | null;
|
|
||||||
seatsShowAvailabilityCount: boolean | null;
|
|
||||||
[x: string | number | symbol]: unknown;
|
|
||||||
},
|
|
||||||
bookingInfo: Partial<
|
|
||||||
Prisma.BookingGetPayload<{
|
|
||||||
include: {
|
|
||||||
attendees: { select: { name: true; email: true } };
|
|
||||||
seatsReferences: { select: { referenceUid: true } };
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
id: true;
|
|
||||||
name: true;
|
|
||||||
email: true;
|
|
||||||
username: true;
|
|
||||||
timeZone: true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>
|
|
||||||
>,
|
|
||||||
seatReferenceUid?: string,
|
|
||||||
userId?: number
|
|
||||||
) => {
|
|
||||||
if (eventType?.seatsPerTimeSlot !== null) {
|
|
||||||
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
|
|
||||||
delete bookingInfo.description;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @TODO: If handling teams, we need to do more check ups for this.
|
|
||||||
if (bookingInfo?.user?.id === userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eventType.seatsShowAttendees) {
|
|
||||||
const seatAttendee = await prisma.bookingSeat.findFirst({
|
|
||||||
where: {
|
|
||||||
referenceUid: seatReferenceUid,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
attendee: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (seatAttendee) {
|
|
||||||
const attendee = bookingInfo?.attendees?.find((a) => {
|
|
||||||
return a.email === seatAttendee.attendee?.email;
|
|
||||||
});
|
|
||||||
bookingInfo["attendees"] = attendee ? [attendee] : [];
|
|
||||||
} else {
|
|
||||||
bookingInfo["attendees"] = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bookingInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
const session = await getServerSession(context);
|
|
||||||
let tz: string | null = null;
|
|
||||||
let userTimeFormat: number | null = null;
|
|
||||||
let requiresLoginToUpdate = false;
|
|
||||||
if (session) {
|
|
||||||
const user = await ssr.viewer.me.fetch();
|
|
||||||
tz = user.timeZone;
|
|
||||||
userTimeFormat = user.timeFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedQuery = querySchema.safeParse(context.query);
|
|
||||||
|
|
||||||
if (!parsedQuery.success) return { notFound: true } as const;
|
|
||||||
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
|
|
||||||
|
|
||||||
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
|
|
||||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
|
||||||
where: {
|
|
||||||
uid: maybeUid,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
id: true,
|
|
||||||
uid: true,
|
|
||||||
description: true,
|
|
||||||
customInputs: true,
|
|
||||||
smsReminderNumber: true,
|
|
||||||
recurringEventId: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
location: true,
|
|
||||||
status: true,
|
|
||||||
metadata: true,
|
|
||||||
cancellationReason: true,
|
|
||||||
responses: true,
|
|
||||||
rejectionReason: true,
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
username: true,
|
|
||||||
timeZone: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attendees: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
eventTypeId: true,
|
|
||||||
eventType: {
|
|
||||||
select: {
|
|
||||||
eventName: true,
|
|
||||||
slug: true,
|
|
||||||
timeZone: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
seatsReferences: {
|
|
||||||
select: {
|
|
||||||
referenceUid: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!bookingInfoRaw) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
} as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventTypeRaw = !bookingInfoRaw.eventTypeId
|
|
||||||
? getDefaultEvent(eventTypeSlug || "")
|
|
||||||
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
|
|
||||||
if (!eventTypeRaw) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
} as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
|
|
||||||
requiresLoginToUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
|
||||||
// probably fixable with json.stringify -> json.parse
|
|
||||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
|
||||||
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
|
||||||
|
|
||||||
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
|
|
||||||
? eventTypeRaw.hosts.map((host) => host.user)
|
|
||||||
: eventTypeRaw.users;
|
|
||||||
|
|
||||||
if (!eventTypeRaw.users.length) {
|
|
||||||
if (!eventTypeRaw.owner)
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
} as const;
|
|
||||||
eventTypeRaw.users.push({
|
|
||||||
...eventTypeRaw.owner,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventType = {
|
|
||||||
...eventTypeRaw,
|
|
||||||
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
|
|
||||||
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
|
||||||
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
|
||||||
};
|
|
||||||
|
|
||||||
const profile = {
|
|
||||||
name: eventType.team?.name || eventType.users[0]?.name || null,
|
|
||||||
email: eventType.team ? null : eventType.users[0].email || null,
|
|
||||||
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
|
|
||||||
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
|
|
||||||
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
|
|
||||||
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
|
|
||||||
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const payment = await prisma.payment.findFirst({
|
|
||||||
where: {
|
|
||||||
bookingId: bookingInfo.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
success: true,
|
|
||||||
refunded: true,
|
|
||||||
currency: true,
|
|
||||||
amount: true,
|
|
||||||
paymentOption: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
|
||||||
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
|
||||||
profile,
|
|
||||||
eventType,
|
|
||||||
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
|
||||||
bookingInfo,
|
|
||||||
paymentStatus: payment,
|
|
||||||
...(tz && { tz }),
|
|
||||||
userTimeFormat,
|
|
||||||
requiresLoginToUpdate,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRecurringBookings(recurringEventId: string | null) {
|
|
||||||
if (!recurringEventId) return null;
|
|
||||||
const recurringBookings = await prisma.booking.findMany({
|
|
||||||
where: {
|
|
||||||
recurringEventId,
|
|
||||||
status: BookingStatus.ACCEPTED,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
startTime: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return recurringBookings.map((obj) => obj.startTime.toString());
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
"use client";
|
||||||
|
|
||||||
import { getServerSideProps as _getServerSideProps } from "../[uid]";
|
import { getServerSideProps as _getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
|
||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
export { default } from "../[uid]";
|
export { default } from "../[uid]";
|
||||||
|
|
||||||
|
|
|
@ -1,74 +1,17 @@
|
||||||
import { getLayout } from "@calcom/features/MainLayout";
|
"use client";
|
||||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
|
||||||
import { UpgradeTip } from "@calcom/features/tips";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Button, ButtonGroup } from "@calcom/ui";
|
|
||||||
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
|
|
||||||
|
import EnterprisePage from "@components/EnterprisePage";
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function EnterprisePage() {
|
const ProxifiedEnterprisePage = new Proxy<{
|
||||||
const { t } = useLocale();
|
(): JSX.Element;
|
||||||
|
PageWrapper?: typeof PageWrapper;
|
||||||
|
getLayout?: typeof getLayout;
|
||||||
|
}>(EnterprisePage, {});
|
||||||
|
|
||||||
const features = [
|
ProxifiedEnterprisePage.PageWrapper = PageWrapper;
|
||||||
{
|
ProxifiedEnterprisePage.getLayout = getLayout;
|
||||||
icon: <Globe className="h-5 w-5 text-red-500" />,
|
|
||||||
title: t("branded_subdomain"),
|
|
||||||
description: t("branded_subdomain_description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <BarChart className="h-5 w-5 text-blue-500" />,
|
|
||||||
title: t("org_insights"),
|
|
||||||
description: t("org_insights_description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
|
|
||||||
title: t("extensive_whitelabeling"),
|
|
||||||
description: t("extensive_whitelabeling_description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Users className="h-5 w-5 text-orange-500" />,
|
|
||||||
title: t("unlimited_teams"),
|
|
||||||
description: t("unlimited_teams_description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <CreditCard className="h-5 w-5 text-green-500" />,
|
|
||||||
title: t("unified_billing"),
|
|
||||||
description: t("unified_billing_description"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Lock className="h-5 w-5 text-purple-500" />,
|
|
||||||
title: t("advanced_managed_events"),
|
|
||||||
description: t("advanced_managed_events_description"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ShellMain heading="Enterprise" subtitle={t("enterprise_description")}>
|
|
||||||
<UpgradeTip
|
|
||||||
plan="enterprise"
|
|
||||||
title={t("create_your_org")}
|
|
||||||
description={t("create_your_org_description")}
|
|
||||||
features={features}
|
|
||||||
background="/tips/enterprise"
|
|
||||||
buttons={
|
|
||||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button color="primary" href="https://i.cal.com/sales/enterprise?duration=25" target="_blank">
|
|
||||||
{t("contact_sales")}
|
|
||||||
</Button>
|
|
||||||
<Button color="minimal" href="https://cal.com/enterprise" target="_blank">
|
|
||||||
{t("learn_more")}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<>Create Org</>
|
|
||||||
</UpgradeTip>
|
|
||||||
</ShellMain>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnterprisePage.PageWrapper = PageWrapper;
|
export default ProxifiedEnterprisePage;
|
||||||
EnterprisePage.getLayout = getLayout;
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { getLayout } from "@calcom/features/MainLayout";
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
|
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// page can be a server component
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { URLSearchParams } from "url";
|
import { URLSearchParams } from "url";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import withEmbedSsr from "@lib/withEmbedSsr";
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
import { getServerSideProps as _getServerSideProps } from "../[uid]";
|
import { getServerSideProps as _getServerSideProps } from "../[uid]";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
@ -98,10 +100,15 @@ const AppearanceView = ({
|
||||||
reset: resetBookerLayoutThemeReset,
|
reset: resetBookerLayoutThemeReset,
|
||||||
} = bookerLayoutFormMethods;
|
} = bookerLayoutFormMethods;
|
||||||
|
|
||||||
|
const DEFAULT_BRAND_COLOURS = {
|
||||||
|
light: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||||
|
dark: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
const brandColorsFormMethods = useForm({
|
const brandColorsFormMethods = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
brandColor: user.brandColor || DEFAULT_LIGHT_BRAND_COLOR,
|
brandColor: DEFAULT_BRAND_COLOURS.light,
|
||||||
darkBrandColor: user.darkBrandColor || DEFAULT_DARK_BRAND_COLOR,
|
darkBrandColor: DEFAULT_BRAND_COLOURS.dark,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -231,12 +238,12 @@ const AppearanceView = ({
|
||||||
<Controller
|
<Controller
|
||||||
name="brandColor"
|
name="brandColor"
|
||||||
control={brandColorsFormMethods.control}
|
control={brandColorsFormMethods.control}
|
||||||
defaultValue={user.brandColor}
|
defaultValue={DEFAULT_BRAND_COLOURS.light}
|
||||||
render={() => (
|
render={() => (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
|
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
defaultValue={user.brandColor}
|
defaultValue={DEFAULT_BRAND_COLOURS.light}
|
||||||
resetDefaultValue={DEFAULT_LIGHT_BRAND_COLOR}
|
resetDefaultValue={DEFAULT_LIGHT_BRAND_COLOR}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
try {
|
try {
|
||||||
|
@ -260,12 +267,12 @@ const AppearanceView = ({
|
||||||
<Controller
|
<Controller
|
||||||
name="darkBrandColor"
|
name="darkBrandColor"
|
||||||
control={brandColorsFormMethods.control}
|
control={brandColorsFormMethods.control}
|
||||||
defaultValue={user.darkBrandColor}
|
defaultValue={DEFAULT_BRAND_COLOURS.dark}
|
||||||
render={() => (
|
render={() => (
|
||||||
<div className="mt-6 sm:mt-0">
|
<div className="mt-6 sm:mt-0">
|
||||||
<p className="text-default mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
|
<p className="text-default mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
defaultValue={user.darkBrandColor}
|
defaultValue={DEFAULT_BRAND_COLOURS.dark}
|
||||||
resetDefaultValue={DEFAULT_DARK_BRAND_COLOR}
|
resetDefaultValue={DEFAULT_DARK_BRAND_COLOR}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useReducer } from "react";
|
import { useReducer } from "react";
|
||||||
|
|
||||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
|
@ -0,0 +1,241 @@
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useForm, useFormState } from "react-hook-form";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
|
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
|
import { Button, Meta, showToast, Select, SkeletonText, UpgradeTeamsBadge, Switch } from "@calcom/ui";
|
||||||
|
import { TableNew, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
import { OutOfOfficeDateRangePicker } from "@components/out-of-office/DateRangePicker";
|
||||||
|
|
||||||
|
export type BookingRedirectForm = {
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
toTeamUserId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OutOfOfficeSection = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
|
const [dateRange, setDateRange] = useState<[Date | null, Date | null, null | null]>([
|
||||||
|
dayjs().startOf("d").toDate(),
|
||||||
|
dayjs().add(1, "d").endOf("d").toDate(),
|
||||||
|
null,
|
||||||
|
]);
|
||||||
|
const [profileRedirect, setProfileRedirect] = useState(false);
|
||||||
|
const [selectedMember, setSelectedMember] = useState<{ label: string; value: number | null } | null>(null);
|
||||||
|
|
||||||
|
const { handleSubmit, setValue } = useForm<BookingRedirectForm>({
|
||||||
|
defaultValues: {
|
||||||
|
startDate: dateRange[0]?.toISOString(),
|
||||||
|
endDate: dateRange[1]?.toISOString(),
|
||||||
|
toTeamUserId: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createOutOfOfficeEntry = trpc.viewer.outOfOfficeCreate.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast(t("success_entry_created"), "success");
|
||||||
|
utils.viewer.outOfOfficeEntriesList.invalidate();
|
||||||
|
setProfileRedirect(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(t(error.message), "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { hasTeamPlan } = useHasTeamPlan();
|
||||||
|
const { data: listMembers } = trpc.viewer.teams.listMembers.useQuery({});
|
||||||
|
const me = useMeQuery();
|
||||||
|
const memberListOptions: {
|
||||||
|
value: number | null;
|
||||||
|
label: string;
|
||||||
|
}[] =
|
||||||
|
listMembers
|
||||||
|
?.filter((member) => me?.data?.id !== member.id)
|
||||||
|
.map((member) => ({
|
||||||
|
value: member.id || null,
|
||||||
|
label: member.name || "",
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data) => {
|
||||||
|
createOutOfOfficeEntry.mutate(data);
|
||||||
|
setValue("toTeamUserId", null);
|
||||||
|
setSelectedMember(null);
|
||||||
|
})}>
|
||||||
|
<div className="border-subtle flex flex-col rounded-b-lg border border-t-0 p-6 px-6 py-8 text-sm">
|
||||||
|
{/* Add startDate and end date inputs */}
|
||||||
|
<div className="border-subtle mt-2 rounded-lg border bg-gray-50 p-6 dark:bg-transparent">
|
||||||
|
{/* Add toggle to enable/disable redirect */}
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Switch
|
||||||
|
disabled={!hasTeamPlan}
|
||||||
|
data-testid="profile-redirect-switch"
|
||||||
|
checked={profileRedirect}
|
||||||
|
id="profile-redirect-switch"
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
setProfileRedirect(state);
|
||||||
|
}}
|
||||||
|
label={hasTeamPlan ? t("redirect_team_enabled") : t("redirect_team_disabled")}
|
||||||
|
/>
|
||||||
|
{!hasTeamPlan && (
|
||||||
|
<div className="mx-2" data-testid="upgrade-team-badge">
|
||||||
|
<UpgradeTeamsBadge />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-row">
|
||||||
|
{profileRedirect && (
|
||||||
|
<div className="mr-2 w-1/2 lg:w-1/3">
|
||||||
|
<p className="text-emphasis block text-sm font-medium">{t("team_member")}</p>
|
||||||
|
<Select
|
||||||
|
className="mt-1 h-4 max-w-[350px] text-white"
|
||||||
|
name="toTeamUsername"
|
||||||
|
data-testid="team_username_select"
|
||||||
|
value={selectedMember}
|
||||||
|
placeholder={t("select_team_member")}
|
||||||
|
isSearchable
|
||||||
|
innerClassNames={{
|
||||||
|
control: "h-[38px]",
|
||||||
|
}}
|
||||||
|
options={memberListOptions}
|
||||||
|
onChange={(selectedOption) => {
|
||||||
|
if (selectedOption?.value) {
|
||||||
|
setSelectedMember(selectedOption);
|
||||||
|
setValue("toTeamUserId", selectedOption?.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="w-1/2 lg:w-1/3">
|
||||||
|
<p className="text-emphasis mb-1 block text-sm font-medium">{t("time_range")}</p>
|
||||||
|
|
||||||
|
<OutOfOfficeDateRangePicker
|
||||||
|
dateRange={dateRange}
|
||||||
|
setValue={setValue}
|
||||||
|
setDateRange={setDateRange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-7">
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={createOutOfOfficeEntry.isLoading}
|
||||||
|
data-testid="create-entry-ooo-redirect">
|
||||||
|
{t("create_entry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OutOfOfficeEntriesList />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OutOfOfficeEntriesList = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { data, isLoading } = trpc.viewer.outOfOfficeEntriesList.useQuery();
|
||||||
|
const deleteOutOfOfficeEntryMutation = trpc.viewer.outOfOfficeEntryDelete.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast(t("success_deleted_entry_out_of_office"), "success");
|
||||||
|
utils.viewer.outOfOfficeEntriesList.invalidate();
|
||||||
|
useFormState;
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast(`An error ocurred`, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (data === null || data?.length === 0 || data === undefined) return null;
|
||||||
|
return (
|
||||||
|
<div className="border-subtle mt-6 rounded-lg border">
|
||||||
|
<TableNew className="border-0">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="rounded-tl-lg font-normal capitalize">{t("time_range")}</TableHead>
|
||||||
|
<TableHead className="font-normal">{t("username")}</TableHead>
|
||||||
|
|
||||||
|
<TableHead className="rounded-tr-lg font-normal">{t("action")}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((item) => (
|
||||||
|
<TableRow key={item.id} data-testid={`table-redirect-${item.toUser?.username || "n-a"}`}>
|
||||||
|
<TableCell>
|
||||||
|
<p className="px-2">
|
||||||
|
{dayjs(item.start).format("ll")} - {dayjs(item.end).format("ll")}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<p className="px-2">{item.toUser?.username || "N/A"}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="minimal"
|
||||||
|
variant="icon"
|
||||||
|
disabled={deleteOutOfOfficeEntryMutation.isLoading}
|
||||||
|
StartIcon={Trash2}
|
||||||
|
onClick={() => {
|
||||||
|
deleteOutOfOfficeEntryMutation.mutate({ outOfOfficeUid: item.uuid });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{isLoading && (
|
||||||
|
<TableRow>
|
||||||
|
{new Array(6).fill(0).map((_, index) => (
|
||||||
|
<TableCell key={index}>
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && (data === undefined || data.length === 0) && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="text-center">
|
||||||
|
<p className="text-subtle text-sm">{t("no_redirects_found")}</p>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</TableNew>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OutOfOfficePage = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title={t("out_of_office")} description={t("out_of_office_description")} borderInShellHeader />
|
||||||
|
<ShellMain>
|
||||||
|
<OutOfOfficeSection />
|
||||||
|
</ShellMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
OutOfOfficePage.getLayout = getLayout;
|
||||||
|
OutOfOfficePage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
|
export default OutOfOfficePage;
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import type { BaseSyntheticEvent } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { randomString } from "@calcom/lib/random";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
|
test.describe("Out of office", () => {
|
||||||
|
test("User can create out of office entry", async ({ page, users }) => {
|
||||||
|
const user = await users.create({ name: "userOne" });
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/settings/my-account/out-of-office");
|
||||||
|
|
||||||
|
await page.locator("data-testid=create-entry-ooo-redirect").click();
|
||||||
|
|
||||||
|
await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("User can configure booking redirect", async ({ page, users }) => {
|
||||||
|
const user = await users.create({ name: "userOne" });
|
||||||
|
const userTo = await users.create({ name: "userTwo" });
|
||||||
|
|
||||||
|
const team = await prisma.team.create({
|
||||||
|
data: {
|
||||||
|
name: "test-insights",
|
||||||
|
slug: `test-insights-${Date.now()}-${randomString(5)}}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// create memberships
|
||||||
|
await prisma.membership.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
accepted: true,
|
||||||
|
role: "ADMIN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: userTo.id,
|
||||||
|
teamId: team.id,
|
||||||
|
accepted: true,
|
||||||
|
role: "ADMIN",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto(`/settings/my-account/out-of-office`);
|
||||||
|
|
||||||
|
await page.getByTestId("profile-redirect-switch").click();
|
||||||
|
await page
|
||||||
|
.getByTestId("team_username_select")
|
||||||
|
.locator("div")
|
||||||
|
.filter({ hasText: "Select team member" })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await page.locator("#react-select-2-option-0 div").click();
|
||||||
|
|
||||||
|
// send request
|
||||||
|
await page.getByTestId("create-entry-ooo-redirect").click();
|
||||||
|
|
||||||
|
// expect table-redirect-toUserId to be visible
|
||||||
|
await expect(page.locator(`data-testid=table-redirect-${userTo.username}`)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Profile redirection", async ({ page, users }) => {
|
||||||
|
const user = await users.create({ name: "userOne" });
|
||||||
|
const userTo = await users.create({ name: "userTwo" });
|
||||||
|
const uuid = uuidv4();
|
||||||
|
await prisma.outOfOfficeEntry.create({
|
||||||
|
data: {
|
||||||
|
start: dayjs().startOf("day").toDate(),
|
||||||
|
end: dayjs().startOf("day").add(1, "w").toDate(),
|
||||||
|
uuid,
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
toUser: { connect: { id: userTo.id } },
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}`);
|
||||||
|
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
// regex to match username
|
||||||
|
expect(page.url()).toMatch(new RegExp(`/${userTo.username}`));
|
||||||
|
|
||||||
|
await page.goto(`/${userTo.username}/30-min`);
|
||||||
|
|
||||||
|
expect(page.url()).toMatch(new RegExp(`/${userTo.username}/30-min`));
|
||||||
|
});
|
||||||
|
});
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "اسم نوع الحدث",
|
"event_name_info": "اسم نوع الحدث",
|
||||||
"event_date_info": "تاريخ الحدث",
|
"event_date_info": "تاريخ الحدث",
|
||||||
"event_time_info": "وقت بدء الحدث",
|
"event_time_info": "وقت بدء الحدث",
|
||||||
|
"location_variable": "الموقع",
|
||||||
"location_info": "موقع الحدث",
|
"location_info": "موقع الحدث",
|
||||||
|
"additional_notes_variable": "ملاحظات إضافية",
|
||||||
"additional_notes_info": "ملاحظات إضافية للحجز",
|
"additional_notes_info": "ملاحظات إضافية للحجز",
|
||||||
"attendee_name_info": "اسم الشخص صاحب الحجز",
|
"attendee_name_info": "اسم الشخص صاحب الحجز",
|
||||||
"organizer_name_info": "اسم المنظم",
|
"organizer_name_info": "اسم المنظم",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "رمز التحقق",
|
"verification_code": "رمز التحقق",
|
||||||
"can_you_try_again": "هل يمكنك المحاولة مرة أخرى في وقت مختلف؟",
|
"can_you_try_again": "هل يمكنك المحاولة مرة أخرى في وقت مختلف؟",
|
||||||
"verify": "التحقق",
|
"verify": "التحقق",
|
||||||
|
"timezone_variable": "المنطقة الزمنية",
|
||||||
"timezone_info": "المنطقة الزمنية للشخص الذي يتلقى الحجز",
|
"timezone_info": "المنطقة الزمنية للشخص الذي يتلقى الحجز",
|
||||||
"event_end_time_variable": "وقت انتهاء الحدث",
|
"event_end_time_variable": "وقت انتهاء الحدث",
|
||||||
"event_end_time_info": "وقت نهاية الحدث",
|
"event_end_time_info": "وقت نهاية الحدث",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Název typu události",
|
"event_name_info": "Název typu události",
|
||||||
"event_date_info": "Datum události",
|
"event_date_info": "Datum události",
|
||||||
"event_time_info": "Čas začátku události",
|
"event_time_info": "Čas začátku události",
|
||||||
|
"location_variable": "Místo",
|
||||||
"location_info": "Místo události",
|
"location_info": "Místo události",
|
||||||
|
"additional_notes_variable": "Doplňující poznámky",
|
||||||
"additional_notes_info": "Další poznámky k rezervaci",
|
"additional_notes_info": "Další poznámky k rezervaci",
|
||||||
"attendee_name_info": "Jméno osoby provádějící rezervaci",
|
"attendee_name_info": "Jméno osoby provádějící rezervaci",
|
||||||
"organizer_name_info": "Jméno organizátora",
|
"organizer_name_info": "Jméno organizátora",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Ověřovací kód",
|
"verification_code": "Ověřovací kód",
|
||||||
"can_you_try_again": "Můžete to zkusit znovu a použít jiný čas?",
|
"can_you_try_again": "Můžete to zkusit znovu a použít jiný čas?",
|
||||||
"verify": "Ověřit",
|
"verify": "Ověřit",
|
||||||
|
"timezone_variable": "Časová zóna",
|
||||||
"timezone_info": "Časové pásmo přijímající osoby",
|
"timezone_info": "Časové pásmo přijímající osoby",
|
||||||
"event_end_time_variable": "Čas ukončení události",
|
"event_end_time_variable": "Čas ukončení události",
|
||||||
"event_end_time_info": "Čas ukončení události",
|
"event_end_time_info": "Čas ukončení události",
|
||||||
|
|
|
@ -1175,7 +1175,9 @@
|
||||||
"event_name_info": "Navn på begivenhedstypen",
|
"event_name_info": "Navn på begivenhedstypen",
|
||||||
"event_date_info": "Dato for begivenheden",
|
"event_date_info": "Dato for begivenheden",
|
||||||
"event_time_info": "Starttidspunkt for begivenheden",
|
"event_time_info": "Starttidspunkt for begivenheden",
|
||||||
|
"location_variable": "Placering",
|
||||||
"location_info": "Begivenhedens placering",
|
"location_info": "Begivenhedens placering",
|
||||||
|
"additional_notes_variable": "Yderligere bemærkninger",
|
||||||
"additional_notes_info": "De yderligere noter til booking",
|
"additional_notes_info": "De yderligere noter til booking",
|
||||||
"attendee_name_info": "Navn på personen der booker",
|
"attendee_name_info": "Navn på personen der booker",
|
||||||
"to": "Til",
|
"to": "Til",
|
||||||
|
@ -1529,6 +1531,7 @@
|
||||||
"this_will_be_the_placeholder": "Dette vil være pladsholderen",
|
"this_will_be_the_placeholder": "Dette vil være pladsholderen",
|
||||||
"verification_code": "Bekræftelseskode",
|
"verification_code": "Bekræftelseskode",
|
||||||
"verify": "Bekræft",
|
"verify": "Bekræft",
|
||||||
|
"timezone_variable": "Tidszone",
|
||||||
"confirm_your_details": "Bekræft dine oplysninger",
|
"confirm_your_details": "Bekræft dine oplysninger",
|
||||||
"overlay_my_calendar": "Vis min kalender",
|
"overlay_my_calendar": "Vis min kalender",
|
||||||
"need_help": "Brug for hjælp?"
|
"need_help": "Brug for hjælp?"
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Name des Ereignistyps",
|
"event_name_info": "Name des Ereignistyps",
|
||||||
"event_date_info": "Das Datum der Veranstaltung",
|
"event_date_info": "Das Datum der Veranstaltung",
|
||||||
"event_time_info": "Die Startzeit des Termins",
|
"event_time_info": "Die Startzeit des Termins",
|
||||||
|
"location_variable": "Ort",
|
||||||
"location_info": "Der Ort des Events",
|
"location_info": "Der Ort des Events",
|
||||||
|
"additional_notes_variable": "Zusätzliche Notizen",
|
||||||
"additional_notes_info": "Die zusätzlichen Anmerkungen der Buchung",
|
"additional_notes_info": "Die zusätzlichen Anmerkungen der Buchung",
|
||||||
"attendee_name_info": "Name der buchenden Person",
|
"attendee_name_info": "Name der buchenden Person",
|
||||||
"organizer_name_info": "Name des Organisators",
|
"organizer_name_info": "Name des Organisators",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Bestätigungscode",
|
"verification_code": "Bestätigungscode",
|
||||||
"can_you_try_again": "Können Sie es zu einem anderen Zeitpunkt erneut versuchen?",
|
"can_you_try_again": "Können Sie es zu einem anderen Zeitpunkt erneut versuchen?",
|
||||||
"verify": "Bestätigen",
|
"verify": "Bestätigen",
|
||||||
|
"timezone_variable": "Zeitzone",
|
||||||
"timezone_info": "Die Zeitzone der empfangenden Person",
|
"timezone_info": "Die Zeitzone der empfangenden Person",
|
||||||
"event_end_time_variable": "Endzeitpunkt des Termins",
|
"event_end_time_variable": "Endzeitpunkt des Termins",
|
||||||
"event_end_time_info": "Der Endzeitpunkt des Termins",
|
"event_end_time_info": "Der Endzeitpunkt des Termins",
|
||||||
|
|
|
@ -275,5 +275,7 @@
|
||||||
"label": "Ετικέτα",
|
"label": "Ετικέτα",
|
||||||
"edit": "Επεξεργασία",
|
"edit": "Επεξεργασία",
|
||||||
"disable_guests": "Απενεργοποίηση επισκεπτών",
|
"disable_guests": "Απενεργοποίηση επισκεπτών",
|
||||||
|
"location_variable": "Τοποθεσία",
|
||||||
|
"additional_notes_variable": "Πρόσθετες σημειώσεις",
|
||||||
"already_have_account": "Έχετε ήδη λογαριασμό;"
|
"already_have_account": "Έχετε ήδη λογαριασμό;"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1387,7 +1387,9 @@
|
||||||
"event_date_info": "The event date",
|
"event_date_info": "The event date",
|
||||||
"event_time_info": "The event start time",
|
"event_time_info": "The event start time",
|
||||||
"event_type_not_found": "EventType not Found",
|
"event_type_not_found": "EventType not Found",
|
||||||
|
"location_variable": "Location",
|
||||||
"location_info": "The location of the event",
|
"location_info": "The location of the event",
|
||||||
|
"additional_notes_variable": "Additional notes",
|
||||||
"additional_notes_info": "The additional notes of booking",
|
"additional_notes_info": "The additional notes of booking",
|
||||||
"attendee_name_info": "The person booking's name",
|
"attendee_name_info": "The person booking's name",
|
||||||
"organizer_name_info": "Organizer’s name",
|
"organizer_name_info": "Organizer’s name",
|
||||||
|
@ -1848,6 +1850,7 @@
|
||||||
"verification_code": "Verification code",
|
"verification_code": "Verification code",
|
||||||
"can_you_try_again": "Can you try again with a different time?",
|
"can_you_try_again": "Can you try again with a different time?",
|
||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
|
"timezone_variable": "Timezone",
|
||||||
"timezone_info": "The timezone of the person receiving",
|
"timezone_info": "The timezone of the person receiving",
|
||||||
"event_end_time_variable": "Event end time",
|
"event_end_time_variable": "Event end time",
|
||||||
"event_end_time_info": "The event end time",
|
"event_end_time_info": "The event end time",
|
||||||
|
@ -2148,7 +2151,6 @@
|
||||||
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
|
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
|
||||||
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
|
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
|
||||||
"join_event_location":"Join {{eventLocationType}}",
|
"join_event_location":"Join {{eventLocationType}}",
|
||||||
"join_meeting":"Join Meeting",
|
|
||||||
"troubleshooting":"Troubleshooting",
|
"troubleshooting":"Troubleshooting",
|
||||||
"calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
|
"calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
|
||||||
"availabilty_schedules":"Availability schedules",
|
"availabilty_schedules":"Availability schedules",
|
||||||
|
@ -2190,5 +2192,35 @@
|
||||||
"uprade_to_create_instant_bookings": "Upgrade to Enterprise and let guests join an instant call that attendees can jump straight into. This is only for team event types",
|
"uprade_to_create_instant_bookings": "Upgrade to Enterprise and let guests join an instant call that attendees can jump straight into. This is only for team event types",
|
||||||
"dont_want_to_wait": "Don't want to wait?",
|
"dont_want_to_wait": "Don't want to wait?",
|
||||||
"meeting_started": "Meeting Started",
|
"meeting_started": "Meeting Started",
|
||||||
|
"user_redirect_title": "{{username}} is currently away for a brief period of time.",
|
||||||
|
"user_redirect_description": "In the meantime, {{profile.username}} will be in charge of all the new scheduled meetings on behalf of {{username}}.",
|
||||||
|
"out_of_office": "Out of office",
|
||||||
|
"out_of_office_description": "Configure actions in your profile while you are away.",
|
||||||
|
"send_request": "Send request",
|
||||||
|
"start_date_and_end_date_required": "Start date and end date are required",
|
||||||
|
"start_date_must_be_before_end_date": "Start date must be before end date",
|
||||||
|
"start_date_must_be_in_the_future": "Start date must be in the future",
|
||||||
|
"user_not_found": "User not found",
|
||||||
|
"out_of_office_entry_already_exists": "Out of office entry already exists",
|
||||||
|
"out_of_office_id_required": "Out of office entry id required",
|
||||||
|
"booking_redirect_infinite_not_allowed": "There is already a booking redirect from that user to you.",
|
||||||
|
"success_entry_created": "Successfully created a new entry",
|
||||||
|
"booking_redirect_email_subject": "Booking redirect notification",
|
||||||
|
"booking_redirect_email_title": "Booking Redirect Notification",
|
||||||
|
"booking_redirect_email_description": "You have received a booking redirection from {{toName}} so their profile links will be redirect to yours for the time interval: ",
|
||||||
|
"success_accept_booking_redirect":"You have accepted this booking redirect request.",
|
||||||
|
"success_reject_booking_redirect":"You have rejected this booking redirect request.",
|
||||||
|
"copy_link_booking_redirect_request": "Copy link to share request",
|
||||||
|
"booking_redirect_request_title": "Booking Redirect Request",
|
||||||
|
"select_team_member": "Select team member",
|
||||||
|
"going_away_title": "Going away? Simply mark your profile link as unavailable for a period of time.",
|
||||||
|
"redirect_team_enabled": "Redirect your profile to another team member",
|
||||||
|
"redirect_team_disabled": "Redirect your profile to another team member (Team plan required)",
|
||||||
|
"out_of_office_unavailable_list": "Out of office unavailability list",
|
||||||
|
"success_deleted_entry_out_of_office": "Successfully deleted entry",
|
||||||
|
"temporarily_out_of_office":"Temporarily Out-Of-Office?",
|
||||||
|
"add_a_redirect": "Add a redirect",
|
||||||
|
"create_entry": "Create entry",
|
||||||
|
"time_range": "Time range",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1351,7 +1351,9 @@
|
||||||
"event_name_info": "Nombre del tipo de evento",
|
"event_name_info": "Nombre del tipo de evento",
|
||||||
"event_date_info": "Fecha del evento",
|
"event_date_info": "Fecha del evento",
|
||||||
"event_time_info": "Hora de inicio del evento",
|
"event_time_info": "Hora de inicio del evento",
|
||||||
|
"location_variable": "Lugar",
|
||||||
"location_info": "Ubicación del evento",
|
"location_info": "Ubicación del evento",
|
||||||
|
"additional_notes_variable": "Notas Adicionales",
|
||||||
"additional_notes_info": "Notas adicionales de la reserva",
|
"additional_notes_info": "Notas adicionales de la reserva",
|
||||||
"attendee_name_info": "Nombre de la persona que reserva",
|
"attendee_name_info": "Nombre de la persona que reserva",
|
||||||
"organizer_name_info": "Nombre del organizador",
|
"organizer_name_info": "Nombre del organizador",
|
||||||
|
@ -1802,6 +1804,7 @@
|
||||||
"verification_code": "Código de verificación",
|
"verification_code": "Código de verificación",
|
||||||
"can_you_try_again": "¿Puede intentarlo de nuevo con una hora diferente?",
|
"can_you_try_again": "¿Puede intentarlo de nuevo con una hora diferente?",
|
||||||
"verify": "Verificar",
|
"verify": "Verificar",
|
||||||
|
"timezone_variable": "Zona Horaria",
|
||||||
"timezone_info": "Zona horaria de la persona que recibe",
|
"timezone_info": "Zona horaria de la persona que recibe",
|
||||||
"event_end_time_variable": "Hora de finalización del evento",
|
"event_end_time_variable": "Hora de finalización del evento",
|
||||||
"event_end_time_info": "La hora de finalización del evento",
|
"event_end_time_info": "La hora de finalización del evento",
|
||||||
|
|
|
@ -697,7 +697,9 @@
|
||||||
"event_name_info": "Gertaeraren motaren izena",
|
"event_name_info": "Gertaeraren motaren izena",
|
||||||
"event_date_info": "Gertaeraren data",
|
"event_date_info": "Gertaeraren data",
|
||||||
"event_time_info": "Gertaeraren hasiera-ordua",
|
"event_time_info": "Gertaeraren hasiera-ordua",
|
||||||
|
"location_variable": "Kokapena",
|
||||||
"location_info": "Gertaeraren kokapena",
|
"location_info": "Gertaeraren kokapena",
|
||||||
|
"additional_notes_variable": "Ohar gehigarriak",
|
||||||
"additional_notes_info": "Erreserbaren ohar gehigarriak",
|
"additional_notes_info": "Erreserbaren ohar gehigarriak",
|
||||||
"organizer_name_info": "Antolatzailearen izena",
|
"organizer_name_info": "Antolatzailearen izena",
|
||||||
"download_responses": "Deskargatu erantzunak",
|
"download_responses": "Deskargatu erantzunak",
|
||||||
|
@ -757,5 +759,6 @@
|
||||||
"options": "Aukerak",
|
"options": "Aukerak",
|
||||||
"add_an_option": "Gehitu aukera bat",
|
"add_an_option": "Gehitu aukera bat",
|
||||||
"radio": "Irratia",
|
"radio": "Irratia",
|
||||||
"all_bookings_filter_label": "Erreserba guztiak"
|
"all_bookings_filter_label": "Erreserba guztiak",
|
||||||
|
"timezone_variable": "Ordu-eremua"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1358,7 +1358,9 @@
|
||||||
"event_name_info": "Nom du type d'événement",
|
"event_name_info": "Nom du type d'événement",
|
||||||
"event_date_info": "Date de l'événement",
|
"event_date_info": "Date de l'événement",
|
||||||
"event_time_info": "Heure de début de l'événement",
|
"event_time_info": "Heure de début de l'événement",
|
||||||
|
"location_variable": "Lieu",
|
||||||
"location_info": "Lieu de l'événement",
|
"location_info": "Lieu de l'événement",
|
||||||
|
"additional_notes_variable": "Notes supplémentaires",
|
||||||
"additional_notes_info": "Notes supplémentaires de la réservation",
|
"additional_notes_info": "Notes supplémentaires de la réservation",
|
||||||
"attendee_name_info": "Nom du participant",
|
"attendee_name_info": "Nom du participant",
|
||||||
"organizer_name_info": "Nom de l'organisateur",
|
"organizer_name_info": "Nom de l'organisateur",
|
||||||
|
@ -1808,6 +1810,7 @@
|
||||||
"verification_code": "Code de vérification",
|
"verification_code": "Code de vérification",
|
||||||
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
|
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
|
||||||
"verify": "Vérifier",
|
"verify": "Vérifier",
|
||||||
|
"timezone_variable": "Fuseau horaire",
|
||||||
"timezone_info": "Le fuseau horaire du destinataire",
|
"timezone_info": "Le fuseau horaire du destinataire",
|
||||||
"event_end_time_variable": "Heure de fin de l'événement",
|
"event_end_time_variable": "Heure de fin de l'événement",
|
||||||
"event_end_time_info": "L'heure de fin de l'événement",
|
"event_end_time_info": "L'heure de fin de l'événement",
|
||||||
|
|
|
@ -1385,7 +1385,9 @@
|
||||||
"event_date_info": "תאריך האירוע",
|
"event_date_info": "תאריך האירוע",
|
||||||
"event_time_info": "שעת ההתחלה של האירוע",
|
"event_time_info": "שעת ההתחלה של האירוע",
|
||||||
"event_type_not_found": "EventType לא נמצא",
|
"event_type_not_found": "EventType לא נמצא",
|
||||||
|
"location_variable": "מיקום",
|
||||||
"location_info": "מיקום האירוע",
|
"location_info": "מיקום האירוע",
|
||||||
|
"additional_notes_variable": "הערות נוספות",
|
||||||
"additional_notes_info": "הערות נוספות להזמנה",
|
"additional_notes_info": "הערות נוספות להזמנה",
|
||||||
"attendee_name_info": "שם האדם שביצע את ההזמנה",
|
"attendee_name_info": "שם האדם שביצע את ההזמנה",
|
||||||
"organizer_name_info": "שם המארגן/ת",
|
"organizer_name_info": "שם המארגן/ת",
|
||||||
|
@ -1845,6 +1847,7 @@
|
||||||
"verification_code": "קוד אימות",
|
"verification_code": "קוד אימות",
|
||||||
"can_you_try_again": "אתה יכול לנסות שוב עם שעה אחרת?",
|
"can_you_try_again": "אתה יכול לנסות שוב עם שעה אחרת?",
|
||||||
"verify": "אמת",
|
"verify": "אמת",
|
||||||
|
"timezone_variable": "אזור זמן",
|
||||||
"timezone_info": "אזור הזמן של האדם שיקבל את ההזמנה",
|
"timezone_info": "אזור הזמן של האדם שיקבל את ההזמנה",
|
||||||
"event_end_time_variable": "שעת סיום האירוע",
|
"event_end_time_variable": "שעת סיום האירוע",
|
||||||
"event_end_time_info": "שעת הסיום של האירוע",
|
"event_end_time_info": "שעת הסיום של האירוע",
|
||||||
|
@ -2149,6 +2152,7 @@
|
||||||
"manage_calendars": "ניהול לוחות שנה",
|
"manage_calendars": "ניהול לוחות שנה",
|
||||||
"lock_timezone_toggle_on_booking_page": "נעילת אזור הזמן בדף ההזמנות",
|
"lock_timezone_toggle_on_booking_page": "נעילת אזור הזמן בדף ההזמנות",
|
||||||
"description_lock_timezone_toggle_on_booking_page": "כדי לנעול את אזור הזמן בדף ההזמנות – שימושי לאירועים אישיים.",
|
"description_lock_timezone_toggle_on_booking_page": "כדי לנעול את אזור הזמן בדף ההזמנות – שימושי לאירועים אישיים.",
|
||||||
|
"number_in_international_format": "נא למלא מספר בתבנית בינלאומית.",
|
||||||
"install_calendar": "התקנת לוח שנה",
|
"install_calendar": "התקנת לוח שנה",
|
||||||
"branded_subdomain": "תת־תחום ממותג",
|
"branded_subdomain": "תת־תחום ממותג",
|
||||||
"branded_subdomain_description": "קבלת תת־תחום ממותג משלך, כגון acme.cal.com",
|
"branded_subdomain_description": "קבלת תת־תחום ממותג משלך, כגון acme.cal.com",
|
||||||
|
@ -2177,5 +2181,27 @@
|
||||||
"uprade_to_create_instant_bookings": "ניתן לשדג לרישיון התאגידי ולאפשר למשתמשים להצטרף לשיחה מיידית שמשתתפים יכולים לקפוץ ישירות אליה. זה מיועד רק לסוגי אירועים של צוותים",
|
"uprade_to_create_instant_bookings": "ניתן לשדג לרישיון התאגידי ולאפשר למשתמשים להצטרף לשיחה מיידית שמשתתפים יכולים לקפוץ ישירות אליה. זה מיועד רק לסוגי אירועים של צוותים",
|
||||||
"dont_want_to_wait": "לא רוצה להמתין?",
|
"dont_want_to_wait": "לא רוצה להמתין?",
|
||||||
"meeting_started": "הפגישה החלה",
|
"meeting_started": "הפגישה החלה",
|
||||||
|
"out_of_office": "מחוץ למשרד",
|
||||||
|
"out_of_office_description": "הגדרת פעולות בפרופיל שלך כשאינך במשרד.",
|
||||||
|
"send_request": "שליחת בקשה",
|
||||||
|
"start_date_and_end_date_required": "צריך תאריכי התחלה וסיום",
|
||||||
|
"start_date_must_be_before_end_date": "תאריך ההתחלה חייב להיות לפני תאריך הסיום",
|
||||||
|
"start_date_must_be_in_the_future": "תאריך ההתחלה חייב להיות בעתיד",
|
||||||
|
"user_not_found": "המשתמש לא נמצא",
|
||||||
|
"out_of_office_entry_already_exists": "רשומת מחוץ למשרד כבר קיימת",
|
||||||
|
"out_of_office_id_required": "צריך מזהה רשומת מחוץ למשרד",
|
||||||
|
"booking_redirect_infinite_not_allowed": "כבר יש הפניית הזמנה מהמשתמש הזה אליך.",
|
||||||
|
"success_entry_created": "רשומה חדשה נוצרה בהצלחה",
|
||||||
|
"booking_redirect_email_subject": "התראת הפניית הזמנה",
|
||||||
|
"booking_redirect_email_title": "התראת הפניית הזמנה",
|
||||||
|
"copy_link_booking_redirect_request": "העתקת קישור לבקשת שיתוף",
|
||||||
|
"booking_redirect_request_title": "בקשת הפניית הזמנות",
|
||||||
|
"select_team_member": "בחירת נציגות מהצוות",
|
||||||
|
"out_of_office_unavailable_list": "רשימת אי־זמינות למחוץ למשרד",
|
||||||
|
"success_deleted_entry_out_of_office": "רשומה נמחקה בהצלחה",
|
||||||
|
"temporarily_out_of_office": "יצאת למשרד באופן זמני?",
|
||||||
|
"add_a_redirect": "הוספת הפנייה",
|
||||||
|
"create_entry": "יצירת רשומה",
|
||||||
|
"time_range": "טווח זמן",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,5 +205,8 @@
|
||||||
"minute_timeUnit": "Perc",
|
"minute_timeUnit": "Perc",
|
||||||
"remove_app": "Alkalmazás eltávolítása",
|
"remove_app": "Alkalmazás eltávolítása",
|
||||||
"yes_remove_app": "Igen, távolítsd el az alkalmazást",
|
"yes_remove_app": "Igen, távolítsd el az alkalmazást",
|
||||||
"web_conference": "Online konferencia"
|
"web_conference": "Online konferencia",
|
||||||
|
"location_variable": "Helyszín",
|
||||||
|
"additional_notes_variable": "Egyéb jegyzetek",
|
||||||
|
"timezone_variable": "Időzóna"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1351,7 +1351,9 @@
|
||||||
"event_name_info": "Nome del tipo di evento",
|
"event_name_info": "Nome del tipo di evento",
|
||||||
"event_date_info": "Data dell'evento",
|
"event_date_info": "Data dell'evento",
|
||||||
"event_time_info": "Ora di inizio dell'evento",
|
"event_time_info": "Ora di inizio dell'evento",
|
||||||
|
"location_variable": "Luogo",
|
||||||
"location_info": "Luogo dell'evento",
|
"location_info": "Luogo dell'evento",
|
||||||
|
"additional_notes_variable": "Note aggiuntive",
|
||||||
"additional_notes_info": "Note aggiuntive sulla prenotazione",
|
"additional_notes_info": "Note aggiuntive sulla prenotazione",
|
||||||
"attendee_name_info": "Nome del partecipante",
|
"attendee_name_info": "Nome del partecipante",
|
||||||
"organizer_name_info": "Nome organizzatore",
|
"organizer_name_info": "Nome organizzatore",
|
||||||
|
@ -1802,6 +1804,7 @@
|
||||||
"verification_code": "Codice di verifica",
|
"verification_code": "Codice di verifica",
|
||||||
"can_you_try_again": "Riprovare specificando un orario differente.",
|
"can_you_try_again": "Riprovare specificando un orario differente.",
|
||||||
"verify": "Verifica",
|
"verify": "Verifica",
|
||||||
|
"timezone_variable": "Timezone",
|
||||||
"timezone_info": "Il fuso orario della persona che riceve la prenotazione",
|
"timezone_info": "Il fuso orario della persona che riceve la prenotazione",
|
||||||
"event_end_time_variable": "Ora di conclusione dell'evento",
|
"event_end_time_variable": "Ora di conclusione dell'evento",
|
||||||
"event_end_time_info": "Ora di conclusione dell'evento",
|
"event_end_time_info": "Ora di conclusione dell'evento",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "イベントの種類の名前",
|
"event_name_info": "イベントの種類の名前",
|
||||||
"event_date_info": "イベントの日付",
|
"event_date_info": "イベントの日付",
|
||||||
"event_time_info": "イベントの開始時刻",
|
"event_time_info": "イベントの開始時刻",
|
||||||
|
"location_variable": "場所",
|
||||||
"location_info": "イベントの場所",
|
"location_info": "イベントの場所",
|
||||||
|
"additional_notes_variable": "備考",
|
||||||
"additional_notes_info": "予約の追加メモ",
|
"additional_notes_info": "予約の追加メモ",
|
||||||
"attendee_name_info": "予約者名",
|
"attendee_name_info": "予約者名",
|
||||||
"organizer_name_info": "主催者名",
|
"organizer_name_info": "主催者名",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "確認コード",
|
"verification_code": "確認コード",
|
||||||
"can_you_try_again": "別の時間帯でもう 1 度お試しください",
|
"can_you_try_again": "別の時間帯でもう 1 度お試しください",
|
||||||
"verify": "確認する",
|
"verify": "確認する",
|
||||||
|
"timezone_variable": "タイムゾーン",
|
||||||
"timezone_info": "受信するユーザーのタイムゾーン",
|
"timezone_info": "受信するユーザーのタイムゾーン",
|
||||||
"event_end_time_variable": "イベントの終了時刻",
|
"event_end_time_variable": "イベントの終了時刻",
|
||||||
"event_end_time_info": "イベントの終了時刻",
|
"event_end_time_info": "イベントの終了時刻",
|
||||||
|
|
|
@ -251,5 +251,6 @@
|
||||||
"invoices": "វិក្កយបត្រ",
|
"invoices": "វិក្កយបត្រ",
|
||||||
"users": "អ្នកប្រើប្រាស់",
|
"users": "អ្នកប្រើប្រាស់",
|
||||||
"user": "អ្នកប្រើប្រាស់",
|
"user": "អ្នកប្រើប្រាស់",
|
||||||
"general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។"
|
"general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។",
|
||||||
|
"timezone_variable": "ល្វែងម៉ោង"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "이벤트 유형 이름",
|
"event_name_info": "이벤트 유형 이름",
|
||||||
"event_date_info": "이벤트 날짜",
|
"event_date_info": "이벤트 날짜",
|
||||||
"event_time_info": "이벤트 시작 시간",
|
"event_time_info": "이벤트 시작 시간",
|
||||||
|
"location_variable": "위치",
|
||||||
"location_info": "이벤트 위치",
|
"location_info": "이벤트 위치",
|
||||||
|
"additional_notes_variable": "추가 참고 사항",
|
||||||
"additional_notes_info": "예약 추가 메모",
|
"additional_notes_info": "예약 추가 메모",
|
||||||
"attendee_name_info": "예약자 이름",
|
"attendee_name_info": "예약자 이름",
|
||||||
"organizer_name_info": "주최자 이름",
|
"organizer_name_info": "주최자 이름",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "인증 코드",
|
"verification_code": "인증 코드",
|
||||||
"can_you_try_again": "다른 시간으로 다시 해보시겠어요?",
|
"can_you_try_again": "다른 시간으로 다시 해보시겠어요?",
|
||||||
"verify": "인증",
|
"verify": "인증",
|
||||||
|
"timezone_variable": "시간대",
|
||||||
"timezone_info": "예약 수신자의 시간대",
|
"timezone_info": "예약 수신자의 시간대",
|
||||||
"event_end_time_variable": "이벤트 종료 시간",
|
"event_end_time_variable": "이벤트 종료 시간",
|
||||||
"event_end_time_info": "이벤트가 종료되는 시간",
|
"event_end_time_info": "이벤트가 종료되는 시간",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Naam van het type gebeurtenis",
|
"event_name_info": "Naam van het type gebeurtenis",
|
||||||
"event_date_info": "De datum van de gebeurtenis",
|
"event_date_info": "De datum van de gebeurtenis",
|
||||||
"event_time_info": "De begintijd van de gebeurtenis",
|
"event_time_info": "De begintijd van de gebeurtenis",
|
||||||
|
"location_variable": "Locatie",
|
||||||
"location_info": "De locatie van de gebeurtenis",
|
"location_info": "De locatie van de gebeurtenis",
|
||||||
|
"additional_notes_variable": "Aanvullende opmerkingen",
|
||||||
"additional_notes_info": "De aanvullende opmerkingen bij de boeking",
|
"additional_notes_info": "De aanvullende opmerkingen bij de boeking",
|
||||||
"attendee_name_info": "De naam van de persoon die boekt",
|
"attendee_name_info": "De naam van de persoon die boekt",
|
||||||
"organizer_name_info": "Naam organisator",
|
"organizer_name_info": "Naam organisator",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Verificatiecode",
|
"verification_code": "Verificatiecode",
|
||||||
"can_you_try_again": "Kunt u het opnieuw proberen met een andere tijd?",
|
"can_you_try_again": "Kunt u het opnieuw proberen met een andere tijd?",
|
||||||
"verify": "Verifiëren",
|
"verify": "Verifiëren",
|
||||||
|
"timezone_variable": "Tijdzone",
|
||||||
"timezone_info": "De tijdzone van de ontvanger",
|
"timezone_info": "De tijdzone van de ontvanger",
|
||||||
"event_end_time_variable": "Eindtijd gebeurtenis",
|
"event_end_time_variable": "Eindtijd gebeurtenis",
|
||||||
"event_end_time_info": "De eindtijd van de gebeurtenis",
|
"event_end_time_info": "De eindtijd van de gebeurtenis",
|
||||||
|
|
|
@ -1157,7 +1157,9 @@
|
||||||
"event_name_info": "Navnet på hendelsestypen",
|
"event_name_info": "Navnet på hendelsestypen",
|
||||||
"event_date_info": "Dato for hendelse",
|
"event_date_info": "Dato for hendelse",
|
||||||
"event_time_info": "Start-tidspunkt for hendelsen",
|
"event_time_info": "Start-tidspunkt for hendelsen",
|
||||||
|
"location_variable": "Sted",
|
||||||
"location_info": "Hendelsessted",
|
"location_info": "Hendelsessted",
|
||||||
|
"additional_notes_variable": "Tilleggsinformasjon",
|
||||||
"additional_notes_info": "Tilleggsinformasjonen om bookingen",
|
"additional_notes_info": "Tilleggsinformasjonen om bookingen",
|
||||||
"attendee_name_info": "Navnet på personen som booker",
|
"attendee_name_info": "Navnet på personen som booker",
|
||||||
"to": "Til",
|
"to": "Til",
|
||||||
|
@ -1385,5 +1387,6 @@
|
||||||
"configure": "Konfigurer",
|
"configure": "Konfigurer",
|
||||||
"sso_configuration": "Enkel Pålogging",
|
"sso_configuration": "Enkel Pålogging",
|
||||||
"booking_confirmation_failed": "Booking-bekreftelse feilet",
|
"booking_confirmation_failed": "Booking-bekreftelse feilet",
|
||||||
|
"timezone_variable": "Tidssone",
|
||||||
"need_help": "Trenger du hjelp?"
|
"need_help": "Trenger du hjelp?"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Nazwa typu wydarzenia",
|
"event_name_info": "Nazwa typu wydarzenia",
|
||||||
"event_date_info": "Data wydarzenia",
|
"event_date_info": "Data wydarzenia",
|
||||||
"event_time_info": "Godzina rozpoczęcia wydarzenia",
|
"event_time_info": "Godzina rozpoczęcia wydarzenia",
|
||||||
|
"location_variable": "Lokalizacja",
|
||||||
"location_info": "Lokalizacja wydarzenia",
|
"location_info": "Lokalizacja wydarzenia",
|
||||||
|
"additional_notes_variable": "Dodatkowe uwagi",
|
||||||
"additional_notes_info": "Dodatkowe uwagi dotyczące rezerwacji",
|
"additional_notes_info": "Dodatkowe uwagi dotyczące rezerwacji",
|
||||||
"attendee_name_info": "Imię i nazwisko osoby rezerwującej",
|
"attendee_name_info": "Imię i nazwisko osoby rezerwującej",
|
||||||
"organizer_name_info": "Nazwa organizatora",
|
"organizer_name_info": "Nazwa organizatora",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Kod weryfikacyjny",
|
"verification_code": "Kod weryfikacyjny",
|
||||||
"can_you_try_again": "Czy możesz spróbować ponownie w innym terminie?",
|
"can_you_try_again": "Czy możesz spróbować ponownie w innym terminie?",
|
||||||
"verify": "Zweryfikuj",
|
"verify": "Zweryfikuj",
|
||||||
|
"timezone_variable": "Strefa Czasowa",
|
||||||
"timezone_info": "Strefa czasowa osoby rezerwowanej",
|
"timezone_info": "Strefa czasowa osoby rezerwowanej",
|
||||||
"event_end_time_variable": "Czas zakończenia wydarzenia",
|
"event_end_time_variable": "Czas zakończenia wydarzenia",
|
||||||
"event_end_time_info": "Czas zakończenia wydarzenia",
|
"event_end_time_info": "Czas zakończenia wydarzenia",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "O nome do tipo de evento",
|
"event_name_info": "O nome do tipo de evento",
|
||||||
"event_date_info": "A data do evento",
|
"event_date_info": "A data do evento",
|
||||||
"event_time_info": "O horário inicial do evento",
|
"event_time_info": "O horário inicial do evento",
|
||||||
|
"location_variable": "Local",
|
||||||
"location_info": "O local do evento",
|
"location_info": "O local do evento",
|
||||||
|
"additional_notes_variable": "Observações adicionais",
|
||||||
"additional_notes_info": "Notas adicionais da reserva",
|
"additional_notes_info": "Notas adicionais da reserva",
|
||||||
"attendee_name_info": "O nome da pessoa que fez a reserva",
|
"attendee_name_info": "O nome da pessoa que fez a reserva",
|
||||||
"organizer_name_info": "Nome do organizador",
|
"organizer_name_info": "Nome do organizador",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Código de verificação",
|
"verification_code": "Código de verificação",
|
||||||
"can_you_try_again": "Poderia tentar novamente em um horário diferente?",
|
"can_you_try_again": "Poderia tentar novamente em um horário diferente?",
|
||||||
"verify": "Verificar",
|
"verify": "Verificar",
|
||||||
|
"timezone_variable": "Fuso Horário",
|
||||||
"timezone_info": "O fuso horário do destinatário",
|
"timezone_info": "O fuso horário do destinatário",
|
||||||
"event_end_time_variable": "Horário de término do evento",
|
"event_end_time_variable": "Horário de término do evento",
|
||||||
"event_end_time_info": "O horário de término do evento",
|
"event_end_time_info": "O horário de término do evento",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "O nome do tipo de evento",
|
"event_name_info": "O nome do tipo de evento",
|
||||||
"event_date_info": "A data do evento",
|
"event_date_info": "A data do evento",
|
||||||
"event_time_info": "A hora de início do evento",
|
"event_time_info": "A hora de início do evento",
|
||||||
|
"location_variable": "Localização",
|
||||||
"location_info": "O local do evento",
|
"location_info": "O local do evento",
|
||||||
|
"additional_notes_variable": "Notas Adicionais",
|
||||||
"additional_notes_info": "As notas adicionais da reserva",
|
"additional_notes_info": "As notas adicionais da reserva",
|
||||||
"attendee_name_info": "O nome do responsável pela reserva",
|
"attendee_name_info": "O nome do responsável pela reserva",
|
||||||
"organizer_name_info": "Nome do organizador",
|
"organizer_name_info": "Nome do organizador",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Código de verificação",
|
"verification_code": "Código de verificação",
|
||||||
"can_you_try_again": "Pode tentar novamente noutra altura?",
|
"can_you_try_again": "Pode tentar novamente noutra altura?",
|
||||||
"verify": "Verificar",
|
"verify": "Verificar",
|
||||||
|
"timezone_variable": "Fuso Horário",
|
||||||
"timezone_info": "O fuso horário do anfitrião",
|
"timezone_info": "O fuso horário do anfitrião",
|
||||||
"event_end_time_variable": "Hora de fim do evento",
|
"event_end_time_variable": "Hora de fim do evento",
|
||||||
"event_end_time_info": "A hora de fim do evento",
|
"event_end_time_info": "A hora de fim do evento",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Denumirea tipului de eveniment",
|
"event_name_info": "Denumirea tipului de eveniment",
|
||||||
"event_date_info": "Data evenimentului",
|
"event_date_info": "Data evenimentului",
|
||||||
"event_time_info": "Ora începerii evenimentului",
|
"event_time_info": "Ora începerii evenimentului",
|
||||||
|
"location_variable": "Loc",
|
||||||
"location_info": "Locul de desfășurare a evenimentului",
|
"location_info": "Locul de desfășurare a evenimentului",
|
||||||
|
"additional_notes_variable": "Note suplimentare",
|
||||||
"additional_notes_info": "Observații suplimentare privind rezervarea",
|
"additional_notes_info": "Observații suplimentare privind rezervarea",
|
||||||
"attendee_name_info": "Numele persoanei care efectuează rezervarea",
|
"attendee_name_info": "Numele persoanei care efectuează rezervarea",
|
||||||
"organizer_name_info": "Numele organizatorului",
|
"organizer_name_info": "Numele organizatorului",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Cod de verificare",
|
"verification_code": "Cod de verificare",
|
||||||
"can_you_try_again": "Puteți încerca din nou cu o oră diferită?",
|
"can_you_try_again": "Puteți încerca din nou cu o oră diferită?",
|
||||||
"verify": "Verificare",
|
"verify": "Verificare",
|
||||||
|
"timezone_variable": "Timezone",
|
||||||
"timezone_info": "Fusul orar al destinatarului",
|
"timezone_info": "Fusul orar al destinatarului",
|
||||||
"event_end_time_variable": "Oră de încheiere eveniment",
|
"event_end_time_variable": "Oră de încheiere eveniment",
|
||||||
"event_end_time_info": "Ora de încheiere a evenimentului",
|
"event_end_time_info": "Ora de încheiere a evenimentului",
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"a_refund_failed": "Возврат не удался",
|
"a_refund_failed": "Возврат не удался",
|
||||||
"awaiting_payment_subject": "Ожидание оплаты: {{title}} на {{date}}",
|
"awaiting_payment_subject": "Ожидание оплаты: {{title}} на {{date}}",
|
||||||
"meeting_awaiting_payment": "Ваша встреча ожидает оплаты",
|
"meeting_awaiting_payment": "Ваша встреча ожидает оплаты",
|
||||||
|
"dark_theme_contrast_error": "Темный цвет темы не проходит проверку на контрастность. Мы рекомендуем вам изменить этот цвет, чтобы ваши кнопки были более заметны.",
|
||||||
"help": "Помощь",
|
"help": "Помощь",
|
||||||
"price": "Цена",
|
"price": "Цена",
|
||||||
"paid": "Оплачено",
|
"paid": "Оплачено",
|
||||||
|
@ -1352,7 +1353,9 @@
|
||||||
"event_name_info": "Название типа события",
|
"event_name_info": "Название типа события",
|
||||||
"event_date_info": "Дата события",
|
"event_date_info": "Дата события",
|
||||||
"event_time_info": "Время начала события",
|
"event_time_info": "Время начала события",
|
||||||
|
"location_variable": "Местоположение",
|
||||||
"location_info": "Место проведения события",
|
"location_info": "Место проведения события",
|
||||||
|
"additional_notes_variable": "Дополнительная информация",
|
||||||
"additional_notes_info": "Дополнительные заметки о бронировании",
|
"additional_notes_info": "Дополнительные заметки о бронировании",
|
||||||
"attendee_name_info": "Участник, на которого оформляется бронирование",
|
"attendee_name_info": "Участник, на которого оформляется бронирование",
|
||||||
"organizer_name_info": "Имя организатора",
|
"organizer_name_info": "Имя организатора",
|
||||||
|
@ -1803,6 +1806,7 @@
|
||||||
"verification_code": "Код подтверждения",
|
"verification_code": "Код подтверждения",
|
||||||
"can_you_try_again": "Попробуйте указать другое время встречи.",
|
"can_you_try_again": "Попробуйте указать другое время встречи.",
|
||||||
"verify": "Подтвердить",
|
"verify": "Подтвердить",
|
||||||
|
"timezone_variable": "Часовой пояс",
|
||||||
"timezone_info": "Часовой пояс получателя",
|
"timezone_info": "Часовой пояс получателя",
|
||||||
"event_end_time_variable": "Время окончания события",
|
"event_end_time_variable": "Время окончания события",
|
||||||
"event_end_time_info": "Время окончания события",
|
"event_end_time_info": "Время окончания события",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Ime tipa događaja",
|
"event_name_info": "Ime tipa događaja",
|
||||||
"event_date_info": "Datum događaja",
|
"event_date_info": "Datum događaja",
|
||||||
"event_time_info": "Vreme početka događaja",
|
"event_time_info": "Vreme početka događaja",
|
||||||
|
"location_variable": "Lokacija",
|
||||||
"location_info": "Lokacija događaja",
|
"location_info": "Lokacija događaja",
|
||||||
|
"additional_notes_variable": "Dodatne beleške",
|
||||||
"additional_notes_info": "Dodatne napomene o rezervaciji",
|
"additional_notes_info": "Dodatne napomene o rezervaciji",
|
||||||
"attendee_name_info": "Ime osobe koja rezerviše",
|
"attendee_name_info": "Ime osobe koja rezerviše",
|
||||||
"organizer_name_info": "Ime organizatora",
|
"organizer_name_info": "Ime organizatora",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Verifikacioni kôd",
|
"verification_code": "Verifikacioni kôd",
|
||||||
"can_you_try_again": "Možete li da pokušate sa drugim terminom?",
|
"can_you_try_again": "Možete li da pokušate sa drugim terminom?",
|
||||||
"verify": "Verifikuj",
|
"verify": "Verifikuj",
|
||||||
|
"timezone_variable": "Vremenska zona",
|
||||||
"timezone_info": "Vremenska zona primaoca",
|
"timezone_info": "Vremenska zona primaoca",
|
||||||
"event_end_time_variable": "Vreme završetka događaja",
|
"event_end_time_variable": "Vreme završetka događaja",
|
||||||
"event_end_time_info": "Vreme završetka događaja",
|
"event_end_time_info": "Vreme završetka događaja",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Namn på händelsetyp",
|
"event_name_info": "Namn på händelsetyp",
|
||||||
"event_date_info": "Datum för händelsen",
|
"event_date_info": "Datum för händelsen",
|
||||||
"event_time_info": "Händelsens starttid",
|
"event_time_info": "Händelsens starttid",
|
||||||
|
"location_variable": "Plats",
|
||||||
"location_info": "Händelsens plats",
|
"location_info": "Händelsens plats",
|
||||||
|
"additional_notes_variable": "Ytterligare inmatning",
|
||||||
"additional_notes_info": "Ytterligare bokningsanteckningar",
|
"additional_notes_info": "Ytterligare bokningsanteckningar",
|
||||||
"attendee_name_info": "Personens bokningsnamn",
|
"attendee_name_info": "Personens bokningsnamn",
|
||||||
"organizer_name_info": "Arrangörens namn",
|
"organizer_name_info": "Arrangörens namn",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Verifieringskod",
|
"verification_code": "Verifieringskod",
|
||||||
"can_you_try_again": "Kan du försöka igen med en annan tid?",
|
"can_you_try_again": "Kan du försöka igen med en annan tid?",
|
||||||
"verify": "Verifiera",
|
"verify": "Verifiera",
|
||||||
|
"timezone_variable": "Tidszon",
|
||||||
"timezone_info": "Tidszon för mottagande person",
|
"timezone_info": "Tidszon för mottagande person",
|
||||||
"event_end_time_variable": "Händelsens sluttid",
|
"event_end_time_variable": "Händelsens sluttid",
|
||||||
"event_end_time_info": "Händelsens sluttid",
|
"event_end_time_info": "Händelsens sluttid",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Etkinlik türü adı",
|
"event_name_info": "Etkinlik türü adı",
|
||||||
"event_date_info": "Etkinlik tarihi",
|
"event_date_info": "Etkinlik tarihi",
|
||||||
"event_time_info": "Etkinlik başlama saati",
|
"event_time_info": "Etkinlik başlama saati",
|
||||||
|
"location_variable": "Konum",
|
||||||
"location_info": "Etkinlik yeri",
|
"location_info": "Etkinlik yeri",
|
||||||
|
"additional_notes_variable": "Ek notlar",
|
||||||
"additional_notes_info": "Ek rezervasyon notları",
|
"additional_notes_info": "Ek rezervasyon notları",
|
||||||
"attendee_name_info": "Rezervasyon yaptıran kişinin adı",
|
"attendee_name_info": "Rezervasyon yaptıran kişinin adı",
|
||||||
"organizer_name_info": "Düzenleyenin adı",
|
"organizer_name_info": "Düzenleyenin adı",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Doğrulama kodu",
|
"verification_code": "Doğrulama kodu",
|
||||||
"can_you_try_again": "Başka bir saatle tekrar deneyebilir misiniz?",
|
"can_you_try_again": "Başka bir saatle tekrar deneyebilir misiniz?",
|
||||||
"verify": "Doğrula",
|
"verify": "Doğrula",
|
||||||
|
"timezone_variable": "Saat dilimi",
|
||||||
"timezone_info": "Alıcının saat dilimi",
|
"timezone_info": "Alıcının saat dilimi",
|
||||||
"event_end_time_variable": "Etkinlik bitiş saati",
|
"event_end_time_variable": "Etkinlik bitiş saati",
|
||||||
"event_end_time_info": "Etkinlik bitiş saati",
|
"event_end_time_info": "Etkinlik bitiş saati",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "Назва типу заходу",
|
"event_name_info": "Назва типу заходу",
|
||||||
"event_date_info": "Дата заходу",
|
"event_date_info": "Дата заходу",
|
||||||
"event_time_info": "Час початку заходу",
|
"event_time_info": "Час початку заходу",
|
||||||
|
"location_variable": "Розташування",
|
||||||
"location_info": "Місце проведення заходу",
|
"location_info": "Місце проведення заходу",
|
||||||
|
"additional_notes_variable": "Додаткові примітки",
|
||||||
"additional_notes_info": "Додаткові примітки щодо бронювання",
|
"additional_notes_info": "Додаткові примітки щодо бронювання",
|
||||||
"attendee_name_info": "Ім’я особи, яка бронює",
|
"attendee_name_info": "Ім’я особи, яка бронює",
|
||||||
"organizer_name_info": "Ім’я організатора",
|
"organizer_name_info": "Ім’я організатора",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "Код перевірки",
|
"verification_code": "Код перевірки",
|
||||||
"can_you_try_again": "Виберіть інший час і повторіть спробу.",
|
"can_you_try_again": "Виберіть інший час і повторіть спробу.",
|
||||||
"verify": "Перевірити",
|
"verify": "Перевірити",
|
||||||
|
"timezone_variable": "Часовий пояс",
|
||||||
"timezone_info": "Часовий пояс одержувача",
|
"timezone_info": "Часовий пояс одержувача",
|
||||||
"event_end_time_variable": "Час завершення заходу",
|
"event_end_time_variable": "Час завершення заходу",
|
||||||
"event_end_time_info": "Час завершення заходу",
|
"event_end_time_info": "Час завершення заходу",
|
||||||
|
|
|
@ -1355,7 +1355,9 @@
|
||||||
"event_name_info": "Tên loại sự kiện",
|
"event_name_info": "Tên loại sự kiện",
|
||||||
"event_date_info": "Ngày sự kiện",
|
"event_date_info": "Ngày sự kiện",
|
||||||
"event_time_info": "Thời gian bắt đầu sự kiện",
|
"event_time_info": "Thời gian bắt đầu sự kiện",
|
||||||
|
"location_variable": "Vị trí",
|
||||||
"location_info": "Địa điểm sự kiện",
|
"location_info": "Địa điểm sự kiện",
|
||||||
|
"additional_notes_variable": "Ghi chú bổ sung",
|
||||||
"additional_notes_info": "Ghi chú thêm cho lịch hẹn",
|
"additional_notes_info": "Ghi chú thêm cho lịch hẹn",
|
||||||
"attendee_name_info": "Tên người tham gia lịch hẹn",
|
"attendee_name_info": "Tên người tham gia lịch hẹn",
|
||||||
"organizer_name_info": "Tên của tổ chức",
|
"organizer_name_info": "Tên của tổ chức",
|
||||||
|
@ -1807,6 +1809,7 @@
|
||||||
"verification_code": "Mã xác minh",
|
"verification_code": "Mã xác minh",
|
||||||
"can_you_try_again": "Bạn có thể thử lại lúc khác được chứ?",
|
"can_you_try_again": "Bạn có thể thử lại lúc khác được chứ?",
|
||||||
"verify": "Xác minh",
|
"verify": "Xác minh",
|
||||||
|
"timezone_variable": "Múi giờ",
|
||||||
"timezone_info": "Múi giờ của người nhận",
|
"timezone_info": "Múi giờ của người nhận",
|
||||||
"event_end_time_variable": "Thời gian kết thúc sự kiện",
|
"event_end_time_variable": "Thời gian kết thúc sự kiện",
|
||||||
"event_end_time_info": "Thời gian kết thúc sự kiện",
|
"event_end_time_info": "Thời gian kết thúc sự kiện",
|
||||||
|
|
|
@ -1353,7 +1353,9 @@
|
||||||
"event_name_info": "活动类型名称",
|
"event_name_info": "活动类型名称",
|
||||||
"event_date_info": "活动日期",
|
"event_date_info": "活动日期",
|
||||||
"event_time_info": "活动开始时间",
|
"event_time_info": "活动开始时间",
|
||||||
|
"location_variable": "位置",
|
||||||
"location_info": "活动位置",
|
"location_info": "活动位置",
|
||||||
|
"additional_notes_variable": "附加备注",
|
||||||
"additional_notes_info": "预约附加说明",
|
"additional_notes_info": "预约附加说明",
|
||||||
"attendee_name_info": "预约人的姓名",
|
"attendee_name_info": "预约人的姓名",
|
||||||
"organizer_name_info": "组织者姓名",
|
"organizer_name_info": "组织者姓名",
|
||||||
|
@ -1804,6 +1806,7 @@
|
||||||
"verification_code": "验证码",
|
"verification_code": "验证码",
|
||||||
"can_you_try_again": "可否用不同的时间再次尝试?",
|
"can_you_try_again": "可否用不同的时间再次尝试?",
|
||||||
"verify": "验证",
|
"verify": "验证",
|
||||||
|
"timezone_variable": "时区",
|
||||||
"timezone_info": "接收人的时区",
|
"timezone_info": "接收人的时区",
|
||||||
"event_end_time_variable": "活动结束时间",
|
"event_end_time_variable": "活动结束时间",
|
||||||
"event_end_time_info": "活动结束时间",
|
"event_end_time_info": "活动结束时间",
|
||||||
|
|
|
@ -1352,7 +1352,9 @@
|
||||||
"event_name_info": "活動類型名稱",
|
"event_name_info": "活動類型名稱",
|
||||||
"event_date_info": "活動日期",
|
"event_date_info": "活動日期",
|
||||||
"event_time_info": "活動開始時間",
|
"event_time_info": "活動開始時間",
|
||||||
|
"location_variable": "地點",
|
||||||
"location_info": "活動地點",
|
"location_info": "活動地點",
|
||||||
|
"additional_notes_variable": "備註",
|
||||||
"additional_notes_info": "預約額外備註",
|
"additional_notes_info": "預約額外備註",
|
||||||
"attendee_name_info": "預約人姓名",
|
"attendee_name_info": "預約人姓名",
|
||||||
"organizer_name_info": "主辦者姓名",
|
"organizer_name_info": "主辦者姓名",
|
||||||
|
@ -1803,6 +1805,7 @@
|
||||||
"verification_code": "驗證碼",
|
"verification_code": "驗證碼",
|
||||||
"can_you_try_again": "您可以再試試其他時間嗎?",
|
"can_you_try_again": "您可以再試試其他時間嗎?",
|
||||||
"verify": "驗證",
|
"verify": "驗證",
|
||||||
|
"timezone_variable": "時區",
|
||||||
"timezone_info": "接收人的時區",
|
"timezone_info": "接收人的時區",
|
||||||
"event_end_time_variable": "活動結束時間",
|
"event_end_time_variable": "活動結束時間",
|
||||||
"event_end_time_info": "活動結束時間",
|
"event_end_time_info": "活動結束時間",
|
||||||
|
|
|
@ -143,7 +143,8 @@ html.todesktop header {
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.todesktop header button {
|
html.todesktop header button,
|
||||||
|
html.todesktop header a {
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ import AttendeeScheduledEmail from "./templates/attendee-scheduled-email";
|
||||||
import type { EmailVerifyCode } from "./templates/attendee-verify-email";
|
import type { EmailVerifyCode } from "./templates/attendee-verify-email";
|
||||||
import AttendeeVerifyEmail from "./templates/attendee-verify-email";
|
import AttendeeVerifyEmail from "./templates/attendee-verify-email";
|
||||||
import AttendeeWasRequestedToRescheduleEmail from "./templates/attendee-was-requested-to-reschedule-email";
|
import AttendeeWasRequestedToRescheduleEmail from "./templates/attendee-was-requested-to-reschedule-email";
|
||||||
|
import BookingRedirectEmailNotification from "./templates/booking-redirect-notification";
|
||||||
|
import type { IBookingRedirect } from "./templates/booking-redirect-notification";
|
||||||
import BrokenIntegrationEmail from "./templates/broken-integration-email";
|
import BrokenIntegrationEmail from "./templates/broken-integration-email";
|
||||||
import DisabledAppEmail from "./templates/disabled-app-email";
|
import DisabledAppEmail from "./templates/disabled-app-email";
|
||||||
import type { Feedback } from "./templates/feedback-email";
|
import type { Feedback } from "./templates/feedback-email";
|
||||||
|
@ -437,3 +439,7 @@ export const sendMonthlyDigestEmails = async (eventData: MonthlyDigestEmailData)
|
||||||
export const sendAdminOrganizationNotification = async (input: OrganizationNotification) => {
|
export const sendAdminOrganizationNotification = async (input: OrganizationNotification) => {
|
||||||
await sendEmail(() => new AdminOrganizationNotification(input));
|
await sendEmail(() => new AdminOrganizationNotification(input));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendBookingRedirectNotification = async (bookingRedirect: IBookingRedirect) => {
|
||||||
|
await sendEmail(() => new BookingRedirectEmailNotification(bookingRedirect));
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { IBookingRedirect } from "../../templates/booking-redirect-notification";
|
||||||
|
import { BaseEmailHtml } from "../components";
|
||||||
|
|
||||||
|
export const BookingRedirectEmailNotification = (
|
||||||
|
props: IBookingRedirect & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailHtml
|
||||||
|
subject={props.language("booking_redirect_email_subject")}
|
||||||
|
title={props.language("booking_redirect_email_title")}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "black",
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
fontWeight: "400",
|
||||||
|
}}>
|
||||||
|
{props.language("booking_redirect_email_description", {
|
||||||
|
toName: props.toName,
|
||||||
|
})}
|
||||||
|
{props.dates}
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginTop: "16px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</BaseEmailHtml>
|
||||||
|
);
|
||||||
|
};
|
|
@ -31,3 +31,4 @@ export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail
|
||||||
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
|
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
|
||||||
export { MonthlyDigestEmail } from "./MonthlyDigestEmail";
|
export { MonthlyDigestEmail } from "./MonthlyDigestEmail";
|
||||||
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
|
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
|
||||||
|
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { TFunction } from "next-i18next";
|
||||||
|
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import { renderEmail } from "..";
|
||||||
|
import BaseEmail from "./_base-email";
|
||||||
|
|
||||||
|
export interface IBookingRedirect {
|
||||||
|
language: TFunction;
|
||||||
|
fromEmail: string;
|
||||||
|
toEmail: string;
|
||||||
|
toName: string;
|
||||||
|
dates: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BookingRedirectNotification extends BaseEmail {
|
||||||
|
bookingRedirect: IBookingRedirect;
|
||||||
|
|
||||||
|
constructor(bookingRedirect: IBookingRedirect) {
|
||||||
|
super();
|
||||||
|
this.name = "BOOKING_REDIRECT_NOTIFICATION";
|
||||||
|
this.bookingRedirect = bookingRedirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
|
return {
|
||||||
|
to: `${this.bookingRedirect.toEmail} <${this.bookingRedirect.toName}>`,
|
||||||
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
|
subject: this.bookingRedirect.language("booking_redirect_email_subject"),
|
||||||
|
html: await renderEmail("BookingRedirectEmailNotification", {
|
||||||
|
...this.bookingRedirect,
|
||||||
|
}),
|
||||||
|
text: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user