Merge branch 'main' into update-yarn-lock

This commit is contained in:
Hariom Balhara 2023-11-16 18:15:53 +05:30 committed by GitHub
commit 80366fb858
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 3463 additions and 784 deletions

View File

@ -25,6 +25,7 @@ CALCOM_LICENSE_KEY=
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
INSIGHTS_DATABASE_URL=
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
# Cold boots will be faster and you'll be able to scale your DB independently of your app.

View File

@ -1,7 +1,7 @@
import type { NextMiddleware } from "next-api-middleware";
import { CONSOLE_URL } from "@calcom/lib/constants";
import prisma, { customPrisma } from "@calcom/prisma";
import { customPrisma } from "@calcom/prisma";
const LOCAL_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL;
@ -12,41 +12,32 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => {
} = req;
// If no custom api Id is provided, attach to request the regular cal.com prisma client.
if (!key) {
req.prisma = prisma;
req.prisma = customPrisma();
await next();
return;
}
try {
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
.then((res) => res.json())
.then((res) => res.databaseUrl);
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
.then((res) => res.json())
.then((res) => res.databaseUrl);
if (!databaseUrl) {
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
return;
}
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
/* @note:
if (!databaseUrl) {
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
return;
}
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
/* @note:
In order to skip verifyApiKey for customPrisma requests,
we pass isAdmin true, and userId 0, if we detect them later,
we skip verifyApiKey logic and pass onto next middleware instead.
*/
req.isAdmin = true;
req.isCustomPrisma = true;
// We don't need the key from here and on. Prevents unrecognized key errors.
delete req.query.key;
await next();
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
} catch (err) {
if (req.prisma) {
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
}
throw err;
}
req.isAdmin = true;
req.isCustomPrisma = true;
// We don't need the key from here and on. Prevents unrecognized key errors.
delete req.query.key;
await next();
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
};

View File

@ -26,6 +26,7 @@ const schemaAvailabilityCreateParams = z
startTime: z.date().or(z.string()),
endTime: z.date().or(z.string()),
days: z.array(z.number()).optional(),
date: z.date().or(z.string()).optional(),
})
.strict();
@ -34,6 +35,7 @@ const schemaAvailabilityEditParams = z
startTime: z.date().or(z.string()).optional(),
endTime: z.date().or(z.string()).optional(),
days: z.array(z.number()).optional(),
date: z.date().or(z.string()).optional(),
})
.strict();

View File

@ -21,7 +21,16 @@ export const schemaSchedulePublic = z
.merge(
z.object({
availability: z
.array(Availability.pick({ id: true, eventTypeId: true, days: true, startTime: true, endTime: true }))
.array(
Availability.pick({
id: true,
eventTypeId: true,
date: true,
days: true,
startTime: true,
endTime: true,
})
)
.transform((v) =>
v.map((item) => ({
...item,

View File

@ -8,6 +8,7 @@ import { describe, expect, test, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
@ -148,7 +149,7 @@ describe.skipIf(true)("POST /api/bookings", () => {
expect(res._getStatusCode()).toBe(500);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
message: "No available users found.",
message: ErrorCode.NoAvailableUsersFound,
})
);
});

7
apps/api/vercel.json Normal file
View File

@ -0,0 +1,7 @@
{
"functions": {
"pages/api/slots/*.ts": {
"memory": 512
}
}
}

View File

@ -0,0 +1,50 @@
import { getBucket } from "abTest/utils";
import type { NextMiddleware, NextRequest } from "next/server";
import { NextResponse } from "next/server";
import z from "zod";
const ROUTES: [RegExp, boolean][] = [
[/^\/event-types$/, Boolean(process.env.APP_ROUTER_EVENT_TYPES_ENABLED)],
];
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override";
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled";
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]).default("legacy");
export const abTestMiddlewareFactory =
(next: (req: NextRequest) => Promise<NextResponse<unknown>>): NextMiddleware =>
async (req: NextRequest) => {
const response = await next(req);
const { pathname } = req.nextUrl;
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
const route = ROUTES.find(([regExp]) => regExp.test(pathname)) ?? null;
const enabled = route !== null ? route[1] || override : false;
if (pathname.includes("future") || !enabled) {
return response;
}
const safeParsedBucket = override
? { success: true as const, data: "future" as const }
: bucketSchema.safeParse(req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value);
if (!safeParsedBucket.success) {
// cookie does not exist or it has incorrect value
const res = NextResponse.next(response);
res.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, getBucket(), { expires: 1000 * 60 * 30 }); // 30 min in ms
return res;
}
const bucketUrlPrefix = safeParsedBucket.data === "future" ? "future" : "";
const url = req.nextUrl.clone();
url.pathname = `${bucketUrlPrefix}${pathname}/`;
return NextResponse.rewrite(url, response);
};

9
apps/web/abTest/utils.ts Normal file
View File

@ -0,0 +1,9 @@
import { AB_TEST_BUCKET_PROBABILITY } from "@calcom/lib/constants";
const cryptoRandom = () => {
return crypto.getRandomValues(new Uint8Array(1))[0] / 0xff;
};
export const getBucket = () => {
return cryptoRandom() * 100 < AB_TEST_BUCKET_PROBABILITY ? "future" : "legacy";
};

View File

@ -42,6 +42,7 @@ export type AppPageProps = {
disableInstall?: boolean;
dependencies?: string[];
concurrentMeetings: AppType["concurrentMeetings"];
paid?: AppType["paid"];
};
export const AppPage = ({
@ -67,6 +68,7 @@ export const AppPage = ({
isTemplate,
dependencies,
concurrentMeetings,
paid,
}: AppPageProps) => {
const { t, i18n } = useLocale();
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
@ -163,6 +165,19 @@ export const AppPage = ({
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]}
</Link>{" "}
{paid && (
<>
<Badge className="mr-1">
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
maximumFractionDigits: 0,
}).format(paid.priceInUsd)}
/{t("month")}
</Badge>
</>
)}
{" "}
<a target="_blank" rel="noreferrer" href={website}>
{t("published_by", { author })}
@ -206,6 +221,7 @@ export const AppPage = ({
addAppMutationInput={{ type, variant, slug }}
multiInstall
concurrentMeetings={concurrentMeetings}
paid={paid}
{...props}
/>
);
@ -244,6 +260,7 @@ export const AppPage = ({
addAppMutationInput={{ type, variant, slug }}
credentials={appDbQuery.data?.credentials}
concurrentMeetings={concurrentMeetings}
paid={paid}
{...props}
/>
);
@ -263,7 +280,7 @@ export const AppPage = ({
<SkeletonButton className="mt-6 h-20 grow" />
))}
{price !== 0 && (
{price !== 0 && !paid && (
<span className="block text-right">
{feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar}
{feeType === "monthly" && `/${t("month")}`}
@ -273,23 +290,27 @@ export const AppPage = ({
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && `/${t("month")}`}
</>
)}
</span>
{!paid && (
<>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && `/${t("month")}`}
</>
)}
</span>
</>
)}
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5">

View File

@ -26,6 +26,7 @@ export const InstallAppButtonChild = ({
multiInstall,
credentials,
concurrentMeetings,
paid,
...props
}: {
userAdminTeams?: UserAdminTeams;
@ -34,6 +35,7 @@ export const InstallAppButtonChild = ({
multiInstall?: boolean;
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
concurrentMeetings?: boolean;
paid?: AppFrontendPayload["paid"];
} & ButtonProps) => {
const { t } = useLocale();
@ -46,8 +48,27 @@ export const InstallAppButtonChild = ({
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
},
});
const shouldDisableInstallation = !multiInstall ? !!(credentials && credentials.length) : false;
if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) {
// Paid apps don't support team installs at the moment
// Also, cal.ai(the only paid app at the moment) doesn't support team install either
if (paid) {
return (
<Button
data-testid="install-app-button"
{...props}
disabled={shouldDisableInstallation}
color="primary"
size="base">
{paid.trial ? t("start_paid_trial") : t("install_paid_app")}
</Button>
);
}
if (
!userAdminTeams?.length ||
!doesAppSupportTeamInstall({ appCategories, concurrentMeetings, isPaid: !!paid })
) {
return (
<Button
data-testid="install-app-button"
@ -55,6 +76,7 @@ export const InstallAppButtonChild = ({
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
color="primary"
disabled={shouldDisableInstallation}
size="base">
{multiInstall ? t("install_another") : t("install_app")}
</Button>

View File

@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => {
const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => {
return function (key, value) {
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
@ -57,6 +57,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
[appId]: {
...appData,
[key]: value,
credentialId,
},
});
};
@ -76,7 +77,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
appCards.push(
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
key={app.slug}
app={app}
eventType={eventType}
@ -90,7 +91,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
appCards.push(
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, team.credentialId)}
key={app.slug + team?.credentialId}
app={{
...app,
@ -147,7 +148,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
return (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
key={app.slug}
app={app}
eventType={eventType}

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.4.8",
"version": "3.5.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",

View File

@ -73,7 +73,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
}
return res.status(200);
if (!res.writableEnded) return res.status(200);
return res;
} catch (error) {
console.error(error);
if (error instanceof HttpError) {

View File

@ -0,0 +1,116 @@
import { buffer } from "micro";
import type { NextApiRequest, NextApiResponse } from "next";
import type Stripe from "stripe";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
export const config = {
api: {
bodyParser: false,
},
};
// This file is a catch-all for any integration related subscription/paid app.
const handleSubscriptionUpdate = async (event: Stripe.Event) => {
const subscription = event.data.object as Stripe.Subscription;
if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" });
const app = await prisma.credential.findFirst({
where: {
subscriptionId: subscription.id,
},
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
}
await prisma.credential.update({
where: {
id: app.id,
},
data: {
paymentStatus: subscription.status,
},
});
};
const handleSubscriptionDeleted = async (event: Stripe.Event) => {
const subscription = event.data.object as Stripe.Subscription;
if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" });
const app = await prisma.credential.findFirst({
where: {
subscriptionId: subscription.id,
},
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
}
// should we delete the credential here rather than marking as inactive?
await prisma.credential.update({
where: {
id: app.id,
},
data: {
paymentStatus: "inactive",
billingCycleStart: null,
},
});
};
type WebhookHandler = (event: Stripe.Event) => Promise<void>;
const webhookHandlers: Record<string, WebhookHandler | undefined> = {
"customer.subscription.updated": handleSubscriptionUpdate,
"customer.subscription.deleted": handleSubscriptionDeleted,
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "POST") {
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
}
const sig = req.headers["stripe-signature"];
if (!sig) {
throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" });
}
if (!process.env.STRIPE_WEBHOOK_SECRET) {
throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
}
const requestBuffer = await buffer(req);
const payload = requestBuffer.toString();
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
const handler = webhookHandlers[event.type];
if (handler) {
await handler(event);
} else {
/** Not really an error, just letting Stripe know that the webhook was received but unhandled */
throw new HttpCode({
statusCode: 202,
message: `Unhandled Stripe Webhook event type ${event.type}`,
});
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
res.status(err.statusCode ?? 500).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
});
return;
}
// Return a response to acknowledge receipt of the event
res.json({ received: true });
}

View File

@ -79,6 +79,7 @@ function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
isTemplate={data.isTemplate}
dependencies={data.dependencies}
concurrentMeetings={data.concurrentMeetings}
paid={data.paid}
// tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy"
body={

View File

@ -278,7 +278,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context;
const { req, res, query } = context;
const session = await getServerSession({ req, res });
const ssr = await ssrInit(context);
@ -318,6 +318,24 @@ const _getServerSideProps = async function getServerSideProps(context: GetServer
}
if (session) {
const { callbackUrl } = query;
if (callbackUrl) {
try {
const destination = getSafeRedirectUrl(callbackUrl as string);
if (destination) {
return {
redirect: {
destination,
permanent: false,
},
};
}
} catch (e) {
console.warn(e);
}
}
return {
redirect: {
destination: "/",

View File

@ -9,8 +9,8 @@ import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import checkIfItFallbackImage from "@calcom/lib/checkIfItFallbackImage";
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import turndown from "@calcom/lib/turndownService";
@ -72,32 +72,24 @@ interface DeleteAccountValues {
type FormValues = {
username: string;
avatar: string | null;
avatar: string;
name: string;
email: string;
bio: string;
};
const checkIfItFallbackImage = (fetchedImgSrc?: string) => {
return !fetchedImgSrc || fetchedImgSrc.endsWith(AVATAR_FALLBACK);
};
const ProfileView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { update } = useSession();
const [fetchedImgSrc, setFetchedImgSrc] = useState<string | undefined>(undefined);
const [fetchedImgSrc, setFetchedImgSrc] = useState<string>("");
const { data: user, isLoading } = trpc.viewer.me.useQuery(undefined, {
onSuccess: async (userData) => {
try {
if (!userData.organization) {
const res = await fetch(userData.avatar);
if (res.url) setFetchedImgSrc(res.url);
} else {
setFetchedImgSrc("");
}
const res = await fetch(userData.avatar);
if (res.url) setFetchedImgSrc(res.url);
} catch (err) {
setFetchedImgSrc("");
}
@ -234,7 +226,7 @@ const ProfileView = () => {
const defaultValues = {
username: user.username || "",
avatar: user.avatar || "",
avatar: fetchedImgSrc || "",
name: user.name || "",
email: user.email || "",
bio: user.bio || "",
@ -251,8 +243,6 @@ const ProfileView = () => {
key={JSON.stringify(defaultValues)}
defaultValues={defaultValues}
isLoading={updateProfileMutation.isLoading}
isFallbackImg={checkIfItFallbackImage(fetchedImgSrc)}
userAvatar={user.avatar}
user={user}
userOrganization={user.organization}
onSubmit={(values) => {
@ -397,8 +387,6 @@ const ProfileForm = ({
onSubmit,
extraField,
isLoading = false,
isFallbackImg,
userAvatar,
user,
userOrganization,
}: {
@ -406,8 +394,6 @@ const ProfileForm = ({
onSubmit: (values: FormValues) => void;
extraField?: React.ReactNode;
isLoading: boolean;
isFallbackImg: boolean;
userAvatar: string;
user: RouterOutputs["viewer"]["me"];
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
}) => {
@ -416,7 +402,7 @@ const ProfileForm = ({
const profileFormSchema = z.object({
username: z.string(),
avatar: z.string().nullable(),
avatar: z.string(),
name: z
.string()
.trim()
@ -438,7 +424,6 @@ const ProfileForm = ({
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
return (
<Form form={formMethods} handleSubmit={onSubmit}>
<div className="border-subtle border-x px-4 pb-10 pt-8 sm:px-6">
@ -447,7 +432,7 @@ const ProfileForm = ({
control={formMethods.control}
name="avatar"
render={({ field: { value } }) => {
const showRemoveAvatarButton = !isFallbackImg || (value && userAvatar !== value);
const showRemoveAvatarButton = !checkIfItFallbackImage(value);
const organization =
userOrganization && userOrganization.id
? {
@ -474,7 +459,7 @@ const ProfileForm = ({
handleAvatarChange={(newAvatar) => {
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
}}
imageSrc={value || undefined}
imageSrc={value}
triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"}
/>
@ -482,7 +467,7 @@ const ProfileForm = ({
<Button
color="secondary"
onClick={() => {
formMethods.setValue("avatar", null, { shouldDirty: true });
formMethods.setValue("avatar", "", { shouldDirty: true });
}}>
{t("remove")}
</Button>

View File

@ -11,10 +11,6 @@ let pages = (exports.pages = glob
)
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]")));
// Following routes don't exist but they work by doing rewrite. Thus they need to be excluded from matching the orgRewrite patterns
// Make sure to keep it upto date as more nonExistingRouteRewrites are added.
const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
// It would also not match /free/30min/embed because we are ensuring just two slashes
@ -27,11 +23,26 @@ let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
));
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes);
exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`;
exports.orgUserTypeRoutePath = `/:user((?!${beforeRewriteExcludePages.join(
"/|"
)}|_next/|public/)[^/]+)/:type((?!avatar\.png)[^/]+)`;
exports.orgUserTypeEmbedRoutePath = `/:user((?!${beforeRewriteExcludePages.join(
"/|"
)}|_next/|public/)[^/]+)/:type/embed`;
/**
* Returns a regex that matches all existing routes, virtual routes (like /forms, /router, /success etc) and nextjs special paths (_next, public)
*/
function getRegExpMatchingAllReservedRoutes(suffix) {
// Following routes don't exist but they work by doing rewrite. Thus they need to be excluded from matching the orgRewrite patterns
// Make sure to keep it upto date as more nonExistingRouteRewrites are added.
const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
const nextJsSpecialPaths = ["_next", "public"];
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes).concat(nextJsSpecialPaths);
return beforeRewriteExcludePages.join(`${suffix}|`) + suffix;
}
// To handle /something
exports.orgUserRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/?$")})[a-zA-Z0-9\-_]+)`;
// To handle /something/somethingelse
exports.orgUserTypeRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes(
"/"
)})[^/]+)/:type((?!avatar\.png)[^/]+)`;
// To handle /something/somethingelse/embed
exports.orgUserTypeEmbedRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/")})[^/]+)/:type/embed`;

View File

@ -1,6 +1,8 @@
import { expect } from "@playwright/test";
import { randomString } from "@calcom/lib/random";
import { SchedulingType } from "@calcom/prisma/client";
import type { Schedule, TimeRange } from "@calcom/types/schedule";
import { test } from "./lib/fixtures";
import {
@ -342,3 +344,92 @@ test.describe("Booking on different layouts", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
});
test.describe("Booking round robin event", () => {
test.beforeEach(async ({ page, users }) => {
const teamMatesObj = [{ name: "teammate-1" }];
const dateRanges: TimeRange = {
start: new Date(new Date().setUTCHours(10, 0, 0, 0)), //one hour after default schedule (teammate-1's schedule)
end: new Date(new Date().setUTCHours(17, 0, 0, 0)),
};
const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []];
const testUser = await users.create(
{ username: "test-user", name: "Test User", email: "testuser@example.com", schedule },
{
hasTeam: true,
schedulingType: SchedulingType.ROUND_ROBIN,
teamEventLength: 120,
teammates: teamMatesObj,
}
);
const team = await testUser.getFirstTeam();
await page.goto(`/team/${team.team.slug}`);
});
test("Does not book round robin host outside availability with date override", async ({ page, users }) => {
const [testUser] = users.get();
testUser.apiLogin();
const team = await testUser.getFirstTeam();
// Click first event type (round robin)
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="incrementMonth"]');
// books 9AM slots for 120 minutes (test-user is not available at this time, availability starts at 10)
await page.locator('[data-testid="time"]').nth(0).click();
await page.waitForLoadState("networkidle");
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const host = await page.locator('[data-testid="booking-host-name"]');
const hostName = await host.innerText();
//expect teammate-1 to be booked, test-user is not available at this time
expect(hostName).toBe("teammate-1");
// make another booking to see if also for the second booking teammate-1 is booked
await page.goto(`/team/${team.team.slug}`);
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="incrementMonth"]');
await page.click('[data-testid="incrementMonth"]');
// Again book a 9AM slot for 120 minutes where test-user is not available
await page.locator('[data-testid="time"]').nth(0).click();
await page.waitForLoadState("networkidle");
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const hostSecondBooking = await page.locator('[data-testid="booking-host-name"]');
const hostNameSecondBooking = await hostSecondBooking.innerText();
expect(hostNameSecondBooking).toBe("teammate-1"); // teammate-1 should be booked again
});
});

View File

@ -10,6 +10,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { Schedule } from "@calcom/types/schedule";
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
import { TimeZoneEnum } from "./types";
@ -46,6 +47,7 @@ const createTeamEventType = async (
schedulingType?: SchedulingType;
teamEventTitle?: string;
teamEventSlug?: string;
teamEventLength?: number;
}
) => {
return await prisma.eventType.create({
@ -65,10 +67,16 @@ const createTeamEventType = async (
id: user.id,
},
},
hosts: {
create: {
userId: user.id,
isFixed: scenario?.schedulingType === SchedulingType.COLLECTIVE ? true : false,
},
},
schedulingType: scenario?.schedulingType ?? SchedulingType.COLLECTIVE,
title: scenario?.teamEventTitle ?? `${teamEventTitle}-team-id-${team.id}`,
slug: scenario?.teamEventSlug ?? `${teamEventSlug}-team-id-${team.id}`,
length: 30,
length: scenario?.teamEventLength ?? 30,
},
});
};
@ -135,6 +143,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
schedulingType?: SchedulingType;
teamEventTitle?: string;
teamEventSlug?: string;
teamEventLength?: number;
isOrg?: boolean;
hasSubteam?: true;
isUnpublished?: true;
@ -489,6 +498,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
// ignores adding the worker-index after username
useExactUsername?: boolean;
roleInOrganization?: MembershipRole;
schedule?: Schedule;
};
// creates the actual user in the db.
@ -520,7 +530,7 @@ const createUser = (
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
availability: {
createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
data: getAvailabilityFromSchedule(opts?.schedule ?? DEFAULT_SCHEDULE),
},
},
},
@ -641,7 +651,7 @@ export async function apiLogin(
export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, page: Page) {
await page.goto(`/event-types/${eventType?.id}?tabName=apps`);
await page.locator("div > .ml-auto").first().click();
await page.locator("[data-testid='app-switch']").first().click();
await page.getByPlaceholder("Price").fill("100");
await page.getByTestId("update-eventtype").click();
}

View File

@ -1,6 +1,10 @@
import { expect } from "@playwright/test";
import type Prisma from "@prisma/client";
import prisma from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { test } from "./lib/fixtures";
import type { Fixtures } from "./lib/fixtures";
import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
@ -34,6 +38,95 @@ test.describe("Stripe integration", () => {
});
});
test("when enabling Stripe, credentialId is included", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await page.goto("/apps/installed");
await user.getPaymentCredential();
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
await user.setupEventWithPrice(eventType);
// Need to wait for the DB to be updated with the metadata
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
// Check event type metadata to see if credentialId is included
const eventTypeMetadata = await prisma.eventType.findFirst({
where: {
id: eventType.id,
},
select: {
metadata: true,
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
expect(stripeAppMetadata).toHaveProperty("credentialId");
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
});
test("when enabling Stripe, team credentialId is included", async ({ page, users }) => {
const ownerObj = { username: "pro-user", name: "pro-user" };
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(ownerObj, {
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
});
await owner.apiLogin();
const { team } = await owner.getFirstTeam();
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
const teamEvent = await owner.getFirstTeamEvent(team.id);
await page.goto("/apps/stripe");
/** We start the Stripe flow */
await Promise.all([
page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"),
page.click('[data-testid="install-app-button"]'),
page.click('[data-testid="anything else"]'),
]);
await Promise.all([
page.waitForURL("/apps/installed/payment?hl=stripe"),
/** We skip filling Stripe forms (testing mode only) */
page.click('[id="skip-account-app"]'),
]);
await owner.setupEventWithPrice(teamEvent);
// Need to wait for the DB to be updated with the metadata
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
// Check event type metadata to see if credentialId is included
const eventTypeMetadata = await prisma.eventType.findFirst({
where: {
id: teamEvent.id,
},
select: {
metadata: true,
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
expect(stripeAppMetadata).toHaveProperty("credentialId");
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
});
test("Can book a paid booking", async ({ page, users }) => {
const user = await users.create();
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;

View File

@ -134,7 +134,7 @@ test.describe("Teams - NonOrg", () => {
// Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull();
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
expect(teamMatesObj.concat([{ name: ownerObj.name }]).some(({ name }) => name === chosenUser)).toBe(true);
// TODO: Assert whether the user received an email
});
@ -370,7 +370,7 @@ test.describe("Teams - Org", () => {
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
// All the teammates should be in the booking
for (const teammate of teamMatesObj) {
for (const teammate of teamMatesObj.concat([{ name: owner.name || "" }])) {
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
}
}
@ -412,7 +412,7 @@ test.describe("Teams - Org", () => {
// Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull();
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
expect(teamMatesObj.concat([{ name: ownerObj.name }]).some(({ name }) => name === chosenUser)).toBe(true);
// TODO: Assert whether the user received an email
});
});

View File

@ -849,6 +849,7 @@
"next_step": "تخطي الخطوة",
"prev_step": "الخطوة السابقة",
"install": "تثبيت",
"install_paid_app": "اشتراك",
"installed": "تم التثبيت",
"active_install_one": "{{count}} تثبيت نشط",
"active_install_other": "{{count}} تثبيت نشط",

View File

@ -268,6 +268,7 @@
"set_availability": "Nastavte, jak jste dostupní",
"availability_settings": "Nastavení dostupnosti",
"continue_without_calendar": "Pokračovat bez kalendáře",
"continue_with": "Pokračovat přes {{appName}}",
"connect_your_calendar": "Připojit svůj kalendář",
"connect_your_video_app": "Propojte své video aplikace",
"connect_your_video_app_instructions": "Propojte své video aplikace a používejte je ve svých typech událostí.",
@ -288,6 +289,8 @@
"when": "Kdy",
"where": "Kde",
"add_to_calendar": "Přidat do kalendáře",
"add_to_calendar_description": "Vyberte, kam se mají přidávat události, pokud jste zarezervováni.",
"add_events_to": "Přidat události do",
"add_another_calendar": "Přidat další kalendář",
"other": "Ostatní",
"email_sign_in_subject": "Váš přihlašovací odkaz pro {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Rezervace vytvořena",
"booking_rejected": "Rezervace byla zamítnuta",
"booking_requested": "Váš požadavek na rezervaci byl úspěšně odeslán",
"booking_payment_initiated": "Platba za rezervaci zahájena",
"meeting_ended": "Schůzka skončila",
"form_submitted": "Formulář byl odeslán",
"booking_paid": "Rezervace uhrazena",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Tento uživatel zatím nezaložil žádné typy událostí.",
"edit_logo": "Upravit logo",
"upload_a_logo": "Nahrát logo",
"upload_logo": "Nahrát logo",
"remove_logo": "Odstranit logo",
"enable": "Povolit",
"code": "Kód",
@ -568,6 +573,7 @@
"your_team_name": "Název vašeho týmu",
"team_updated_successfully": "Tým byl úspěšně aktualizován",
"your_team_updated_successfully": "Váš tým byl úspěšně aktualizován.",
"your_org_updated_successfully": "Vaše organizace byla aktualizována.",
"about": "O aplikaci",
"team_description": "Pár vět o vašem týmu. Objeví se na URL stránce vašeho týmu.",
"org_description": "Několik vět o vaší organizaci. Zobrazí se na adrese URL vaší organizace.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Skrýt tlačítko Rezervovat člena týmu",
"hide_book_a_team_member_description": "Skrýt tlačítko Rezervovat člena týmu na veřejných stránkách.",
"danger_zone": "Nebezpečná zóna",
"account_deletion_cannot_be_undone": "Pozor. Odstranění účtu nelze vrátit.",
"back": "Zpět",
"cancel": "Zrušit",
"cancel_all_remaining": "Zrušit vše zbývající",
@ -688,6 +695,7 @@
"people": "Lidé",
"your_email": "Váš e-mail",
"change_avatar": "Změnit avatar",
"upload_avatar": "Nahrát avatar",
"language": "Jazyk",
"timezone": "Časová zóna",
"first_day_of_week": "První den v týdnu",
@ -778,6 +786,7 @@
"disable_guests": "Zakázat hosty",
"disable_guests_description": "Zakázat přidávání dalších hostů během rezervace.",
"private_link": "Vygenerovat soukromou adresu URL",
"enable_private_url": "Zapnout soukromou adresu URL",
"private_link_label": "Soukromý odkaz",
"private_link_hint": "Váš soukromý odkaz se po každém použití obnoví",
"copy_private_link": "Zkopírovat soukromý odkaz",
@ -840,6 +849,7 @@
"next_step": "Přeskočit krok",
"prev_step": "Předchozí krok",
"install": "Nainstalovat",
"install_paid_app": "Odebírat",
"installed": "Nainstalováno",
"active_install_one": "Aktivní instalace: {{count}}",
"active_install_other": "Aktivní instalace: {{count}}",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Jméno organizátora",
"app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.",
"invalid_number": "Neplatné telefonní číslo",
"invalid_url_error_message": "Neplatná adresa URL pro {{label}}. Příklad URL: {{sampleUrl}}",
"navigate": "Navigace",
"open": "Otevřít",
"close": "Zavřít",
@ -1276,6 +1287,7 @@
"personal_cal_url": "Moje osobní adresa URL {{appName}}",
"bio_hint": "Několik vět o vás. Obsah se zobrazí se na vaší osobní stránce URL.",
"user_has_no_bio": "Tento uživatel zatím nepřidal životopis.",
"bio": "Bio",
"delete_account_modal_title": "Odstranit účet",
"confirm_delete_account_modal": "Opravdu chcete odstranit svůj účet {{appName}}?",
"delete_my_account": "Odstranit můj účet",
@ -1286,6 +1298,7 @@
"select_calendars": "Vyberte kalendáře, u kterých chcete kontrolovat konflikty v zájmu prevence dvojích rezervací.",
"check_for_conflicts": "Zkontrolovat konflikty",
"view_recordings": "Zobrazit nahrávky",
"check_for_recordings": "Zkontrolovat nahrávky",
"adding_events_to": "Přidání událostí do:",
"follow_system_preferences": "Řídit se předvolbami systému",
"custom_brand_colors": "Vlastní barvy značky",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "Při registraci subdomény došlo k problému, zkuste to prosím znovu nebo kontaktujte správce",
"team_publish": "Zveřejnit tým",
"number_text_notifications": "Telefonní číslo (SMS oznámení)",
"number_sms_notifications": "Telefonní číslo (SMS oznámení)",
"attendee_email_variable": "E-mail účastníka",
"attendee_email_info": "E-mail osoby provádějící rezervaci",
"kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...",
@ -1594,6 +1608,7 @@
"options": "Možnosti",
"enter_option": "Zadejte možnost {{index}}",
"add_an_option": "Přidat možnost",
"location_already_exists": "Toto místo již existuje. Vyberte prosím nové místo",
"radio": "Přepínač",
"google_meet_warning": "Abyste mohli používat službu Google Meet, musíte jako cílový kalendář nastavit Kalendář Google",
"individual": "Jedinec",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Zadat nedostupnost ve vybraných datech",
"date_overrides_add_btn": "Přidat změnu",
"date_overrides_update_btn": "Aktualizovat změnu",
"date_successfully_added": "Přidána změna dnů",
"event_type_duplicate_copy_text": "{{slug}} kopie",
"set_as_default": "Nastavit jako výchozí",
"hide_eventtype_details": "Skrýt podrobnosti o typu události",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Počet hostitelů, kteří se musí zúčastnit",
"hosts": "Hostitelé",
"upgrade_to_enable_feature": "K povolení této funkce je třeba vytvořit tým. Tým vytvoříte kliknutím.",
"orgs_upgrade_to_enable_feature": "Pokud chcete zapnout tuto funkci, musíte upgradovat na náš tarif Enterprise.",
"new_attendee": "Nový účastník",
"awaiting_approval": "Čeká na schválení",
"requires_google_calendar": "Tato aplikace vyžaduje připojení ke Kalendáři Google",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Zobrazit na stránce rezervace",
"get_started_zapier_templates": "Začněte používat šablony Zapier",
"team_is_unpublished": "Tým {{team}} není zveřejněn",
"org_is_unpublished_description": "Tento odkaz organizace není v současné době k dispozici. Kontaktujte prosím vlastníka organizace nebo ho požádejte o jeho zveřejnění.",
"team_is_unpublished_description": "Tento odkaz subjektu ({{entity}}) není v současné době k dispozici. Kontaktujte prosím vlastníka subjektu ({{entity}}) nebo ho požádejte o jeho zveřejnění.",
"team_member": "Člen týmu",
"a_routing_form": "Směrovací formulář",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Upravit nastavení odkazu",
"invite_link_copied": "Odkaz pozvánky byl zkopírován",
"invite_link_deleted": "Odkaz pozvánky byl odstraněn",
"api_key_deleted": "Klíč API odstraněn",
"invite_link_updated": "Nastavení odkazu pozvánky bylo uloženo",
"link_expires_after": "Platnost odkazů vyprší za...",
"one_day": "1 den",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Příjmení účastníka",
"attendee_first_name_info": "Jméno rezervující osoby",
"attendee_last_name_info": "Příjmení rezervující osoby",
"your_monthly_digest": "Váš měsíční přehled",
"member_name": "Jméno člena",
"most_popular_events": "Nejoblíbenější události",
"summary_of_events_for_your_team_for_the_last_30_days": "Zde je přehled oblíbených událostí vašeho týmu {{teamName}} za posledních 30 dní",
"me": "Já",
"monthly_digest_email": "E-mail s měsíčním přehledem",
"monthly_digest_email_for_teams": "Měsíční e-mail s přehledem pro týmy",
"verify_team_tooltip": "Proveďte ověření svého týmu a zapněte odesílání zpráv účastníkům",
"member_removed": "Člen odstraněn",
"my_availability": "Moje dostupnost",
@ -2039,13 +2064,41 @@
"team_no_event_types": "Tento tým nemá žádné typy událostí",
"seat_options_doesnt_multiple_durations": "Volba místa nepodporuje více dob trvání",
"include_calendar_event": "Zahrnout událost kalendáře",
"oAuth": "OAuth",
"recently_added": "Nedávno přidáno",
"no_members_found": "Nenalezeni žádní členové",
"event_setup_length_error": "Nastavení události: Doba trvání musí být alespoň 1 minuta.",
"availability_schedules": "Plány dostupnosti",
"unauthorized": "Neautorizováno",
"access_cal_account": "{{clientName}} žádá o přístup k vašemu účtu {{appName}}",
"select_account_team": "Vyberte účet nebo tým",
"allow_client_to": "To umožní klientovi {{clientName}}",
"associate_with_cal_account": "Přiřadit vám vaše osobní údaje z klienta {{clientName}}",
"see_personal_info": "Zobrazit vaše osobní údaje, včetně všech osobních údajů, které jste zveřejnili",
"see_primary_email_address": "Zobrazit vaši primární e-mailovou adresu",
"connect_installed_apps": "Připojit se k vašim nainstalovaným aplikacím",
"access_event_type": "Číst, upravovat a odstraňovat vaše typy událostí",
"access_availability": "Číst, upravovat a odstraňovat vaši dostupnost",
"access_bookings": "Číst, upravovat a odstraňovat rezervace",
"allow_client_to_do": "Chcete povolit klientovi {{clientName}} provádět uvedené akce?",
"oauth_access_information": "Kliknutím na tlačítko Povolit této aplikaci umožníte používat vaše údaje v souladu s podmínkami služby a zásadami ochrany osobních údajů. Přístup k aplikaci {{appName}} můžete odebrat přes App Store.",
"allow": "Povolit",
"view_only_edit_availability_not_onboarded": "Tento uživatel ještě nedokončil onboarding. Dokud nedokončí onboarding, dostupnost nebude možné nastavit.",
"view_only_edit_availability": "Právě máte zobrazenou dostupnost tohoto uživatele. Upravovat lze pouze vlastní dostupnost.",
"you_can_override_calendar_in_advanced_tab": "Tuto možnost můžete zrušit pro každou událost zvlášť v pokročilém nastavení jednotlivých typů událostí.",
"edit_users_availability": "Upravte dostupnost uživatele: {{username}}",
"resend_invitation": "Znovu odeslat pozvánku",
"invitation_resent": "Pozvánka byla odeslána znovu.",
"add_client": "Přidat klienta",
"copy_client_secret_info": "Po zkopírování již nebude možné tajný klíč zobrazit",
"add_new_client": "Přidat nového klienta",
"this_app_is_not_setup_already": "Tato aplikace ještě nebyla nastavena",
"as_csv": "jako CSV",
"overlay_my_calendar": "Překryv mého kalendáře",
"overlay_my_calendar_toc": "Připojením ke svému kalendáři přijímáte naše zásady ochrany osobních údajů a podmínky používání. Přístup můžete kdykoli odvolat.",
"view_overlay_calendar_events": "Zobrazení událostí v kalendáři, aby se zabránilo kolidujícím rezervacím.",
"lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace",
"description_lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace (užitečné pro osobní události).",
"extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -717,6 +717,7 @@
"next_step": "Spring trin over",
"prev_step": "Forrige trin",
"install": "Installér",
"install_paid_app": "Abonnér",
"installed": "Installeret",
"active_install_one": "{{count}} aktiv installation",
"active_install_other": "{{count}} aktive installationer",

View File

@ -268,6 +268,7 @@
"set_availability": "Verfügbarkeit festlegen",
"availability_settings": "Verfügbarkeitseinstellungen",
"continue_without_calendar": "Ohne Kalender fortfahren",
"continue_with": "Mit {{appName}} fortfahren",
"connect_your_calendar": "Kalender verbinden",
"connect_your_video_app": "Verbinden Sie Ihre Video-Apps",
"connect_your_video_app_instructions": "Verbinden Sie Ihre Video-Apps, um sie für Ihre Termintypen zu verwenden.",
@ -289,6 +290,7 @@
"where": "Wo",
"add_to_calendar": "Zum Kalender hinzufügen",
"add_to_calendar_description": "Legen Sie fest, wo neue Termine hinzugefügt werden sollen, wenn Sie gebucht werden.",
"add_events_to": "Termine hinzufügen zu",
"add_another_calendar": "Einen weiteren Kalender hinzufügen",
"other": "Sonstige",
"email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}",
@ -423,6 +425,7 @@
"booking_created": "Termin erstellt",
"booking_rejected": "Termin abgelehnt",
"booking_requested": "Buchung angefragt",
"booking_payment_initiated": "Buchungszahlung eingeleitet",
"meeting_ended": "Meeting beendet",
"form_submitted": "Formular gesendet",
"booking_paid": "Buchung bezahlt",
@ -457,6 +460,7 @@
"no_event_types_have_been_setup": "Dieser Benutzer hat noch keine Termintypen eingerichtet.",
"edit_logo": "Logo bearbeiten",
"upload_a_logo": "Logo hochladen",
"upload_logo": "Logo hochladen",
"remove_logo": "Logos entfernen",
"enable": "Aktivieren",
"code": "Code",
@ -569,6 +573,7 @@
"your_team_name": "Ihr Teamname",
"team_updated_successfully": "Team erfolgreich aktualisiert",
"your_team_updated_successfully": "Ihr Team wurde erfolgreich aktualisiert.",
"your_org_updated_successfully": "Ihre Org wurde erfolgreich aktualisiert.",
"about": "Beschreibung",
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
"org_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der URL-Seite Ihrer Organisation erscheinen.",
@ -781,6 +786,7 @@
"disable_guests": "Gäste deaktivieren",
"disable_guests_description": "Das Hinzufügen zusätzlicher Gäste deaktivieren.",
"private_link": "Privaten Link generieren",
"enable_private_url": "Private URL aktivieren",
"private_link_label": "Privater Link",
"private_link_hint": "Ihr privater Link wird nach jeder Nutzung neu generiert",
"copy_private_link": "Privaten Link kopieren",
@ -843,6 +849,7 @@
"next_step": "Schritt überspringen",
"prev_step": "Vorheriger Schritt",
"install": "Installieren",
"install_paid_app": "Abonnieren",
"installed": "Installiert",
"active_install_one": "{{count}} aktive Installation",
"active_install_other": "{{count}} aktive Installationen",
@ -1216,6 +1223,7 @@
"organizer_name_variable": "Organisator Name",
"app_upgrade_description": "Um diese Funktion nutzen zu können, müssen Sie ein Upgrade auf einen Pro-Account durchführen.",
"invalid_number": "Ungültige Telefonnummer",
"invalid_url_error_message": "Ungültige URL für {{label}}. Beispiel-URL: {{sampleUrl}}",
"navigate": "Navigieren",
"open": "Öffnen",
"close": "Schließen",
@ -1290,6 +1298,7 @@
"select_calendars": "Wählen Sie aus, in welchen Kalendern Sie nach Konflikten suchen wollen, um Doppelbuchungen zu vermeiden.",
"check_for_conflicts": "Auf Konflikte prüfen",
"view_recordings": "Aufnahmen anzeigen",
"check_for_recordings": "Nach Aufnahmen suchen",
"adding_events_to": "Termine hinzufügen zu",
"follow_system_preferences": "Systemeinstellungen folgen",
"custom_brand_colors": "Eigene Marken-Farben",
@ -1599,6 +1608,7 @@
"options": "Optionen",
"enter_option": "Option {{index}} eingeben",
"add_an_option": "Option hinzufügen",
"location_already_exists": "Dieser Standort existiert bereits. Bitte wählen Sie einen neuen Standort",
"radio": "Radio",
"google_meet_warning": "Um Google Meet nutzen zu können, müssen Sie Ihren Zielkalender zu einem Google Calendar ändern",
"individual": "Person",
@ -1618,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "An ausgewählten Daten als „nicht verfügbar“ markieren",
"date_overrides_add_btn": "Überschreibung hinzufügen",
"date_overrides_update_btn": "Überschreiben aktualisieren",
"date_successfully_added": "Datumsüberbrückung erfolgreich hinzugefügt",
"event_type_duplicate_copy_text": "{{slug}}-Kopie",
"set_as_default": "Als Standard festlegen",
"hide_eventtype_details": "Ereignistyp-Einzelheiten ausblenden",
@ -2062,6 +2073,7 @@
"access_cal_account": "{{clientName}} möchte auf Ihr {{appName}} Konto zugreifen",
"select_account_team": "Konto oder Team auswählen",
"allow_client_to": "Dies wird {{clientName}} erlauben",
"associate_with_cal_account": "Verknüpfen Sie sich mit Ihren persönlichen Daten von {{clientName}}",
"see_personal_info": "Ihre persönlichen Daten einzusehen, einschließlich persönlicher Informationen, die Sie öffentlich zugänglich gemacht haben",
"see_primary_email_address": "Ihre primäre E-Mail-Adresse einzusehen",
"connect_installed_apps": "Sich mit Ihren installierten Apps zu verbinden",
@ -2069,13 +2081,24 @@
"access_availability": "Lesen, Bearbeiten, Löschen Ihrer Verfügbarkeiten",
"access_bookings": "Lesen, Bearbeiten, Löschen Ihrer Termine",
"allow_client_to_do": "{{clientName}} zulassen, dies zu tun?",
"oauth_access_information": "Indem Sie auf „Erlauben“ klicken, erlauben Sie dieser App, Ihre Informationen gemäß ihrer Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können den Zugriff im {{appName}} App Store aufheben.",
"allow": "Zulassen",
"view_only_edit_availability_not_onboarded": "Dieser Benutzer hat das Onboarding noch nicht abgeschlossen. Sie können seine Verfügbarkeit erst festlegen, wenn er das Onboarding abgeschlossen hat.",
"view_only_edit_availability": "Sie sehen die Verfügbarkeit dieses Benutzers. Sie können nur Ihre eigene Verfügbarkeit bearbeiten.",
"you_can_override_calendar_in_advanced_tab": "Sie können dies in den erweiterten Einstellungen pro Termin für jeden Termintyp überschreiben.",
"edit_users_availability": "Benutzerverfügbarkeit bearbeiten: {{username}}",
"resend_invitation": "Einladung erneut senden",
"invitation_resent": "Die Einladung wurde erneut gesendet.",
"add_client": "Kunde hinzufügen",
"copy_client_secret_info": "Nach dem Kopieren des Geheimnisses können Sie es nicht mehr ansehen",
"add_new_client": "Neuen Kunden hinzufügen",
"this_app_is_not_setup_already": "Diese App wurde noch nicht eingerichtet",
"as_csv": "als CSV",
"overlay_my_calendar": "Meinen Kalender überlagern",
"overlay_my_calendar_toc": "Durch das Verbinden mit Ihrem Kalender akzeptieren Sie unsere Datenschutzerklärung und Nutzungsbedingungen. Sie können den Zugriff jederzeit widerrufen.",
"view_overlay_calendar_events": "Sehen Sie sich Ihre Kalendertermine an, um Buchungskonflikte zu vermeiden.",
"lock_timezone_toggle_on_booking_page": "Zeitzone auf der Buchungsseite sperren",
"description_lock_timezone_toggle_on_booking_page": "Um die Zeitzone auf der Buchungsseite zu sperren, nützlich für Termine in Person.",
"extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -56,6 +56,15 @@
"a_refund_failed": "A refund failed",
"awaiting_payment_subject": "Awaiting Payment: {{title}} on {{date}}",
"meeting_awaiting_payment": "Your meeting is awaiting payment",
"payment_not_created_error": "Payment could not be created",
"couldnt_charge_card_error": "Could not charge card for Payment",
"no_available_users_found_error": "No available users found. Could you try another time slot?",
"request_body_end_time_internal_error": "Internal Error. Request body does not contain end time",
"create_calendar_event_error": "Unable to create Calendar event in Organizer's calendar",
"update_calendar_event_error": "Unable to update Calendar event.",
"delete_calendar_event_error": "Unable to delete Calendar event.",
"already_signed_up_for_this_booking_error": "You are already signed up for this booking.",
"hosts_unavailable_for_booking": "Some of the hosts are unavailable for booking.",
"help": "Help",
"price": "Price",
"paid": "Paid",
@ -849,6 +858,8 @@
"next_step": "Skip step",
"prev_step": "Prev step",
"install": "Install",
"install_paid_app": "Subscribe",
"start_paid_trial": "Start free Trial",
"installed": "Installed",
"active_install_one": "{{count}} active install",
"active_install_other": "{{count}} active installs",
@ -1361,6 +1372,7 @@
"event_name_info": "The event type name",
"event_date_info": "The event date",
"event_time_info": "The event start time",
"event_type_not_found": "EventType not Found",
"location_info": "The location of the event",
"additional_notes_info": "The additional notes of booking",
"attendee_name_info": "The person booking's name",

View File

@ -268,6 +268,7 @@
"set_availability": "Establecer Disponibilidad",
"availability_settings": "Configuración de disponibilidad",
"continue_without_calendar": "Continuar sin calendario",
"continue_with": "Continuar con {{appName}}",
"connect_your_calendar": "Conecta tu calendario",
"connect_your_video_app": "Conecte sus aplicaciones favoritas",
"connect_your_video_app_instructions": "Conecte sus aplicaciones de vídeo para usarlas en sus tipos de eventos.",
@ -288,6 +289,8 @@
"when": "Cuándo",
"where": "Donde",
"add_to_calendar": "Añadir al Calendario",
"add_to_calendar_description": "Seleccione dónde se añadirán los eventos cuando haya reservado.",
"add_events_to": "Agregar eventos a",
"add_another_calendar": "Añadir otro calendario",
"other": "Otro",
"email_sign_in_subject": "Su enlace de inicio de sesión para {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Reserva Creada",
"booking_rejected": "Reserva rechazada",
"booking_requested": "Reserva solicitada",
"booking_payment_initiated": "Pago de la reserva iniciado",
"meeting_ended": "Reunión finalizada",
"form_submitted": "Formulario enviado",
"booking_paid": "Reserva pagada",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Este usuario aún no ha configurado ningún tipo de evento.",
"edit_logo": "Cambiar la marca",
"upload_a_logo": "Subir una marca",
"upload_logo": "Cargar logo",
"remove_logo": "Quitar logo",
"enable": "Habilitar",
"code": "Código",
@ -568,6 +573,7 @@
"your_team_name": "Nombre de tu Equipo",
"team_updated_successfully": "Equipo Actualizado Correctamente",
"your_team_updated_successfully": "Tu equipo ha sido actualizado con éxito.",
"your_org_updated_successfully": "Su organización se ha actualizado correctamente.",
"about": "Acerca de",
"team_description": "Comentarios sobre tu equipo. Esta información aparecerá en la página de la URL de tu equipo.",
"org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Ocultar el botón Reservar un miembro del equipo",
"hide_book_a_team_member_description": "Ocultar el botón Reservar un miembro del equipo de sus páginas públicas.",
"danger_zone": "Paso definitivo",
"account_deletion_cannot_be_undone": "Tenga cuidado. La eliminación de la cuenta no se puede deshacer.",
"back": "Atrás",
"cancel": "Cancelar",
"cancel_all_remaining": "Cancelar todos los restantes",
@ -688,6 +695,7 @@
"people": "Personas",
"your_email": "Tu Email",
"change_avatar": "Cambiar Avatar",
"upload_avatar": "Cargar avatar",
"language": "Lenguaje",
"timezone": "Zona Horaria",
"first_day_of_week": "Primer dia de la semana",
@ -778,6 +786,7 @@
"disable_guests": "Desactivar Invitados",
"disable_guests_description": "Desactiva agregar invitados adicionales al hacer la reserva.",
"private_link": "Generar una URL privada",
"enable_private_url": "Activar URL privada",
"private_link_label": "Enlace privado",
"private_link_hint": "Su enlace privado se regenera después de cada uso",
"copy_private_link": "Copiar enlace privado",
@ -840,6 +849,7 @@
"next_step": "Saltar paso",
"prev_step": "Paso anterior",
"install": "Instalar",
"install_paid_app": "Suscribirse",
"installed": "Instalado",
"active_install_one": "{{count}} instalación activa",
"active_install_other": "{{count}} instalaciones activas",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Nombre del organizador",
"app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.",
"invalid_number": "Número de teléfono no válido",
"invalid_url_error_message": "URL no válida para {{label}}. URL de ejemplo: {{sampleUrl}}",
"navigate": "Navegar",
"open": "Abrir",
"close": "Cerrar",
@ -1276,6 +1287,7 @@
"personal_cal_url": "Mi URL personal de {{appName}}",
"bio_hint": "Algunas frases sobre usted, esta información aparecerá en la página de su URL personal.",
"user_has_no_bio": "Este usuario no ha añadido una biografía todavía.",
"bio": "Biografía",
"delete_account_modal_title": "Eliminar cuenta",
"confirm_delete_account_modal": "¿Está seguro que desea eliminar su cuenta de {{appName}}?",
"delete_my_account": "Eliminar mi cuenta",
@ -1286,6 +1298,7 @@
"select_calendars": "Seleccione los calendarios en los que desee comprobar conflictos para evitar reservas dobles.",
"check_for_conflicts": "Comprobar conflictos",
"view_recordings": "Ver grabaciones",
"check_for_recordings": "Comprobar si hay grabaciones",
"adding_events_to": "Agregando eventos a",
"follow_system_preferences": "Siga las preferencias del sistema",
"custom_brand_colors": "Colores de marca personalizados",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "Hubo un problema con el registro del subdominio, intente nuevamente o comuníquese con un administrador",
"team_publish": "Publicar equipo",
"number_text_notifications": "Número de teléfono (Notificaciones de texto)",
"number_sms_notifications": "Número de teléfono (notificaciones SMS)",
"attendee_email_variable": "Correo electrónico del asistente",
"attendee_email_info": "El correo electrónico de la persona que reserva",
"kbar_search_placeholder": "Escriba un comando o búsqueda...",
@ -1594,6 +1608,7 @@
"options": "Opciones",
"enter_option": "Introduzca la opción {{index}}",
"add_an_option": "Agregue una opción",
"location_already_exists": "Esta ubicación ya existe. Seleccione una ubicación nueva",
"radio": "Botón radial",
"google_meet_warning": "Para usar Google Meet, debe establecer un Google Calendar como calendario de destino",
"individual": "Individuo",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Marcar como no disponible en las fechas seleccionadas",
"date_overrides_add_btn": "Agregar anulación",
"date_overrides_update_btn": "Actualizar anulación",
"date_successfully_added": "Sustitución de fechas añadida correctamente",
"event_type_duplicate_copy_text": "{{slug}}-copia",
"set_as_default": "Establecer como predeterminado",
"hide_eventtype_details": "Ocultar detalles del tipo de evento",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Número de anfitriones requeridos para asistir",
"hosts": "Anfitriones",
"upgrade_to_enable_feature": "Debe crear un equipo para activar esta función. Haga clic para crear un equipo.",
"orgs_upgrade_to_enable_feature": "Debe pasarse a nuestro plan Enterprise para habilitar esta función.",
"new_attendee": "Nuevo asistente",
"awaiting_approval": "En espera de aprobación",
"requires_google_calendar": "Esta aplicación requiere una conexión con Google Calendar",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Mostrar en la página de reserva",
"get_started_zapier_templates": "Comience con las plantillas de Zapier",
"team_is_unpublished": "{{team}} no está publicado",
"org_is_unpublished_description": "El enlace de esta organización no está disponible actualmente. Comuníquese con el propietario de la organización o pídale que lo publique.",
"team_is_unpublished_description": "Este enlace de {{entity}} no está disponible actualmente. Póngase en contacto con el propietario de {{entity}} o pídale que lo publique.",
"team_member": "Miembro del equipo",
"a_routing_form": "Un formulario de enrutamiento",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Editar ajustes de enlace",
"invite_link_copied": "Enlace de invitación copiado",
"invite_link_deleted": "Enlace de invitación eliminado",
"api_key_deleted": "Clave API eliminada",
"invite_link_updated": "Configuración de enlace de invitación guardada",
"link_expires_after": "Enlaces establecidos para expirar después de...",
"one_day": "1 día",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Apellido del asistente",
"attendee_first_name_info": "Nombre de la persona que reserva",
"attendee_last_name_info": "Apellido de la persona que reserva",
"your_monthly_digest": "Su resumen mensual",
"member_name": "Nombre del miembro",
"most_popular_events": "Eventos más populares",
"summary_of_events_for_your_team_for_the_last_30_days": "Este es el resumen de los eventos populares de su equipo {{teamName}} durante los últimos 30 días",
"me": "Yo",
"monthly_digest_email": "Correo electrónico del resumen mensual",
"monthly_digest_email_for_teams": "Correo electrónico de resumen mensual para equipos",
"verify_team_tooltip": "Verifique su equipo para activar el envío de mensajes a los asistentes",
"member_removed": "Miembro eliminado",
"my_availability": "Mi disponibilidad",
@ -2039,13 +2064,41 @@
"team_no_event_types": "Este equipo no tiene tipos de eventos",
"seat_options_doesnt_multiple_durations": "La opción Cupo no soporta múltiples duraciones",
"include_calendar_event": "Incluir evento del calendario",
"oAuth": "OAuth",
"recently_added": "Añadido recientemente",
"no_members_found": "No se encontraron miembros",
"event_setup_length_error": "Configuración del evento: la duración debe ser de al menos 1 minuto.",
"availability_schedules": "Horarios de disponibilidad",
"unauthorized": "Sin autorización",
"access_cal_account": "{{clientName}} quiere acceder a su cuenta de {{appName}}",
"select_account_team": "Seleccionar cuenta o equipo",
"allow_client_to": "Esto permitirá que {{clientName}}",
"associate_with_cal_account": "Lo asocie con su información personal de {{clientName}}",
"see_personal_info": "Consulte su información personal, incluida la información personal que haya hecho pública",
"see_primary_email_address": "Consulte su dirección de correo electrónico principal",
"connect_installed_apps": "Se conecte a sus aplicaciones instaladas",
"access_event_type": "Lea, edite y elimine sus tipos de eventos",
"access_availability": "Lea, edite y elimine su disponibilidad",
"access_bookings": "Lea, edite y elimine sus reservas",
"allow_client_to_do": "¿Desea permitir que {{clientName}} haga esto?",
"oauth_access_information": "Al hacer clic en permitir, permite que esta aplicación utilice su información de acuerdo con sus términos de servicio y política de privacidad. Puede eliminar el acceso en la App Store de {{appName}}.",
"allow": "Permitir",
"view_only_edit_availability_not_onboarded": "Este usuario no ha completado la incorporación. No podrá establecer su disponibilidad hasta que haya completado la incorporación.",
"view_only_edit_availability": "Está viendo la disponibilidad de este usuario. Sólo puede editar su propia disponibilidad.",
"you_can_override_calendar_in_advanced_tab": "Puede anular esto por evento en la Configuración avanzada de cada tipo de evento.",
"edit_users_availability": "Editar disponibilidad del usuario: {{username}}",
"resend_invitation": "Reenviar invitación",
"invitation_resent": "Se reenvió la invitación.",
"add_client": "Agregar cliente",
"copy_client_secret_info": "Después de copiar el secreto, ya no podrá volver a verlo",
"add_new_client": "Agregar nuevo cliente",
"this_app_is_not_setup_already": "Esta aplicación aún no se ha configurado",
"as_csv": "como CSV",
"overlay_my_calendar": "Superponer mi calendario",
"overlay_my_calendar_toc": "Al conectarse a su calendario, acepta nuestra política de privacidad y nuestros términos de uso. Puede revocar el acceso en cualquier momento.",
"view_overlay_calendar_events": "Consulte los eventos de su calendario para evitar el conflicto de reservas.",
"lock_timezone_toggle_on_booking_page": "Bloquear la zona horaria en la página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Bloquear la zona horaria en la página de reserva, es útil para eventos en persona.",
"extensive_whitelabeling": "Asistencia dedicada en materia de incorporación e ingeniería",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -506,6 +506,7 @@
"next_step": "Saltatu pausoa",
"prev_step": "Aurreko pausoa",
"install": "Instalatu",
"install_paid_app": "Harpidetu",
"installed": "Instalatua",
"disconnect": "Deskonektatu",
"automation": "Automatizazioa",

View File

@ -289,6 +289,8 @@
"when": "Quand",
"where": "Où",
"add_to_calendar": "Ajouter au calendrier",
"add_to_calendar_description": "Sélectionnez l'endroit où ajouter des événements lorsque vous êtes réservé.",
"add_events_to": "Ajouter les événements à",
"add_another_calendar": "Ajouter un autre calendrier",
"other": "Autre",
"email_sign_in_subject": "Votre lien de connexion pour {{appName}}",
@ -450,13 +452,14 @@
"go_to_billing_portal": "Accéder au portail de facturation",
"need_anything_else": "Besoin d'autre chose ?",
"further_billing_help": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.",
"contact": "Contact",
"contact": "Contacter",
"our_support_team": "notre équipe d'assistance",
"contact_our_support_team": "Contactez notre équipe d'assistance",
"uh_oh": "Oups !",
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de type d'événement.",
"edit_logo": "Modifier le logo",
"upload_a_logo": "Télécharger un logo",
"upload_logo": "Télécharger un logo",
"remove_logo": "Supprimer le logo",
"enable": "Activer",
"code": "Code",
@ -569,6 +572,7 @@
"your_team_name": "Nom de votre équipe",
"team_updated_successfully": "Équipe mise à jour avec succès",
"your_team_updated_successfully": "Votre équipe a été mise à jour avec succès.",
"your_org_updated_successfully": "Votre organisation a été mise à jour avec succès.",
"about": "À propos",
"team_description": "Quelques mots à propos de votre équipe. Ces informations apparaîtront sur la page publique de votre équipe.",
"org_description": "Quelques phrases à propos de votre organisation. Elles apparaîtront sur la page de profil public de votre organisation.",
@ -844,6 +848,8 @@
"next_step": "Passer l'étape",
"prev_step": "Étape précédente",
"install": "Installer",
"install_paid_app": "S'abonner",
"start_paid_trial": "Démarrer l'essai gratuit",
"installed": "Installée",
"active_install_one": "{{count}} installation active",
"active_install_other": "{{count}} installations actives",
@ -2065,12 +2071,18 @@
"access_bookings": "Lire, modifier, supprimer vos réservations",
"allow_client_to_do": "Autoriser {{clientName}} à faire cela ?",
"allow": "Autoriser",
"you_can_override_calendar_in_advanced_tab": "Vous pouvez modifier ceci pour chaque événement dans les paramètres avancés de chaque type d'événement.",
"edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}",
"resend_invitation": "Renvoyer l'invitation",
"invitation_resent": "L'invitation a été renvoyée.",
"add_client": "Ajouter un client",
"add_new_client": "Ajouter un nouveau client",
"as_csv": "au format CSV",
"overlay_my_calendar": "Superposer mon calendrier",
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
"extensive_whitelabeling": "Marque blanche étendue",
"unlimited_teams": "Équipes illimitées",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -840,6 +840,7 @@
"next_step": "לדלג על שלב זה",
"prev_step": "לשלב הקודם",
"install": "התקנה",
"install_paid_app": "הרשמה למינוי",
"installed": "מותקן",
"active_install_one": "התקנה פעילה {{count}}",
"active_install_other": "{{count}} התקנות פעילות",

View File

@ -268,6 +268,7 @@
"set_availability": "Imposta la tua disponibilità",
"availability_settings": "Impostazioni di disponibilità",
"continue_without_calendar": "Continua senza calendario",
"continue_with": "Continua con {{appName}}",
"connect_your_calendar": "Collega il tuo calendario",
"connect_your_video_app": "Collega le tue applicazioni video",
"connect_your_video_app_instructions": "Collega le tue applicazioni video per utilizzarle per i tuoi tipi di evento.",
@ -288,6 +289,8 @@
"when": "Quando",
"where": "Dove",
"add_to_calendar": "Aggiungi al calendario",
"add_to_calendar_description": "Seleziona dove aggiungere eventi quando c'è una prenotazione.",
"add_events_to": "Aggiungi eventi a",
"add_another_calendar": "Aggiungi un altro calendario",
"other": "Altro",
"email_sign_in_subject": "Link di accesso a {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Prenotazione Creata",
"booking_rejected": "Prenotazione Rifiutata",
"booking_requested": "Richiesta di prenotazione inviata",
"booking_payment_initiated": "Pagamento della prenotazione iniziato",
"meeting_ended": "Riunione terminata",
"form_submitted": "Modulo inviato",
"booking_paid": "Prenotazione pagata",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Questo utente non ha ancora impostato alcun tipo di evento.",
"edit_logo": "Modifica logo",
"upload_a_logo": "Carica un logo",
"upload_logo": "Carica logo",
"remove_logo": "Rimuovi logo",
"enable": "Abilita",
"code": "Codice",
@ -568,6 +573,7 @@
"your_team_name": "Nome del tuo team",
"team_updated_successfully": "Team aggiornato con successo",
"your_team_updated_successfully": "Il tuo team è stato aggiornato con successo.",
"your_org_updated_successfully": "La tua organizzazione è stata aggiornata con successo.",
"about": "Informazioni",
"team_description": "Alcune frasi sul tuo team. Appariranno nella pagina URL del team.",
"org_description": "Qualche informazione sulla tua organizzazione. Le informazioni appariranno nella pagina di profilo dell'organizzazione.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Nascondi pulsante Prenota un membro del team",
"hide_book_a_team_member_description": "Nasconde il pulsante Prenota un membro del team nelle pagine pubbliche.",
"danger_zone": "Zona pericolosa",
"account_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione dell'account.",
"back": "Indietro",
"cancel": "Annulla",
"cancel_all_remaining": "Annulla tutti i rimanenti",
@ -688,6 +695,7 @@
"people": "Persone",
"your_email": "La Tua Email",
"change_avatar": "Cambia Avatar",
"upload_avatar": "Carica avatar",
"language": "Lingua",
"timezone": "Timezone",
"first_day_of_week": "Primo giorno della settimana",
@ -778,6 +786,7 @@
"disable_guests": "Disabilita Ospiti",
"disable_guests_description": "Disabilita l'aggiunta di ulteriori ospiti durante la prenotazione.",
"private_link": "Genera URL privato",
"enable_private_url": "Abilita URL privato",
"private_link_label": "Link privato",
"private_link_hint": "Il tuo link privato verrà rigenerato dopo ogni utilizzo",
"copy_private_link": "Copia link privato",
@ -840,6 +849,7 @@
"next_step": "Salta passo",
"prev_step": "Passo precedente",
"install": "Installa",
"install_paid_app": "Abbonati",
"installed": "Installato",
"active_install_one": "{{count}} installazione attiva",
"active_install_other": "{{count}} installazioni attive",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Nome organizzatore",
"app_upgrade_description": "Per poter utilizzare questa funzionalità, è necessario passare a un account Pro.",
"invalid_number": "Numero di telefono non valido",
"invalid_url_error_message": "URL non valido per {{label}}. URL di esempio: {{sampleUrl}}",
"navigate": "Esplora",
"open": "Apri",
"close": "Chiudi",
@ -1276,6 +1287,7 @@
"personal_cal_url": "Il mio URL personale di {{appName}}",
"bio_hint": "Scrivi qualcosa di te. Queste informazioni appariranno nella tua pagina personale.",
"user_has_no_bio": "Questo utente non ha ancora aggiunto una biografia.",
"bio": "Biografia",
"delete_account_modal_title": "Elimina account",
"confirm_delete_account_modal": "Eliminare il tuo account {{appName}}?",
"delete_my_account": "Elimina il mio account",
@ -1286,6 +1298,7 @@
"select_calendars": "Seleziona su quali calendari desideri controllare i conflitti per evitare doppie prenotazioni.",
"check_for_conflicts": "Controlla conflitti",
"view_recordings": "Visualizza registrazioni",
"check_for_recordings": "Controlla le registrazioni",
"adding_events_to": "Aggiungendo eventi a",
"follow_system_preferences": "Segui le preferenze di sistema",
"custom_brand_colors": "Colori del marchio personalizzati",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "Si è verificato un problema durante la registrazione del sottodominio, riprova o contatta un amministratore",
"team_publish": "Pubblica team",
"number_text_notifications": "Numero di telefono (notifiche di testo)",
"number_sms_notifications": "Numero di telefono (notifiche SMS)",
"attendee_email_variable": "E-mail partecipante",
"attendee_email_info": "E-mail della persona che prenota",
"kbar_search_placeholder": "Digita un comando o esegui una ricerca...",
@ -1594,6 +1608,7 @@
"options": "Opzioni",
"enter_option": "Immetti opzione {{index}}",
"add_an_option": "Aggiungi un'opzione",
"location_already_exists": "Questa posizione esiste già. Seleziona una nuova posizione",
"radio": "Pulsante di opzione",
"google_meet_warning": "Per usare Google Meet, è necessario impostare Google Calendar come calendario di destinazione",
"individual": "Persona",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Segna come non disponibile nelle date selezionate",
"date_overrides_add_btn": "Aggiungi configurazione data specifica",
"date_overrides_update_btn": "Aggiorna configurazione data specifica",
"date_successfully_added": "Configurazione delle date aggiunta correttamente",
"event_type_duplicate_copy_text": "{{slug}}-copia",
"set_as_default": "Imposta come predefinito",
"hide_eventtype_details": "Nascondi dettagli del tipo di evento",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Numero di organizzatori necessario per partecipare",
"hosts": "Organizzatori",
"upgrade_to_enable_feature": "Per abilitare questa funzione, è necessario creare un team. Fai clic per creare un team.",
"orgs_upgrade_to_enable_feature": "Per abilitare questa funzione, è necessario eseguire l'upgrade al nostro piano Enterprise.",
"new_attendee": "Nuovo partecipante",
"awaiting_approval": "In attesa di approvazione",
"requires_google_calendar": "L'app richiede una connessione a Google Calendar",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Mostra nella pagina di prenotazione",
"get_started_zapier_templates": "Inizia con i modelli Zapier",
"team_is_unpublished": "{{team}} non è pubblicato",
"org_is_unpublished_description": "Il link di questa organizzazione non è attualmente disponibile. Contatta il proprietario dell'organizzazione o chiedigli di pubblicarlo.",
"team_is_unpublished_description": "Questo link di {{entity}} non è attualmente disponibile. Contatta il proprietario di {{entity}} o chiedigli di pubblicarlo.",
"team_member": "Membro del team",
"a_routing_form": "Un modulo di instradamento",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Modifica impostazioni link",
"invite_link_copied": "Link d'invito copiato",
"invite_link_deleted": "Link d'invito eliminato",
"api_key_deleted": "Chiave API eliminata",
"invite_link_updated": "Impostazioni link d'invito salvate",
"link_expires_after": "La scadenza dei link è impostata dopo...",
"one_day": "1 giorno",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Cognome del partecipante",
"attendee_first_name_info": "Nome della persona che prenota",
"attendee_last_name_info": "Cognome della persona che prenota",
"your_monthly_digest": "Riepilogo mensile",
"member_name": "Nome del membro",
"most_popular_events": "Eventi più popolari",
"summary_of_events_for_your_team_for_the_last_30_days": "Ecco il riepilogo degli eventi popolari per il tuo team {{teamName}} negli ultimi 30 giorni",
"me": "Io",
"monthly_digest_email": "E-mail riepilogo mensile",
"monthly_digest_email_for_teams": "E-mail di riepilogo mensili per i team",
"verify_team_tooltip": "Effettua la verifica del tuo team per abilitare l'invio di messaggi ai partecipanti",
"member_removed": "Membro rimosso",
"my_availability": "La mia disponibilità",
@ -2039,13 +2064,41 @@
"team_no_event_types": "Questo team non ha nessun tipo di evento",
"seat_options_doesnt_multiple_durations": "L'opzione di prenotazione dei posti non supporta durate multiple",
"include_calendar_event": "Includi evento del calendario",
"oAuth": "OAuth",
"recently_added": "Aggiunti di recente",
"no_members_found": "Nessun membro trovato",
"event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.",
"availability_schedules": "Calendario disponibilità",
"unauthorized": "Non autorizzato",
"access_cal_account": "{{clientName}} vorrebbe accedere al tuo account {{appName}}",
"select_account_team": "Seleziona un account o un team",
"allow_client_to": "Ciò consentirà a {{clientName}} di",
"associate_with_cal_account": "Associarti con i tuoi dati personali da {{clientName}}",
"see_personal_info": "Vedere i tuoi dati personali, inclusi i dati personali che hai reso disponibili al pubblico",
"see_primary_email_address": "Vedere il tuo indirizzo e-mail principale",
"connect_installed_apps": "Connettersi alle tue applicazioni installate",
"access_event_type": "Leggere, modificare, eliminare i tuoi tipi di eventi",
"access_availability": "Leggere, modificare, eliminare la tua disponibilità",
"access_bookings": "Leggere, modificare, eliminare le tue prenotazioni",
"allow_client_to_do": "Consentire a {{clientName}} di farlo?",
"oauth_access_information": "Facendo clic su Consenti, consentirai a questa applicazione di usare i tuoi dati in conformità ai suoi termini di servizio e informativa sulla privacy. Puoi revocare l'accesso nelle impostazioni di {{appName}} nell'App Store.",
"allow": "Consenti",
"view_only_edit_availability_not_onboarded": "Questo utente non ha completato l'onboarding. Non sarai in grado di impostare la sua disponibilità fino a quando non avrà completato l'onboarding.",
"view_only_edit_availability": "Stai visualizzando la disponibilità di questo utente. Puoi solo modificare la tua disponibilità.",
"you_can_override_calendar_in_advanced_tab": "Puoi configurare queste impostazioni su base per evento nelle impostazioni avanzate di ciascun tipo di evento.",
"edit_users_availability": "Modifica la disponibilità dell'utente: {{username}}",
"resend_invitation": "Invia di nuovo l'invito",
"invitation_resent": "L'invito è stato inviato di nuovo.",
"add_client": "Aggiungi cliente",
"copy_client_secret_info": "Dopo aver copiato questa parola segreta non sarai più in grado di vederla",
"add_new_client": "Aggiungi nuovo cliente",
"this_app_is_not_setup_already": "Questa applicazione non è stata ancora impostata",
"as_csv": "come CSV",
"overlay_my_calendar": "Sovrapponi il mio calendario",
"overlay_my_calendar_toc": "Collegando il tuo calendario, accetti la nostra informativa sulla privacy e i termini di servizio. Puoi revocare l'accesso in qualsiasi momento.",
"view_overlay_calendar_events": "Visualizza gli eventi del tuo calendario per prevenire prenotazioni in conflitto.",
"lock_timezone_toggle_on_booking_page": "Blocca fuso orario nella pagina di prenotazione",
"description_lock_timezone_toggle_on_booking_page": "Per bloccare il fuso orario nella pagina di prenotazione, utile per gli eventi di persona.",
"extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -268,6 +268,7 @@
"set_availability": "利用可否の設定",
"availability_settings": "利用できる設定",
"continue_without_calendar": "カレンダーなしで続行",
"continue_with": "{{appName}} で続行",
"connect_your_calendar": "カレンダーに接続",
"connect_your_video_app": "ビデオアプリを接続",
"connect_your_video_app_instructions": "ご利用のイベントの種類で動画アプリを使用するには、ビデオアプリを接続してください。",
@ -288,6 +289,8 @@
"when": "日時",
"where": "参加方法",
"add_to_calendar": "カレンダーに追加",
"add_to_calendar_description": "予約時にイベントを追加する場所を選びます。",
"add_events_to": "イベントの追加先",
"add_another_calendar": "別のカレンダーを追加",
"other": "その他",
"email_sign_in_subject": "{{appName}} のサインインリンク",
@ -422,6 +425,7 @@
"booking_created": "予約を作成しました",
"booking_rejected": "予約が拒否されました",
"booking_requested": "予約がリクエストされました",
"booking_payment_initiated": "予約に関するお支払いを開始しました",
"meeting_ended": "ミーティングが終了しました",
"form_submitted": "フォームが送信されました",
"booking_paid": "予約の支払いが済みました",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "このユーザーはまだイベントタイプを設定していません。",
"edit_logo": "ロゴを編集",
"upload_a_logo": "ロゴをアップロード",
"upload_logo": "ロゴをアップロード",
"remove_logo": "ロゴを削除する",
"enable": "有効にする",
"code": "コード",
@ -568,6 +573,7 @@
"your_team_name": "チーム名",
"team_updated_successfully": "チームが正常に更新されました",
"your_team_updated_successfully": "チームが正常に更新されました。",
"your_org_updated_successfully": "組織は正常に更新されました。",
"about": "このアプリについて",
"team_description": "あなたのチームについての簡単な説明文です。あなたのチームの URL ページに表示されます。",
"org_description": "あなたの組織についての簡単な説明文です。あなたの組織の URL ページに表示されます。",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "「チームメンバーを予約する」ボタンを非表示にする",
"hide_book_a_team_member_description": "公開ページから「チームメンバーを予約する」ボタンを非表示にします。",
"danger_zone": "危険ゾーン",
"account_deletion_cannot_be_undone": "ご注意ください。アカウントの削除は元に戻せません。",
"back": "戻る",
"cancel": "キャンセル",
"cancel_all_remaining": "残りをすべてキャンセル",
@ -688,6 +695,7 @@
"people": "ユーザー",
"your_email": "あなたのメールアドレス",
"change_avatar": "アバターを変更",
"upload_avatar": "アバターをアップロード",
"language": "言語",
"timezone": "タイムゾーン",
"first_day_of_week": "週の最初の日",
@ -778,6 +786,7 @@
"disable_guests": "ゲストを無効化",
"disable_guests_description": "予約中のゲストの追加を無効にします。",
"private_link": "プライベートリンクを生成",
"enable_private_url": "プライベート URL を有効にする",
"private_link_label": "プライベートリンク",
"private_link_hint": "プライベートリンクは使用するたびに再生成されます",
"copy_private_link": "プライベートリンクをコピー",
@ -840,6 +849,7 @@
"next_step": "手順をスキップ",
"prev_step": "前の手順",
"install": "インストール",
"install_paid_app": "サブスクライブ",
"installed": "インストール済み",
"active_install_one": "{{count}} 件のアクティブなインストール",
"active_install_other": "{{count}} 件のアクティブなインストール",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "主催者名",
"app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。",
"invalid_number": "電話番号が無効です",
"invalid_url_error_message": "{{label}} の無効な URL です。サンプル URL{{sampleUrl}}",
"navigate": "ナビゲート",
"open": "開く",
"close": "閉じる",
@ -1276,6 +1287,7 @@
"personal_cal_url": "私の個人 {{appName}} URL",
"bio_hint": "あなたに関する簡潔な説明。これはあなたの個人 URL ページに表示されます。",
"user_has_no_bio": "このユーザーはまだ経歴を追加していません。",
"bio": "経歴",
"delete_account_modal_title": "アカウントを削除する",
"confirm_delete_account_modal": "{{appName}} アカウントを削除してもよろしいですか?",
"delete_my_account": "アカウントを削除する",
@ -1286,6 +1298,7 @@
"select_calendars": "ダブルブッキングを防ぐために、スケジュールの重なりをチェックするカレンダーを選択してください。",
"check_for_conflicts": "スケジュールの重なりをチェック",
"view_recordings": "録音を表示",
"check_for_recordings": "レコーディングを確認",
"adding_events_to": "イベントの追加先",
"follow_system_preferences": "システム環境設定に従う",
"custom_brand_colors": "カスタムブランドカラー",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "サブドメインの登録時に問題が発生しました。もう一度お試しいただくか、管理者までお問い合せください",
"team_publish": "チームを公開",
"number_text_notifications": "電話番号 (テキスト通知)",
"number_sms_notifications": "電話番号SMS 通知)",
"attendee_email_variable": "出席者のメールアドレス",
"attendee_email_info": "予約者のメールアドレス",
"kbar_search_placeholder": "コマンドを入力するか、検索してください...",
@ -1594,6 +1608,7 @@
"options": "オプション",
"enter_option": "オプション {{index}} を入力してください",
"add_an_option": "オプションを追加",
"location_already_exists": "この場所はすでに存在します。新しい場所を選んでください",
"radio": "ラジオボタン",
"google_meet_warning": "Google Meet を使用するには、目的のカレンダーを Google カレンダーに設定する必要があります",
"individual": "個人",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "選択した日付を参加不可としてマーク",
"date_overrides_add_btn": "上書きを追加",
"date_overrides_update_btn": "上書きを更新",
"date_successfully_added": "日付の上書きが正常に追加されました",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "デフォルトとして設定",
"hide_eventtype_details": "イベントの種類の詳細を非表示にする",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "出席が必要なホストの数",
"hosts": "ホスト",
"upgrade_to_enable_feature": "この機能を有効にするには、チームを作成する必要があります。クリックしてチームを作成してください。",
"orgs_upgrade_to_enable_feature": "この機能を有効にするには Enterprise プランにアップグレードする必要があります。",
"new_attendee": "新規参加者",
"awaiting_approval": "承認を待っています",
"requires_google_calendar": "このアプリは Google カレンダーとの接続が必要です",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "予約ページに表示",
"get_started_zapier_templates": "Zapier テンプレートの使用を開始する",
"team_is_unpublished": "{{team}} は公開されていません",
"org_is_unpublished_description": "この組織のリンクは現在利用できません。組織の所有者に連絡するか、リンクを公開するよう依頼してください。",
"team_is_unpublished_description": "この {{entity}} のリンクは現在利用できません。{{entity}} の所有者に問い合わせるか、リンクを公開するように依頼してください。",
"team_member": "チームメンバー",
"a_routing_form": "ルーティングフォーム",
@ -1877,6 +1895,7 @@
"edit_invite_link": "リンクの設定を編集する",
"invite_link_copied": "招待リンクをコピーしました",
"invite_link_deleted": "招待リンクを削除しました",
"api_key_deleted": "API キーを削除しました",
"invite_link_updated": "招待リンクの設定を保存しました",
"link_expires_after": "リンクの期限切れまで...",
"one_day": "1 日",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "出席者の姓",
"attendee_first_name_info": "予約者の名",
"attendee_last_name_info": "予約者の姓",
"your_monthly_digest": "月 1 回のダイジェスト",
"member_name": "メンバーの名前",
"most_popular_events": "最も人気のイベント",
"summary_of_events_for_your_team_for_the_last_30_days": "こちらはこの 30 日間、チーム {{teamName}} で最も人気があったイベントのサマリーです",
"me": "私",
"monthly_digest_email": "月 1 回のダイジェストのメール",
"monthly_digest_email_for_teams": "チームのための月 1 回のダイジェストのメール",
"verify_team_tooltip": "出席者へのメッセージ送信ができるようにするには、チームを確認してください",
"member_removed": "メンバーが削除されました",
"my_availability": "私の空き状況",
@ -2039,13 +2064,41 @@
"team_no_event_types": "このチームにはイベントタイプはありません",
"seat_options_doesnt_multiple_durations": "座席オプションは複数の期間をサポートしていません",
"include_calendar_event": "カレンダーのイベントを含める",
"oAuth": "OAuth",
"recently_added": "最近追加されました",
"no_members_found": "メンバーが見つかりません",
"event_setup_length_error": "イベント設定:時間は 1 分以上でなくてはいけません。",
"availability_schedules": "空き状況一覧",
"unauthorized": "権限がありません",
"access_cal_account": "{{clientName}}があなたの {{appName}} アカウントへのアクセスを求めています",
"select_account_team": "アカウントまたはチームを選択",
"allow_client_to": "これにより {{clientName}} は",
"associate_with_cal_account": "あなたと {{clientName}} からのあなたの個人情報を関連づけられます",
"see_personal_info": "あなたの個人情報(あなたがこれまでに公開したあなたの個人情報など)を表示",
"see_primary_email_address": "プライマリメールアドレスを表示",
"connect_installed_apps": "インストールしたアプリに接続",
"access_event_type": "イベントタイプを読み取り、編集し、削除する",
"access_availability": "空き状況を読み取り、編集し、削除する",
"access_bookings": "予約を読み取り、編集し、削除する",
"allow_client_to_do": "{{clientName}} にこれの実行を許可しますか?",
"oauth_access_information": "「許可」をクリックすると、このアプリのサービス利用規約とプライバシーポリシーに従って、アプリにあなたの個人情報の使用を許可することになります。{{appName}} の App Store でアクセスを削除できます。",
"allow": "許可",
"view_only_edit_availability_not_onboarded": "このユーザーはオンボーディングを完了していません。オンボーディングを完了するまで、ユーザーの空き状況は設定できません。",
"view_only_edit_availability": "このユーザーの空き状況を表示しています。編集できるのは自分の空き状況だけです。",
"you_can_override_calendar_in_advanced_tab": "各イベントタイプの詳細設定で、イベントごとにこれを上書きすることができます。",
"edit_users_availability": "ユーザーの空き状況を編集:{{username}}",
"resend_invitation": "招待を再送",
"invitation_resent": "招待は再送されました。",
"add_client": "顧客を追加",
"copy_client_secret_info": "このシークレットをコピーすると、もう表示できなくなります",
"add_new_client": "新しい顧客を追加",
"this_app_is_not_setup_already": "このアプリはまだ設定されていません",
"as_csv": "CSV として",
"overlay_my_calendar": "カレンダーを重ね合わせる",
"overlay_my_calendar_toc": "カレンダーに接続することで、弊社のプライバシーポリシーと利用規約に同意することになります。アクセスはいつでも取り消せます。",
"view_overlay_calendar_events": "カレンダーのイベントを表示して予約が重ならないようにします。",
"lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定する",
"description_lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定するためのもので、対面のイベントに役立ちます。",
"extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -849,6 +849,7 @@
"next_step": "건너뛰기",
"prev_step": "이전 단계",
"install": "설치",
"install_paid_app": "구독",
"installed": "설치됨",
"active_install_one": "{{count}}개 활성 설치",
"active_install_other": "{{count}}개 활성 설치",

View File

@ -268,6 +268,7 @@
"set_availability": "Beschikbaarheid bepalen",
"availability_settings": "Beschikbaarheidsinstellingen",
"continue_without_calendar": "Doorgaan zonder kalender",
"continue_with": "Ga door met {{appName}}",
"connect_your_calendar": "Koppel uw agenda",
"connect_your_video_app": "Koppel uw videoapps",
"connect_your_video_app_instructions": "Koppel uw videoapps om ze op uw gebeurtenistypes te gebruiken.",
@ -288,6 +289,8 @@
"when": "Wanneer",
"where": "Waar",
"add_to_calendar": "Toevoegen aan kalender",
"add_to_calendar_description": "Selecteer waar u gebeurtenissen wilt toevoegen wanneer u geboekt bent.",
"add_events_to": "Voeg gebeurtenissen toe aan",
"add_another_calendar": "Andere agenda toevoegen",
"other": "Overige",
"email_sign_in_subject": "Uw aanmeldingslink voor {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Boeking aangemaakt",
"booking_rejected": "Boeking geweigerd",
"booking_requested": "Boeking aangevraagd",
"booking_payment_initiated": "Betaling boeking gestart",
"meeting_ended": "Vergadering beëindigd",
"form_submitted": "Formulier verzonden",
"booking_paid": "Boeking betaald",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Deze gebruiker heeft nog geen evenementen.",
"edit_logo": "Bewerk logo",
"upload_a_logo": "Logo uploaden",
"upload_logo": "Logo uploaden",
"remove_logo": "Logo verwijderen",
"enable": "Inschakelen",
"code": "Code",
@ -568,6 +573,7 @@
"your_team_name": "Naam van uw team",
"team_updated_successfully": "Team bijgewerkt",
"your_team_updated_successfully": "Uw team is succesvol bijgewerkt.",
"your_org_updated_successfully": "Uw organisatie is bijgewerkt.",
"about": "Introductie",
"team_description": "Een paar zinnen over uw team. Dit is zichtbaar op uw teampagina.",
"org_description": "Een paar zinnen over uw organisatie. Dit wordt weergegeven op op uw organisatiepagina.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Knop 'Teamlid boeken' verbergen",
"hide_book_a_team_member_description": "Verberg de knop 'Teamlid boeken' op uw openbare pagina's.",
"danger_zone": "Gevarenzone",
"account_deletion_cannot_be_undone": "Wees voorzichtig. Het verwijderen van een account kan niet ongedaan worden gemaakt.",
"back": "Terug",
"cancel": "Annuleren",
"cancel_all_remaining": "Alle resterende annuleren",
@ -688,6 +695,7 @@
"people": "Mensen",
"your_email": "Uw E-mailadres",
"change_avatar": "Wijzig avatar",
"upload_avatar": "Avatar uploaden",
"language": "Taal",
"timezone": "Tijdzone",
"first_day_of_week": "Eerste dag van de week",
@ -778,6 +786,7 @@
"disable_guests": "Gasten uitschakelen",
"disable_guests_description": "Schakel toevoegen van extra gasten uit tijdens het boeken.",
"private_link": "Privélink genereren",
"enable_private_url": "Privé-URL inschakelen",
"private_link_label": "Privélink",
"private_link_hint": "Uw privélink wordt na elk gebruik opnieuw gegenereerd",
"copy_private_link": "Privélink kopiëren",
@ -840,6 +849,7 @@
"next_step": "Stap overslaan",
"prev_step": "Vorige stap",
"install": "Installeren",
"install_paid_app": "Abonneren",
"installed": "Geinstalleerd",
"active_install_one": "{{count}} actieve installatie",
"active_install_other": "{{count}} actieve installaties",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Naam organisator",
"app_upgrade_description": "Om deze functie te gebruiken, moet u upgraden naar een Pro-account.",
"invalid_number": "Ongeldig telefoonnummer",
"invalid_url_error_message": "Ongeldige URL voor {{label}}. Voorbeeld-URL: {{sampleUrl}}",
"navigate": "Navigeren",
"open": "Openen",
"close": "Sluiten",
@ -1276,6 +1287,7 @@
"personal_cal_url": "Mijn persoonlijke {{appName}}-URL",
"bio_hint": "Een korte introductie over uzelf. Dit wordt weergegeven op uw persoonlijke pagina.",
"user_has_no_bio": "Deze gebruiker heeft nog geen bio toegevoegd.",
"bio": "Bio",
"delete_account_modal_title": "Account verwijderen",
"confirm_delete_account_modal": "Weet u zeker dat u uw {{appName}}-account wilt verwijderen?",
"delete_my_account": "Verwijder mijn account",
@ -1286,6 +1298,7 @@
"select_calendars": "Selecteer de agenda's die u wilt controleren op conflicten om dubbele boekingen te voorkomen.",
"check_for_conflicts": "Controleer op conflicten",
"view_recordings": "Opnames weergeven",
"check_for_recordings": "Controleer op opnames",
"adding_events_to": "Toevoegen van gebeurtenissen aan",
"follow_system_preferences": "Volg systeemvoorkeuren",
"custom_brand_colors": "Aangepaste merkkleuren",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "Er is een probleem opgetreden bij het registreren van het subdomein. Probeer het opnieuw of neem contact op met een beheerder",
"team_publish": "Team publiceren",
"number_text_notifications": "Telefoonnummer (sms-meldingen)",
"number_sms_notifications": "Telefoonnummer (sms-meldingen)",
"attendee_email_variable": "E-mailadres deelnemer",
"attendee_email_info": "Het e-mailadres van de persoon die boekt",
"kbar_search_placeholder": "Typ een opdracht of zoek...",
@ -1594,6 +1608,7 @@
"options": "Opties",
"enter_option": "Voer optie {{index}} in",
"add_an_option": "Voeg een optie toe",
"location_already_exists": "Deze locatie bestaat al. Selecteer een nieuwe locatie",
"radio": "Radio",
"google_meet_warning": "Om Google Meet te kunnen gebruiken, moet u uw bestemmingsagenda instellen op een Google-agenda",
"individual": "Individueel",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Markeren als niet beschikbaar op geselecteerde datums",
"date_overrides_add_btn": "Overschrijving toevoegen",
"date_overrides_update_btn": "Overschrijving bijwerken",
"date_successfully_added": "Datumoverschrijing toegevoegd",
"event_type_duplicate_copy_text": "{{slug}}-kopie",
"set_as_default": "Als standaard instellen",
"hide_eventtype_details": "Gebeurtenistypegegevens verbergen",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Aantal vereiste hosts voor deelname",
"hosts": "Hosts",
"upgrade_to_enable_feature": "U moet een team maken om deze functie in te schakelen. Klik om een team te maken.",
"orgs_upgrade_to_enable_feature": "U moet upgraden naar ons Enterprise-abonnement om deze functie in te schakelen.",
"new_attendee": "Nieuwe deelnemer",
"awaiting_approval": "In afwachting van goedkeuring",
"requires_google_calendar": "Deze app vereist een Google Agenda-koppeling",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Weergeven op boekingspagina",
"get_started_zapier_templates": "Aan de slag met Zapier-sjablonen",
"team_is_unpublished": "{{team}} is niet gepubliceerd",
"org_is_unpublished_description": "Deze organisatiekoppeling is momenteel niet beschikbaar. Neem contact op met de organisatie-eigenaar of vraag om het te publiceren.",
"team_is_unpublished_description": "Deze {{entity}}-link is momenteel niet beschikbaar. Neem contact op met de {{entity}}-eigenaar of vraag hem het te publiceren.",
"team_member": "Teamlid",
"a_routing_form": "Een routeringsformulier",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Linkinstellingen bewerken",
"invite_link_copied": "Uitnodigingslink gekopieerd",
"invite_link_deleted": "Uitnodigingslink verwijderd",
"api_key_deleted": "API-sleutel verwijderd",
"invite_link_updated": "Uitnodigingslinkinstellingen opgeslagen",
"link_expires_after": "Links verlopen na...",
"one_day": "1 dag",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Achternaam deelnemer",
"attendee_first_name_info": "De voornaam van de persoon die boekt",
"attendee_last_name_info": "De achternaam van de persoon die boekt",
"your_monthly_digest": "Uw maandelijks overzicht",
"member_name": "Lidnaam",
"most_popular_events": "Populairste gebeurtenissen",
"summary_of_events_for_your_team_for_the_last_30_days": "Dit is uw overzicht van populaire evenementen voor uw team {{teamName}} van de afgelopen 30 dagen",
"me": "Ik",
"monthly_digest_email": "Maandelijks overzichts-e-mail",
"monthly_digest_email_for_teams": "Maandelijks overzichts-e-mail voor teams",
"verify_team_tooltip": "Verifieer uw team om het versturen van berichten naar deelnemers in te schakelen",
"member_removed": "Lid verwijderd",
"my_availability": "Mijn beschikbaarheid",
@ -2039,13 +2064,41 @@
"team_no_event_types": "Dit team heeft geen gebeurtenistypes",
"seat_options_doesnt_multiple_durations": "De plaatsoptie ondersteunt geen meerdere duren",
"include_calendar_event": "Agendagebeurtenis opnemen",
"oAuth": "OAuth",
"recently_added": "Recent toegevoegd",
"no_members_found": "Geen leden gevonden",
"event_setup_length_error": "Gebeurtenisconfiguratie: de duur moet minimaal 1 minuut zijn.",
"availability_schedules": "Beschikbaarheidsschema's",
"unauthorized": "Ongeautoriseerd",
"access_cal_account": "{{clientName}} wil graag toegang tot uw {{appName}}-account",
"select_account_team": "Account of team selecteren",
"allow_client_to": "Hierdoor kan {{clientName}}",
"associate_with_cal_account": "U koppelen aan uw persoonsgegevens van {{clientName}}",
"see_personal_info": "Uw persoonsgegevens bekijken, waaronder alle persoonsgegevens die u openbaar heeft gemaakt",
"see_primary_email_address": "Uw primaire e-mailadres bekijken",
"connect_installed_apps": "Koppelen met uw geïnstalleerde apps",
"access_event_type": "Uw gebeurtenistypes lezen, bewerken en verwijderen",
"access_availability": "Uw beschikbaarheid lezen, bewerken en verwijderen",
"access_bookings": "Uw boekingen lezen, bewerken en verwijderen",
"allow_client_to_do": "Dit toestaan voor {{clientName}}?",
"oauth_access_information": "Door op Toestaan te klikken, geeft u deze app toestemming om uw gegevens te gebruiken in overeenstemming met hun gebruiksvoorwaarden en privacybeleid. U kunt de toegang verwijderen in de {{appName}} App Store.",
"allow": "Toestaan",
"view_only_edit_availability_not_onboarded": "Deze gebruiker heeft de onboarding nog niet voltooid. U kunt diens beschikbaarheid pas instellen nadat ze men de onboarding heeft voltooid.",
"view_only_edit_availability": "U bekijkt de beschikbaarheid van deze gebruiker. U kunt alleen uw eigen beschikbaarheid bewerken.",
"you_can_override_calendar_in_advanced_tab": "U kunt dit per gebeurtenis overschrijven in Geavanceerde instellingen voor elk gebeurtenistype.",
"edit_users_availability": "Beschikbaarheid van gebruiker bewerken: {{username}}",
"resend_invitation": "Uitnodiging opnieuw versturen",
"invitation_resent": "De uitnodiging is opnieuw verstuurd.",
"add_client": "Klant toevoegen",
"copy_client_secret_info": "Na het kopiëren van het geheim kunt u het niet meer bekijken",
"add_new_client": "Nieuwe klant toevoegen",
"this_app_is_not_setup_already": "Deze app is nog niet ingesteld",
"as_csv": "als CSV",
"overlay_my_calendar": "Mijn agenda overleggen",
"overlay_my_calendar_toc": "Door uw agenda te koppelen accepteert u ons privacybeleid en onze gebruiksvoorwaarden. U kunt de toegang op elk moment weer intrekken.",
"view_overlay_calendar_events": "Bekijk uw agendagebeurtenissen om dubbele boekingen te voorkomen.",
"lock_timezone_toggle_on_booking_page": "Tijdzone vergrendelen op boekingspagina",
"description_lock_timezone_toggle_on_booking_page": "Om de tijdzone op de boekingspagina te vergrendelen, handig voor persoonlijke gebeurtenissen.",
"extensive_whitelabeling": "Speciale onboarding en technische ondersteuning",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -706,6 +706,7 @@
"next_step": "Hopp over trinn",
"prev_step": "Forrige trinn",
"install": "Installer",
"install_paid_app": "Abonner",
"installed": "Installert",
"active_install_one": "{{count}} aktiv installasjon",
"active_install_other": "{{count}} aktive installasjoner",

View File

@ -268,6 +268,7 @@
"set_availability": "Ustaw swoją dostępność",
"availability_settings": "Ustawienia dostępności",
"continue_without_calendar": "Kontynuuj bez kalendarza",
"continue_with": "Kontynuuj za pomocą: {{appName}}",
"connect_your_calendar": "Połącz swój kalendarz",
"connect_your_video_app": "Podłącz aplikacje wideo",
"connect_your_video_app_instructions": "Podłącz aplikacje wideo, aby korzystać z nich w swoich typach wydarzeń.",
@ -288,6 +289,8 @@
"when": "Kiedy",
"where": "Gdzie",
"add_to_calendar": "Dodaj do kalendarza",
"add_to_calendar_description": "Wybierz, gdzie dodawać wydarzenia w zarezerwowanych terminach.",
"add_events_to": "Dodaj wydarzenia do",
"add_another_calendar": "Dodaj kolejny kalendarz",
"other": "Inne",
"email_sign_in_subject": "Twój link logowania do aplikacji {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Rezerwacja Utworzona",
"booking_rejected": "Odrzucono rezerwację",
"booking_requested": "Poproszono o rezerwację",
"booking_payment_initiated": "Rozpoczęto płatność za rezerwację",
"meeting_ended": "Spotkanie zakończone",
"form_submitted": "Przesłano formularz",
"booking_paid": "Rezerwacja opłacona",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Ten użytkownik nie ustawił jeszcze żadnych typów wydarzeń.",
"edit_logo": "Edytuj logo",
"upload_a_logo": "Prześlij logo",
"upload_logo": "Prześlij logo",
"remove_logo": "Usuń logo",
"enable": "Włącz",
"code": "Kod",
@ -568,6 +573,7 @@
"your_team_name": "Nazwa Twojego zespołu",
"team_updated_successfully": "Zespół został pomyślnie zaktualizowany",
"your_team_updated_successfully": "Twój zespół został pomyślnie zaktualizowany.",
"your_org_updated_successfully": "Twoja organizacja została pomyślnie zaktualizowana.",
"about": "O aplikacji",
"team_description": "Kilka zdań na temat Twojego zespołu. Będą wyświetlane na stronie adresu URL Twojego zespołu.",
"org_description": "Kilka zdań na temat Twojej organizacji. Pojawią się na stronie adresu URL Twojej organizacji.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Ukryj przycisk Zarezerwuj członka zespołu",
"hide_book_a_team_member_description": "Ukryj przycisk Zarezerwuj członka zespołu na swoich stronach publicznych.",
"danger_zone": "Strefa zagrożenia",
"account_deletion_cannot_be_undone": "Uwaga. Usunięcie konta jest nieodwracalne.",
"back": "Wróć",
"cancel": "Anuluj",
"cancel_all_remaining": "Anuluj wszystkie pozostałe",
@ -688,6 +695,7 @@
"people": "Ludzie",
"your_email": "Twój e-mail",
"change_avatar": "Zmień Awatar",
"upload_avatar": "Prześlij awatar",
"language": "Język",
"timezone": "Strefa Czasowa",
"first_day_of_week": "Pierwszy dzień tygodnia",
@ -778,6 +786,7 @@
"disable_guests": "Wyłącz Gości",
"disable_guests_description": "Wyłącz dodawanie dodatkowych gości podczas rezerwacji.",
"private_link": "Wygeneruj link prywatny",
"enable_private_url": "Włącz prywatny adres URL",
"private_link_label": "Link prywatny",
"private_link_hint": "Twój link prywatny będzie odnawiać się po każdym użyciu",
"copy_private_link": "Skopiuj link prywatny",
@ -840,6 +849,7 @@
"next_step": "Pomiń krok",
"prev_step": "Poprzedni krok",
"install": "Zainstaluj",
"install_paid_app": "Subskrybuj",
"installed": "Zainstalowane",
"active_install_one": "Aktywne instalacje: {{count}}",
"active_install_other": "Aktywne instalacje: {{count}}",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Nazwa organizatora",
"app_upgrade_description": "Aby korzystać z tej funkcji, musisz uaktualnić do konta Pro.",
"invalid_number": "Nieprawidłowy numer telefonu",
"invalid_url_error_message": "Nieprawidłowy adres URL dla: {{label}}. Przykładowy adres URL: {{sampleUrl}}",
"navigate": "Nawigacja",
"open": "Otwórz",
"close": "Zamknij",
@ -1276,6 +1287,7 @@
"personal_cal_url": "Mój osobisty adres URL aplikacji {{appName}}",
"bio_hint": "Kilka zdań o Tobie. Pojawią się one na Twojej osobistej stronie URL.",
"user_has_no_bio": "Ten użytkownik nie dodał jeszcze biogramu.",
"bio": "Informacje o profilu",
"delete_account_modal_title": "Usuń konto",
"confirm_delete_account_modal": "Czy na pewno chcesz usunąć swoje konto {{appName}}?",
"delete_my_account": "Usuń konto",
@ -1286,6 +1298,7 @@
"select_calendars": "Wybierz kalendarze, które chcesz sprawdzić pod kątem konfliktów, aby uniknąć podwójnych rezerwacji.",
"check_for_conflicts": "Sprawdź konflikty",
"view_recordings": "Wyświetl nagrania",
"check_for_recordings": "Sprawdź nagrania",
"adding_events_to": "Dodawanie wydarzeń do",
"follow_system_preferences": "Śledź preferencje systemowe",
"custom_brand_colors": "Niestandardowe kolory marki",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "Wystąpił problem z zarejestrowaniem subdomeny, spróbuj ponownie lub skontaktuj się z administratorem",
"team_publish": "Opublikuj zespół",
"number_text_notifications": "Numer telefonu (powiadomienia SMS)",
"number_sms_notifications": "Numer telefonu (powiadomienia SMS)",
"attendee_email_variable": "Adres e-mail uczestnika",
"attendee_email_info": "Adres e-mail osoby rezerwującej",
"kbar_search_placeholder": "Wpisz polecenie lub wyszukaj...",
@ -1594,6 +1608,7 @@
"options": "Opcje",
"enter_option": "Wprowadź opcję {{index}}",
"add_an_option": "Dodaj opcję",
"location_already_exists": "Ta lokalizacja już istnieje. Wybierz nową lokalizację",
"radio": "Przycisk radiowy",
"google_meet_warning": "Aby korzystać z usługi Google Meet, musisz wybrać Kalendarz Google jako kalendarz docelowy.",
"individual": "Pojedyncza osoba",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Zaznacz niedostępność w wybrane dni",
"date_overrides_add_btn": "Dodaj zastąpienie",
"date_overrides_update_btn": "Zaktualizuj zastąpienie",
"date_successfully_added": "Pomyślnie dodano nadpisanie daty",
"event_type_duplicate_copy_text": "{{slug}}-kopia",
"set_as_default": "Ustaw jako domyślne",
"hide_eventtype_details": "Ukryj szczegóły typu wydarzenia",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Liczba gospodarzy, których uczestnictwo jest wymagane",
"hosts": "Gospodarze",
"upgrade_to_enable_feature": "Musisz utworzyć zespół, aby włączyć tę funkcję. Kliknij, aby utworzyć zespół.",
"orgs_upgrade_to_enable_feature": "Aby włączyć tę funkcję, musisz przejść na plan Enterprise.",
"new_attendee": "Nowy uczestnik",
"awaiting_approval": "Oczekiwanie na zatwierdzenie",
"requires_google_calendar": "Ta aplikacja wymaga połączenia z Kalendarzem Google",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Pokaż na stronie rezerwacji",
"get_started_zapier_templates": "Zacznij korzystać z szablonów Zapier",
"team_is_unpublished": "Zespół {{team}} nie został opublikowany",
"org_is_unpublished_description": "Link organizacji jest obecnie niedostępny. Skontaktuj się z właścicielem organizacji lub poproś o jego opublikowanie.",
"team_is_unpublished_description": "Link jednostki {{entity}} jest obecnie niedostępny. Skontaktuj się z właścicielem jednostki {{entity}} lub poproś o jego opublikowanie.",
"team_member": "Członek zespołu",
"a_routing_form": "Formularz przekierowania",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Edytuj ustawienia linku",
"invite_link_copied": "Skopiowano link zaproszenia",
"invite_link_deleted": "Usunięto link zaproszenia",
"api_key_deleted": "Klucz API został usunięty",
"invite_link_updated": "Zapisano ustawienia linku zaproszenia",
"link_expires_after": "Linki wygasną po...",
"one_day": "1 dzień",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Nazwisko uczestnika",
"attendee_first_name_info": "Imię osoby rezerwującej",
"attendee_last_name_info": "Nazwisko osoby rezerwującej",
"your_monthly_digest": "Miesięczne podsumowanie",
"member_name": "Nazwa członka",
"most_popular_events": "Najpopularniejsze wydarzenia",
"summary_of_events_for_your_team_for_the_last_30_days": "Oto podsumowanie popularnych wydarzeń dla Twojego zespołu {{teamName}} z ostatnich 30 dni",
"me": "Ja",
"monthly_digest_email": "E-mail z miesięcznym podsumowaniem",
"monthly_digest_email_for_teams": "E-mail dla zespołów z miesięcznym podsumowaniem",
"verify_team_tooltip": "Zweryfikuj zespół, aby włączyć wysyłanie wiadomości do uczestników",
"member_removed": "Usunięto członka",
"my_availability": "Moja dostępność",
@ -2039,13 +2064,41 @@
"team_no_event_types": "Ten zespół nie ma żadnych typów zdarzeń.",
"seat_options_doesnt_multiple_durations": "Opcja Miejsce nie obsługuje wielu czasów trwania.",
"include_calendar_event": "Uwzględnij wydarzenie z kalendarza",
"oAuth": "OAuth",
"recently_added": "Niedawno dodane",
"no_members_found": "Nie znaleziono członków",
"event_setup_length_error": "Konfiguracja wydarzenia: czas trwania musi wynosić co najmniej minutę.",
"availability_schedules": "Harmonogramy dostępności",
"unauthorized": "Brak autoryzacji",
"access_cal_account": "{{clientName}} chce uzyskać dostęp do Twojego konta w aplikacji {{appName}}",
"select_account_team": "Wybierz konto lub zespół",
"allow_client_to": "Dzięki temu klient ({{clientName}}) będzie mógł",
"associate_with_cal_account": "Powiązać Cię z Twoimi danymi osobistymi od klienta ({{clientName}})",
"see_personal_info": "Zobaczyć Twoje dane osobiste, w tym wszystkie dane osobiste udostępnione przez Ciebie publicznie",
"see_primary_email_address": "Zobaczyć Twój główny adres e-mail",
"connect_installed_apps": "Połączyć się z zainstalowanymi przez Ciebie aplikacjami",
"access_event_type": "Przeglądać, edytować i usuwać Twoje typy wydarzeń",
"access_availability": "Przeglądać, edytować i usuwać Twoją dostępność",
"access_bookings": "Przeglądać, edytować i usuwać Twoje rezerwacje",
"allow_client_to_do": "Czy chcesz umożliwić to klientowi ({{clientName}})?",
"oauth_access_information": "Klikając „Zezwól”, pozwolisz tej aplikacji na korzystanie z Twoich danych zgodnie z jej regulaminem i polityką prywatności. Możesz odwołać pozwolenie na dostęp w ustawieniach sklepu aplikacji {{appName}}.",
"allow": "Zezwól",
"view_only_edit_availability_not_onboarded": "Ten użytkownik nie zakończył procesu wdrażania. Ustawienie jego dostępności będzie niedostępne, póki nie zakończy wdrażania.",
"view_only_edit_availability": "Wyświetlasz dostępność tego użytkownika. Możesz edytować tylko własną dostępność.",
"you_can_override_calendar_in_advanced_tab": "Możesz nadpisać to na podstawie wydarzeń w ustawieniach zaawansowanych przy każdym typie wydarzenia.",
"edit_users_availability": "Edytuj dostępność użytkownika: {{username}}",
"resend_invitation": "Ponownie wyślij zaproszenie",
"invitation_resent": "Zaproszenie zostało wysłane ponownie.",
"add_client": "Dodaj klienta",
"copy_client_secret_info": "Po skopiowaniu sekretu nie będzie można go już przeglądać",
"add_new_client": "Dodaj nowego klienta",
"this_app_is_not_setup_already": "Ta aplikacja nie została jeszcze skonfigurowana",
"as_csv": "jako plik CSV",
"overlay_my_calendar": "Nakładka kalendarza",
"overlay_my_calendar_toc": "Łącząc się z kalendarzem, akceptujesz naszą politykę prywatności i warunki użytkowania. Możesz odwołać pozwolenie na dostęp w dowolnym momencie.",
"view_overlay_calendar_events": "Zobacz wydarzenia kalendarza, aby zapobiec kolidującym rezerwacjom.",
"lock_timezone_toggle_on_booking_page": "Zablokuj strefę czasową na stronie rezerwacji",
"description_lock_timezone_toggle_on_booking_page": "Aby zablokować strefę czasową na stronie rezerwacji (przydatne dla wydarzeń stacjonarnych).",
"extensive_whitelabeling": "Dedykowane wsparcie w zakresie wdrożenia i obsługi technicznej",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -268,6 +268,7 @@
"set_availability": "Definir disponibilidade",
"availability_settings": "Configurações de disponibilidade",
"continue_without_calendar": "Continuar sem calendário",
"continue_with": "Continuar com {{appName}}",
"connect_your_calendar": "Conectar seu calendário",
"connect_your_video_app": "Conecte seus aplicativos de vídeo",
"connect_your_video_app_instructions": "Conecte seus aplicativos de vídeo para usá-los nos seus tipos de evento.",
@ -288,6 +289,8 @@
"when": "Quando",
"where": "Onde",
"add_to_calendar": "Adicionar ao calendário",
"add_to_calendar_description": "Selecione onde adicionar eventos quando você já tiver uma reserva.",
"add_events_to": "Adicionar eventos a",
"add_another_calendar": "Adicionar outro calendário",
"other": "Outras",
"email_sign_in_subject": "Seu link de login para {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Reserva Criada",
"booking_rejected": "Reserva recusada",
"booking_requested": "Reserva solicitada",
"booking_payment_initiated": "Pagamento da reserva iniciado",
"meeting_ended": "Reunião encerrada",
"form_submitted": "Formulário enviado",
"booking_paid": "Reserva paga",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Este usuário ainda não definiu nenhum tipo de evento.",
"edit_logo": "Editar logotipo",
"upload_a_logo": "Envie um logotipo",
"upload_logo": "Carregar logotipo",
"remove_logo": "Remover logotipo",
"enable": "Ativado",
"code": "Código",
@ -568,6 +573,7 @@
"your_team_name": "Nome do seu time",
"team_updated_successfully": "Time atualizado com sucesso",
"your_team_updated_successfully": "Seu time foi atualizado com sucesso.",
"your_org_updated_successfully": "Sua organização foi atualizada com sucesso.",
"about": "Sobre",
"team_description": "Fale um pouco sobre seu time. Isso aparecerá na página pública do seu time.",
"org_description": "Uma breve descrição sobre sua organização. Isso aparecerá na página principal da sua organização.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Ocultar botão de agendar um membro da equipe",
"hide_book_a_team_member_description": "Oculte o botão de agendar um membro da equipe em suas páginas públicas.",
"danger_zone": "Zona de perigo",
"account_deletion_cannot_be_undone": "Cuidado. A exclusão da conta não pode ser desfeita.",
"back": "Voltar",
"cancel": "Cancelar",
"cancel_all_remaining": "Cancelar todos os restantes",
@ -688,6 +695,7 @@
"people": "Pessoas",
"your_email": "Seu Email",
"change_avatar": "Alterar Avatar",
"upload_avatar": "Enviar avatar",
"language": "Idioma",
"timezone": "Fuso Horário",
"first_day_of_week": "Primeiro Dia da Semana",
@ -778,6 +786,7 @@
"disable_guests": "Desativar participantes",
"disable_guests_description": "Desativar a adição de participantes adicionais durante a reserva.",
"private_link": "Gerar link privado",
"enable_private_url": "Ativar URL privada",
"private_link_label": "Link privado",
"private_link_hint": "Seu link privado será reaproveitado após cada uso",
"copy_private_link": "Copiar link privado",
@ -840,6 +849,7 @@
"next_step": "Ignorar passo",
"prev_step": "Passo anterior",
"install": "Instalar",
"install_paid_app": "Assinar",
"installed": "Instalado",
"active_install_one": "{{count}} instalação ativa",
"active_install_other": "{{count}} instalações ativas",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Nome do organizador",
"app_upgrade_description": "Para usar este recurso, atualize para uma conta Pro.",
"invalid_number": "Número de telefone inválido",
"invalid_url_error_message": "URL inválida para {{label}}. URL de amostra: {{sampleUrl}}",
"navigate": "Navegar",
"open": "Abrir",
"close": "Fechar",
@ -1276,6 +1287,7 @@
"personal_cal_url": "Meu URL pessoal da {{appName}}",
"bio_hint": "Escreva algumas frases sobre você. Isso aparecerá no URL da sua página pessoal.",
"user_has_no_bio": "Este usuário ainda não adicionou uma biografia.",
"bio": "Biografia",
"delete_account_modal_title": "Excluir conta",
"confirm_delete_account_modal": "Tem certeza de que deseja excluir sua conta {{appName}}?",
"delete_my_account": "Excluir minha conta",
@ -1286,6 +1298,7 @@
"select_calendars": "Selecionar em quais calendários você deseja verificar se há conflitos para evitar dupla reserva.",
"check_for_conflicts": "Verificar se há conflitos",
"view_recordings": "Ver gravações",
"check_for_recordings": "Verificar gravações",
"adding_events_to": "Adicionando eventos a",
"follow_system_preferences": "Seguir preferências do sistema",
"custom_brand_colors": "Cores personalizadas da marca",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "Houve um problema ao registrar o subdomínio, tente novamente ou fale com o administrador",
"team_publish": "Publicar equipe",
"number_text_notifications": "Número de telefone (notificações via texto)",
"number_sms_notifications": "Número de telefone (notificações SMS)",
"attendee_email_variable": "E-mail do participante",
"attendee_email_info": "O e-mail da pessoa que fez a reserva",
"kbar_search_placeholder": "Digite um comando ou procure...",
@ -1594,6 +1608,7 @@
"options": "Opções",
"enter_option": "insira a opção {{index}}",
"add_an_option": "Adicionar uma opção",
"location_already_exists": "Este local já existe. Escolha um novo local",
"radio": "Rádio",
"google_meet_warning": "Para usar o Google Meet, é preciso definir seu calendário de destino para o Google Calendar",
"individual": "Individual",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Marcar indisponível nas datas selecionadas",
"date_overrides_add_btn": "Adicionar substituição",
"date_overrides_update_btn": "Atualizar substituição",
"date_successfully_added": "Substituição de data adicionada com sucesso",
"event_type_duplicate_copy_text": "Cópia de {{slug}}",
"set_as_default": "Definir como padrão",
"hide_eventtype_details": "Ocultar detalhes do tipo de evento",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Número de hosts necessários para participar",
"hosts": "Hosts",
"upgrade_to_enable_feature": "É preciso criar uma equipe para ativar este recurso. Clique para criar uma.",
"orgs_upgrade_to_enable_feature": "Você precisa atualizar para nosso plano empresarial para ativar esse recurso.",
"new_attendee": "Novo participante",
"awaiting_approval": "Aguardando aprovação",
"requires_google_calendar": "Este aplicativo requer uma conexão com o Google Calendar",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Mostrar na página de reservas",
"get_started_zapier_templates": "Comece agora com modelos do Zapier",
"team_is_unpublished": "Publicação de {{team}} cancelada",
"org_is_unpublished_description": "Este link da organização não está disponível no momento. Entre em contato com o proprietário da organização ou peça que seja publicado.",
"team_is_unpublished_description": "O link de {{entity}} não está disponível por enquanto. Fale com o proprietário de {{entity}} ou peça que seja publicado.",
"team_member": "Membro da equipe",
"a_routing_form": "Formulário de roteamento",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Editar configurações do link",
"invite_link_copied": "Link de convite copiado",
"invite_link_deleted": "Link de convite excluído",
"api_key_deleted": "Chave da API excluída",
"invite_link_updated": "Configurações do link de convite salvas",
"link_expires_after": "Link configurado para expirar após...",
"one_day": "1 dia",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Sobrenome do participante",
"attendee_first_name_info": "Nome da pessoa que fez a reserva",
"attendee_last_name_info": "O sobrenome da pessoa que fez a reserva",
"your_monthly_digest": "Seu Resumo Mensal",
"member_name": "Nome do Membro",
"most_popular_events": "Eventos Mais Populares",
"summary_of_events_for_your_team_for_the_last_30_days": "Aqui está seu resumo dos eventos populares da sua equipe {{teamName}} nos últimos 30 dias",
"me": "Eu",
"monthly_digest_email": "E-mail de Resumo Mensal",
"monthly_digest_email_for_teams": "E-mail de resumo mensal para as equipes",
"verify_team_tooltip": "Verifique seu time para habilitar o envio de mensagens aos participantes",
"member_removed": "Membro removido",
"my_availability": "Minha disponibilidade",
@ -2039,13 +2064,41 @@
"team_no_event_types": "Sua equipe não tem tipos de evento",
"seat_options_doesnt_multiple_durations": "A opção de assento não é compatível com múltiplas durações",
"include_calendar_event": "Incluir evento do calendário",
"oAuth": "OAuth",
"recently_added": "Adicionou recentemente",
"no_members_found": "Nenhum membro encontrado",
"event_setup_length_error": "Configuração do evento: deve ter pelo menos 1 minuto de duração.",
"availability_schedules": "Agendamentos de disponibilidade",
"unauthorized": "Não autorizado",
"access_cal_account": "{{clientName}} gostaria de acessar a sua conta {{appName}}",
"select_account_team": "Selecionar conta ou equipe",
"allow_client_to": "Isso permitirá ao {{clientName}}",
"associate_with_cal_account": "Associar você com suas informações pessoais de {{clientName}}",
"see_personal_info": "Veja suas informações pessoais, incluindo quaisquer informações pessoais disponibilizadas publicamente",
"see_primary_email_address": "Veja seu endereço de e-mail principal",
"connect_installed_apps": "Conecte-se aos seus aplicativos instalados",
"access_event_type": "Ler, editar, excluir os seus tipos de eventos",
"access_availability": "Ler, editar, excluir sua disponibilidade",
"access_bookings": "Ler, editar, excluir suas reservas",
"allow_client_to_do": "Permitir que {{clientName}} faça isto?",
"oauth_access_information": "Ao clicar em Permitir, você autoriza que este aplicativo use suas informações, de acordo com os termos de serviço e política de privacidade. Você pode remover o acesso na App Store do {{appName}}.",
"allow": "Permitir",
"view_only_edit_availability_not_onboarded": "Este usuário não concluiu a integração. Não será possível definir a disponibilidade antes de concluir a integração.",
"view_only_edit_availability": "Você está vendo a disponibilidade deste usuário. Você só pode editar sua disponibilidade.",
"you_can_override_calendar_in_advanced_tab": "Você pode substituir isso por evento nas configurações Avançadas em cada tipo de evento.",
"edit_users_availability": "Editar disponibilidade do usuário: {{username}}",
"resend_invitation": "Reenviar convite",
"invitation_resent": "O convite foi reenviado.",
"add_client": "Adicionar cliente",
"copy_client_secret_info": "Depois de copiar o segredo, você não poderá mais visualizá-lo",
"add_new_client": "Adicionar novo cliente",
"this_app_is_not_setup_already": "Este app ainda não foi configurado",
"as_csv": "como CSV",
"overlay_my_calendar": "Sobrepor meu calendário",
"overlay_my_calendar_toc": "Ao conectar-se à seu Calendário, você aceita nossa política de privacidade e termos de uso. Você pode revogar o acesso a qualquer momento.",
"view_overlay_calendar_events": "Veja seus eventos do calendário para evitar conflito nos agendamentos.",
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Bloquear o fuso horário na página de reservas, útil para eventos presenciais.",
"extensive_whitelabeling": "Marca própria abrangente",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -268,6 +268,7 @@
"set_availability": "Definir disponibilidade",
"availability_settings": "Configurações de disponibilidade",
"continue_without_calendar": "Continuar sem calendário",
"continue_with": "Continuar com {{appName}}",
"connect_your_calendar": "Ligar o seu calendário",
"connect_your_video_app": "Associar as suas aplicações de vídeo",
"connect_your_video_app_instructions": "Associe as suas aplicações de vídeo para utilizar as mesmas nos seus tipos de eventos.",
@ -288,6 +289,8 @@
"when": "Quando",
"where": "Onde",
"add_to_calendar": "Adicionar ao calendário",
"add_to_calendar_description": "Selecione onde adicionar eventos quando estiver reservado.",
"add_events_to": "Adicionar eventos a",
"add_another_calendar": "Adicionar outro calendário",
"other": "Outras",
"email_sign_in_subject": "A sua ligação de início de sessão para {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Reserva Criada",
"booking_rejected": "Reserva rejeitada",
"booking_requested": "Reserva solicitada",
"booking_payment_initiated": "Pagamento da reserva iniciado",
"meeting_ended": "A reunião terminou",
"form_submitted": "Formulário enviado",
"booking_paid": "Reserva paga",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Este utilizador ainda não configurou nenhum tipo de evento.",
"edit_logo": "Editar logótipo",
"upload_a_logo": "Carregar logótipo",
"upload_logo": "Carregar logótipo",
"remove_logo": "Remover logótipo",
"enable": "Ativar",
"code": "Código",
@ -568,6 +573,7 @@
"your_team_name": "Nome da sua equipa",
"team_updated_successfully": "Equipa atualizada com sucesso",
"your_team_updated_successfully": "A sua equipa foi atualizada com sucesso.",
"your_org_updated_successfully": "A sua organização foi atualizada com sucesso.",
"about": "Sobre",
"team_description": "Algumas frases sobre a sua equipa. Isso aparecerá na página url da sua equipa.",
"org_description": "Algumas frases sobre a sua organização. Isto será apresentado no URL da página da sua organização.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Ocultar o botão Reservar um Membro da equipa",
"hide_book_a_team_member_description": "Ocultar o botão Reservar um Membro da equipa das suas páginas públicas.",
"danger_zone": "Zona de perigo",
"account_deletion_cannot_be_undone": "Cuidado. A eliminação da conta não pode ser anulada.",
"back": "Anterior",
"cancel": "Cancelar",
"cancel_all_remaining": "Cancelar todos os restantes",
@ -688,6 +695,7 @@
"people": "Pessoas",
"your_email": "Seu Email",
"change_avatar": "Alterar Avatar",
"upload_avatar": "Carregar avatar",
"language": "Idioma",
"timezone": "Fuso Horário",
"first_day_of_week": "Primeiro Dia da Semana",
@ -778,6 +786,7 @@
"disable_guests": "Desativar Convidados",
"disable_guests_description": "Desativar a adição de convidados adicionais durante a reserva.",
"private_link": "Gerar link privado",
"enable_private_url": "Ativar URL privada",
"private_link_label": "Ligação privada",
"private_link_hint": "A sua ligação será renovada após cada utilização",
"copy_private_link": "Copiar ligação privada",
@ -840,6 +849,7 @@
"next_step": "Ignorar passo",
"prev_step": "Passo anterior",
"install": "Instalar",
"install_paid_app": "Subscrever",
"installed": "Instalado",
"active_install_one": "{{count}} instalação activa",
"active_install_other": "{{count}} instalações activas",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Nome do organizador",
"app_upgrade_description": "Para usar esta funcionalidade, tem de actualizar para uma conta Pro.",
"invalid_number": "Número de telefone inválido",
"invalid_url_error_message": "URL inválida para {{label}}. URL de exemplo: {{sampleUrl}}",
"navigate": "Navegar",
"open": "Abrir",
"close": "Fechar",
@ -1276,6 +1287,7 @@
"personal_cal_url": "O meu endereço pessoal do {{appName}}",
"bio_hint": "Algumas frases sobre si. Isto irá ser mostrado no endereço da sua página pessoal.",
"user_has_no_bio": "Este utilizador ainda não adicionou uma biografia.",
"bio": "Biografia",
"delete_account_modal_title": "Eliminar conta",
"confirm_delete_account_modal": "Tem a certeza que pretende eliminar a sua conta {{appName}}?",
"delete_my_account": "Eliminar a minha conta",
@ -1286,6 +1298,7 @@
"select_calendars": "Selecione os calendários nos quais pretende procurar por conflitos para evitar sobreposição de reservas.",
"check_for_conflicts": "Verificar se existem conflitos",
"view_recordings": "Ver gravações",
"check_for_recordings": "Consultar gravações",
"adding_events_to": "A adicionar eventos a",
"follow_system_preferences": "Utilizar as preferências do sistema",
"custom_brand_colors": "Cores personalizadas da marca",
@ -1595,6 +1608,7 @@
"options": "Opções",
"enter_option": "Insira a opção {{index}}",
"add_an_option": "Adicione uma opção",
"location_already_exists": "Esta localização já existe. Escolha uma nova localização",
"radio": "Radio",
"google_meet_warning": "Para utilizar o Google Meet, deve definir um Google Calendar como o seu calendário de destino",
"individual": "Individual",
@ -1614,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Marcar como não disponível em datas específicas",
"date_overrides_add_btn": "Adicionar sobreposição",
"date_overrides_update_btn": "Atualizar sobreposição",
"date_successfully_added": "Sobreposição de data adicionada com sucesso",
"event_type_duplicate_copy_text": "cópia de {{slug}}",
"set_as_default": "Predefinir",
"hide_eventtype_details": "Ocultar detalhes do tipo de evento",
@ -1640,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Número de anfitriões necessários",
"hosts": "Anfitriões",
"upgrade_to_enable_feature": "Precisa de criar uma equipa para ativar esta funcionalidade. Clique para criar uma equipa.",
"orgs_upgrade_to_enable_feature": "Deve atualizar para o nosso plano empresarial para ativar esta funcionalidade.",
"new_attendee": "Novo participante",
"awaiting_approval": "Aguarda aprovação",
"requires_google_calendar": "Esta aplicação requer uma ligação ao Google Calendar",
@ -1744,6 +1760,7 @@
"show_on_booking_page": "Mostrar na página de reservas",
"get_started_zapier_templates": "Comece a utilizar os modelos Zapier",
"team_is_unpublished": "A equipa {{team}} ainda não está publicada",
"org_is_unpublished_description": "Esta ligação da organização não está atualmente disponível. Por favor, entre em contacto com os responsáveis pela organização ou solicite aos mesmos a respetiva publicação.",
"team_is_unpublished_description": "Esta ligação de {{entity}} não está disponível neste momento. Por favor, entre em contacto com o responsável por {{entity}} ou solicite-lhe a respetiva publicação.",
"team_member": "Membro da equipa",
"a_routing_form": "Um formulário de encaminhamento",
@ -1878,6 +1895,7 @@
"edit_invite_link": "Editar definições da ligação",
"invite_link_copied": "Ligação de convite copiada",
"invite_link_deleted": "Ligação do convite copiada",
"api_key_deleted": "Chave de API removida",
"invite_link_updated": "Definições da ligação de convite guardadas",
"link_expires_after": "Ligações definidas para expirar depois...",
"one_day": "1 dia",
@ -2010,7 +2028,13 @@
"attendee_last_name_variable": "Último nome do participante",
"attendee_first_name_info": "Primeiro nome da pessoa da reserva",
"attendee_last_name_info": "Último nome da pessoa da reserva",
"your_monthly_digest": "O seu resumo mensal",
"member_name": "Nome do membro",
"most_popular_events": "Eventos mais populares",
"summary_of_events_for_your_team_for_the_last_30_days": "Aqui está o seu resumo dos eventos populares da sua equipa {{teamName}} para os últimos 30 dias",
"me": "Eu",
"monthly_digest_email": "E-mail de resumo mensal",
"monthly_digest_email_for_teams": "E-mail de resumo mensal para as equipas",
"verify_team_tooltip": "Verifique a sua equipa para poder enviar mensagens aos participantes",
"member_removed": "Membro removido",
"my_availability": "A minha disponibilidade",
@ -2040,13 +2064,41 @@
"team_no_event_types": "Esta equipa não tem tipos de evento",
"seat_options_doesnt_multiple_durations": "A opção lugar não suporta múltiplas durações",
"include_calendar_event": "Incluir evento no calendário",
"oAuth": "OAuth",
"recently_added": "Adicionados recentemente",
"no_members_found": "Nenhum membro encontrado",
"event_setup_length_error": "Configuração do Evento: a duração deve ser de, pelo menos, 1 minuto.",
"availability_schedules": "Horários de Disponibilidade",
"unauthorized": "Não autorizado",
"access_cal_account": "{{clientName}} gostaria de ter acesso à sua conta {{appName}}",
"select_account_team": "Selecionar conta ou equipa",
"allow_client_to": "Isto irá autorizar {{clientName}} a",
"associate_with_cal_account": "Associar as suas informações pessoais do {{clientName}} consigo",
"see_personal_info": "Ver as suas informações pessoais, incluindo todas as informações pessoais disponibilizadas publicamente",
"see_primary_email_address": "Ver o seu endereço de e-mail principal",
"connect_installed_apps": "Ligar-se às suas aplicações instaladas",
"access_event_type": "Ler, editar e eliminar os seus tipos de eventos",
"access_availability": "Ler, editar e eliminar a sua disponibilidade",
"access_bookings": "Ler, editar e eliminar as suas reservas",
"allow_client_to_do": "Permitir que {{clientName}} possa fazer isto?",
"oauth_access_information": "Ao clicar em Permitir, você permite que esta aplicação utilize as suas informações, de acordo com os termos do serviço e a política de privacidade. Pode remover o acesso na App Store do {{appName}}.",
"allow": "Permitir",
"view_only_edit_availability_not_onboarded": "Este utilizador ainda não completou o processo de integração. Não poderá definir a respetiva disponibilidade até que este procedimento esteja concluído para este utilizador.",
"view_only_edit_availability": "Está a visualizar a disponibilidade deste utilizador. Só pode editar a sua própria disponibilidade.",
"you_can_override_calendar_in_advanced_tab": "Pode substituir isto individualmente em cada evento, nas Configurações avançadas em cada tipo de evento.",
"edit_users_availability": "Editar a disponibilidade do utilizador: {{username}}",
"resend_invitation": "Reenviar convite",
"invitation_resent": "O convite foi reenviado.",
"add_client": "Adicionar cliente",
"copy_client_secret_info": "Após copiar o segredo, não será possível ver o mesmo novamente",
"add_new_client": "Adicionar novo Cliente",
"this_app_is_not_setup_already": "Esta aplicação ainda não foi configurada",
"as_csv": "como CSV",
"overlay_my_calendar": "Sobrepor o meu calendário",
"overlay_my_calendar_toc": "Ao ligar-se ao seu calendário, você aceita a nossa política de privacidade e termos de utilização. Pode revogar o acesso a qualquer momento.",
"view_overlay_calendar_events": "Consulte os eventos do seu calendário para evitar agendamentos conflituantes.",
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Para bloquear o fuso horário na página de reservas. Útil para eventos presenciais.",
"extensive_whitelabeling": "Apoio dedicado à integração e engenharia",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -840,6 +840,7 @@
"next_step": "Sari peste",
"prev_step": "Pas anterior",
"install": "Instalați",
"install_paid_app": "Abonare",
"installed": "Instalat",
"active_install_one": "{{count}} instalare activă",
"active_install_other": "{{count}} (de) instalări active",

View File

@ -268,6 +268,7 @@
"set_availability": "Задайте время в которое вы доступны",
"availability_settings": "Настройки доступности",
"continue_without_calendar": "Продолжить без подключения календаря",
"continue_with": "Продолжить с {{appName}}",
"connect_your_calendar": "Подключите календарь",
"connect_your_video_app": "Подключите приложения для видеосвязи",
"connect_your_video_app_instructions": "Подключите приложения для видеосвязи и используйте их для разных типов событий.",
@ -288,6 +289,8 @@
"when": "Когда",
"where": "Где",
"add_to_calendar": "Добавить в календарь",
"add_to_calendar_description": "Выберите, куда следует добавлять новые события при бронировании.",
"add_events_to": "Добавлять события в",
"add_another_calendar": "Добавить другой календарь",
"other": "Прочее",
"email_sign_in_subject": "Ссылка для входа в {{appName}}",
@ -422,6 +425,7 @@
"booking_created": "Бронирование создано",
"booking_rejected": "Бронирование отклонено",
"booking_requested": "Запрос на бронирование отправлен",
"booking_payment_initiated": "Начинается оплата за бронирование",
"meeting_ended": "Встреча завершилась",
"form_submitted": "Форма отправлена",
"booking_paid": "Бронирование оплачено",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Этот пользователь еще не создал ни одного события.",
"edit_logo": "Изменить логотип",
"upload_a_logo": "Загрузить логотип",
"upload_logo": "Загрузить логотип",
"remove_logo": "Удалить логотип",
"enable": "Включить",
"code": "Код",
@ -568,6 +573,7 @@
"your_team_name": "Название вашей команды",
"team_updated_successfully": "Команда успешно обновлена",
"your_team_updated_successfully": "Ваша команда успешно обновлена.",
"your_org_updated_successfully": "Ваша организация успешно обновлена.",
"about": "О нас",
"team_description": "Краткое описание вашей команды. Этот текст будет показываться на странице вашей команды.",
"org_description": "Краткое описание вашей организации. Этот текст будет виден на странице вашей организации.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Скрыть кнопку «Забронировать встречу с одним из членов команды»",
"hide_book_a_team_member_description": "Скрыть кнопку «Забронировать встречу с одним из членов команды» с ваших публичных страниц.",
"danger_zone": "Опасная зона",
"account_deletion_cannot_be_undone": "Внимание! Удаление учетной записи нельзя отменить.",
"back": "Назад",
"cancel": "Отмена",
"cancel_all_remaining": "Отменить все оставшиеся",
@ -688,6 +695,7 @@
"people": "Участники",
"your_email": "Ваш адрес электронной почты",
"change_avatar": "Изменить аватар",
"upload_avatar": "Загрузить аватар",
"language": "Язык",
"timezone": "Часовой пояс",
"first_day_of_week": "Начало недели",
@ -778,6 +786,7 @@
"disable_guests": "Отключить участников",
"disable_guests_description": "Отключить добавление дополнительных участников к бронированию.",
"private_link": "Генерировать защищенную ссылку",
"enable_private_url": "Включить защищенный URL",
"private_link_label": "Защищенная ссылка",
"private_link_hint": "Защищенная ссылка повторно генерируется после каждого использования",
"copy_private_link": "Копировать защищенную ссылку",
@ -840,6 +849,7 @@
"next_step": "Пропустить шаг",
"prev_step": "Предыдущий шаг",
"install": "Установить",
"install_paid_app": "Подписаться",
"installed": "Установлено",
"active_install_one": "Активные установки: {{count}}",
"active_install_other": "Активные установки: {{count}}",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Имя организатора",
"app_upgrade_description": "Чтобы использовать эту функцию, необходимо перейти на аккаунт Pro.",
"invalid_number": "Неверный номер телефона",
"invalid_url_error_message": "Недопустимый URL для {{label}}. Пример URL: {{sampleUrl}}",
"navigate": "Перейти",
"open": "Открыть",
"close": "Закрыть",
@ -1276,6 +1287,7 @@
"personal_cal_url": "URL-адрес моего персонального {{appName}}",
"bio_hint": "Несколько предложений о себе. Они будут отображаться на вашей персональной странице.",
"user_has_no_bio": "Пользователь еще не указал данные о себе.",
"bio": "Данные о себе",
"delete_account_modal_title": "Удалить аккаунт",
"confirm_delete_account_modal": "Удалить аккаунт {{appName}}?",
"delete_my_account": "Удалить мой аккаунт",
@ -1286,6 +1298,7 @@
"select_calendars": "Выберите, в каких календарях нужно проверить наличие конфликтов и дублированного бронирования.",
"check_for_conflicts": "Проверить наличие конфликтов",
"view_recordings": "Просмотреть записи",
"check_for_recordings": "Проверить записи",
"adding_events_to": "Добавление событий в",
"follow_system_preferences": "Следовать системным настройкам",
"custom_brand_colors": "Пользовательские фирменные цвета",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "При регистрации поддомена возникла проблема. Попробуйте еще раз или свяжитесь с администратором",
"team_publish": "Опубликовать команду",
"number_text_notifications": "Номер телефона (SMS-уведомления)",
"number_sms_notifications": "Номер телефона (SMS-уведомления)",
"attendee_email_variable": "Электронная почта участника",
"attendee_email_info": "Электронная почта участника, на которого оформляется бронирование",
"kbar_search_placeholder": "Введите команду или начните поиск...",
@ -1594,6 +1608,7 @@
"options": "Настройки",
"enter_option": "Введите параметр {{index}}",
"add_an_option": "Добавить вариант",
"location_already_exists": "Это местоположение уже существует. Выберите другое местоположение",
"radio": "Переключатель",
"google_meet_warning": "Чтобы использовать Google Meet, необходимо установить в качестве целевого календаря Google Календарь",
"individual": "Пользователь",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Отметить, что я занят(а) в выбранные даты",
"date_overrides_add_btn": "Добавить переопределение",
"date_overrides_update_btn": "Обновить переопределение",
"date_successfully_added": "Переопределения даты успешно добавлено",
"event_type_duplicate_copy_text": "{{slug}}-копия",
"set_as_default": "Использовать по умолчанию",
"hide_eventtype_details": "Скрыть информацию о типе события",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Количество организаторов, которые должны участвовать в мероприятии",
"hosts": "Организаторы",
"upgrade_to_enable_feature": "Чтобы использовать эту функцию, нужно создать команду. Нажмите, чтобы создать команду.",
"orgs_upgrade_to_enable_feature": "Для использования этой функции нужно перейти на тарифный план Enterprise.",
"new_attendee": "Новый участник",
"awaiting_approval": "Ожидает одобрения",
"requires_google_calendar": "Для этого приложения необходимо подключение к Google Календарю",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Показывать на странице бронирования",
"get_started_zapier_templates": "Начать работу с шаблонами Zapier",
"team_is_unpublished": "Команда {{team}} снята с публикации",
"org_is_unpublished_description": "Эта ссылка на организацию в настоящее время недоступна. Свяжитесь с владельцем организации и попросите его опубликовать ее.",
"team_is_unpublished_description": "Эта ссылка на {{entity}} в настоящее время недоступна. Свяжитесь с владельцем {{entity}} и попросите его опубликовать ее.",
"team_member": "Участник команды",
"a_routing_form": "Форма маршрутизации",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Изменить настройки ссылки",
"invite_link_copied": "Ссылка для приглашения скопирована",
"invite_link_deleted": "Ссылка для приглашения удалена",
"api_key_deleted": "Ключ API удален",
"invite_link_updated": "Настройки ссылки для приглашения сохранены",
"link_expires_after": "Срок действия ссылок истекает через...",
"one_day": "1 день",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Фамилия участника",
"attendee_first_name_info": "Имя участника, который оформляет бронирование",
"attendee_last_name_info": "Фамилия участника, который оформляет бронирование",
"your_monthly_digest": "Ежемесячный обзор",
"member_name": "Имя участника",
"most_popular_events": "Наиболее популярные события",
"summary_of_events_for_your_team_for_the_last_30_days": "Информация о популярных событиях для вашей команды {{teamName}} за последние 30 дней",
"me": "Я",
"monthly_digest_email": "Письмо с ежемесячным обзором",
"monthly_digest_email_for_teams": "Письмо с ежемесячным обзором для команд",
"verify_team_tooltip": "Подтвердите свою команду, чтобы отправлять участникам сообщения",
"member_removed": "Участник удален",
"my_availability": "Моя доступность",
@ -2039,13 +2064,41 @@
"team_no_event_types": "У этой команды нет типов событий",
"seat_options_doesnt_multiple_durations": "Параметр «место» не поддерживает несколько вариантов продолжительности",
"include_calendar_event": "Включить событие календаря",
"oAuth": "OAuth",
"recently_added": "Недавно добавленные",
"no_members_found": "Участники не найдены",
"event_setup_length_error": "Настройка события: продолжительность должна быть не менее 1 минуты.",
"availability_schedules": "График с информацией о доступности",
"unauthorized": "Неавторизован(а)",
"access_cal_account": "{{clientName}} хочет получить доступ к вашей учетной записи {{appName}}",
"select_account_team": "Выберите учетную запись или команду",
"allow_client_to": "Это позволит {{clientName}}",
"associate_with_cal_account": "Сопоставить вас с персональными данными {{clientName}}",
"see_personal_info": "Просматривать ваши персональные данные, в том числе общедоступную информацию о вас",
"see_primary_email_address": "Видеть ваш основной адрес электронной почты",
"connect_installed_apps": "Подключаться к установленным вами приложениям",
"access_event_type": "Читать, редактировать и удалять ваши типы событий",
"access_availability": "Читать, редактировать и удалить информацию о доступности",
"access_bookings": "Читать, редактировать и удалять ваши бронирования",
"allow_client_to_do": "Разрешить {{clientName}} эти действия?",
"oauth_access_information": "Нажимая кнопку «Разрешить», вы разрешаете этому приложению использовать информацию о вас в соответствии с условиями предоставления услуг и политикой конфиденциальности. Можно запретить доступ в {{appName}} App Store.",
"allow": "Разрешить",
"view_only_edit_availability_not_onboarded": "Этот пользователь не прошел онбординг. Вы сможете настроить для него информацию о доступность, только когда он пройдет онбординг.",
"view_only_edit_availability": "Вы просматриваете информацию о доступности этого пользователя. Вы можете редактировать только информацию о собственной доступности.",
"you_can_override_calendar_in_advanced_tab": "Эти настройки можно переопределить для каждого события в дополнительных параметрах каждого типа события.",
"edit_users_availability": "Редактировать данные о доступности пользователя: {{username}}",
"resend_invitation": "Повторно отправить приглашение",
"invitation_resent": "Приглашение отправлено еще раз.",
"add_client": "Добавить клиента",
"copy_client_secret_info": "После копирования секретного ключа вы больше не сможете его просматривать",
"add_new_client": "Добавить нового клиента",
"this_app_is_not_setup_already": "Это приложение еще не настроено",
"as_csv": "как CSV",
"overlay_my_calendar": "Объединить календарь",
"overlay_my_calendar_toc": "Подключая календарь, вы принимаете нашу политику конфиденциальности и условия использования. Доступ можно отменить в любое время.",
"view_overlay_calendar_events": "Просматривайте события календаря, чтобы не допустить накладок по времени при бронировании.",
"lock_timezone_toggle_on_booking_page": "Заблокируйте часовой пояс на странице бронирования",
"description_lock_timezone_toggle_on_booking_page": "Блокировка часового пояса на странице бронирования подходит для личных событий.",
"extensive_whitelabeling": "Индивидуальная техподдержка и помощь при онбординге",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -849,6 +849,7 @@
"next_step": "Preskoči korak",
"prev_step": "Prethodni korak",
"install": "Instaliraj",
"install_paid_app": "Prijavite se",
"installed": "Instalirano",
"active_install_one": "{{count}} aktivna instalacija",
"active_install_other": "{{count}} aktivnih instalacija",

View File

@ -849,6 +849,7 @@
"next_step": "Hoppa över steg",
"prev_step": "Föregående steg",
"install": "Installera",
"install_paid_app": "Prenumerera",
"installed": "Installerad",
"active_install_one": "{{count}} aktiv installation",
"active_install_other": "{{count}} aktiva installationer",

View File

@ -268,6 +268,7 @@
"set_availability": "Müsaitlik durumunuzu ayarlayın",
"availability_settings": "Müsaitlik Ayarları",
"continue_without_calendar": "Takvim olmadan devam et",
"continue_with": "{{appName}} ile devam et",
"connect_your_calendar": "Takviminizi bağlayın",
"connect_your_video_app": "Video uygulamalarınızı bağlayın",
"connect_your_video_app_instructions": "Etkinlik türlerinizde kullanmak için video uygulamalarınızı bağlayın.",
@ -288,6 +289,8 @@
"when": "Ne zaman",
"where": "Nerede",
"add_to_calendar": "Takvime ekle",
"add_to_calendar_description": "Rezervasyon yaptığınızda etkinliklerin nereye ekleneceğini seçin.",
"add_events_to": "Etkinlikleri şuraya ekle:",
"add_another_calendar": "Başka bir takvim ekle",
"other": "Diğer",
"email_sign_in_subject": "{{appName}} için giriş bağlantınız",
@ -422,6 +425,7 @@
"booking_created": "Rezervasyon Oluşturuldu",
"booking_rejected": "Rezervasyon Reddedildi",
"booking_requested": "Rezervasyon Talep Edildi",
"booking_payment_initiated": "Rezervasyon Ödemesi Başlatıldı",
"meeting_ended": "Toplantı Sona Erdi",
"form_submitted": "Form Gönderildi",
"booking_paid": "Rezervasyon Ücreti Ödendi",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Bu kullanıcı henüz etkinlik türü ayarlamadı.",
"edit_logo": "Logoyu düzenle",
"upload_a_logo": "Logo yükle",
"upload_logo": "Logo yükle",
"remove_logo": "Logoyu kaldır",
"enable": "Etkinleştir",
"code": "Kod",
@ -568,6 +573,7 @@
"your_team_name": "Ekibinizin adı",
"team_updated_successfully": "Ekip başarıyla güncellendi",
"your_team_updated_successfully": "Ekibiniz başarıyla güncellendi.",
"your_org_updated_successfully": "Kuruluşunuz başarıyla güncellendi.",
"about": "Hakkında",
"team_description": "Birkaç cümleyle ekibinizden bahsedin. Bu, ekibinizin URL sayfasında görünecektir.",
"org_description": "Birkaç cümleyle kuruluşunuzdan bahsedin. Bu, kuruluşunuzun URL sayfasında görünecektir.",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "Ekip Üyesi İçin Rezervasyon Yap Düğmesini Gizle",
"hide_book_a_team_member_description": "Ekip Üyesi İçin Rezervasyon Yap Düğmesini herkese açık sayfalarınızda gizleyin.",
"danger_zone": "Tehlikeli bölge",
"account_deletion_cannot_be_undone": "Dikkat. Hesap silme işlemi geri alınamaz.",
"back": "Geri",
"cancel": "İptal et",
"cancel_all_remaining": "Kalanların tümünü iptal et",
@ -688,6 +695,7 @@
"people": "İnsanlar",
"your_email": "E-postanız",
"change_avatar": "Avatarı değiştir",
"upload_avatar": "Avatar Yükle",
"language": "Dil",
"timezone": "Saat dilimi",
"first_day_of_week": "Haftanın İlk Günü",
@ -778,6 +786,7 @@
"disable_guests": "Misafirleri Devre Dışı Bırak",
"disable_guests_description": "Rezervasyon sırasında ek misafir eklemeyi devre dışı bırakın.",
"private_link": "Özel bağlantı oluştur",
"enable_private_url": "Özel URL'yi etkinleştir",
"private_link_label": "Özel bağlantı",
"private_link_hint": "Özel bağlantınız her kullanımdan sonra yeniden oluşturulacak",
"copy_private_link": "Özel bağlantıyı kopyala",
@ -840,6 +849,7 @@
"next_step": "Adımı geç",
"prev_step": "Önceki adım",
"install": "Yükle",
"install_paid_app": "Abone Ol",
"installed": "Yüklendi",
"active_install_one": "{{count}} aktif yükleme",
"active_install_other": "{{count}} aktif yükleme",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "Düzenleyenin adı",
"app_upgrade_description": "Bu özelliği kullanmak için Pro hesabına geçmeniz gerekiyor.",
"invalid_number": "Geçersiz telefon numarası",
"invalid_url_error_message": "{{label}} için geçersiz URL. Örnek URL: {{sampleUrl}}",
"navigate": "Gezin",
"open": "Aç",
"close": "Kapat",
@ -1276,6 +1287,7 @@
"personal_cal_url": "Kişisel {{appName}} URL'm",
"bio_hint": "Birkaç cümleyle kendinizden bahsedin. Bu, kişisel URL sayfanızda görünecektir.",
"user_has_no_bio": "Bu kullanıcı henüz bir biyografi eklemedi.",
"bio": "Biyografi",
"delete_account_modal_title": "Hesabı Sil",
"confirm_delete_account_modal": "{{appName}} hesabınızı silmek istediğinizden emin misiniz?",
"delete_my_account": "Hesabımı sil",
@ -1286,6 +1298,7 @@
"select_calendars": "Çifte rezervasyonu önlemek için çakışmaları kontrol etmek istediğiniz takvimleri seçin.",
"check_for_conflicts": "Çakışmaları kontrol edin",
"view_recordings": "Kayıtları görüntüle",
"check_for_recordings": "Kayıtları kontrol edin",
"adding_events_to": "Etkinlik ekleme",
"follow_system_preferences": "Sistem tercihlerini takip edin",
"custom_brand_colors": "Özel marka renkleri",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "Alt alan adı kaydıyla ilgili bir sorun oluştu, lütfen tekrar deneyin veya bir yöneticiyle iletişime geçin",
"team_publish": "Ekibi yayınla",
"number_text_notifications": "Telefon numarası (SMS bildirimleri)",
"number_sms_notifications": "Telefon numarası (SMS bildirimleri)",
"attendee_email_variable": "Katılımcı e-postası",
"attendee_email_info": "Rezervasyon yaptıran kişinin e-postası",
"kbar_search_placeholder": "Komut girin veya arayın...",
@ -1594,6 +1608,7 @@
"options": "Seçenekler",
"enter_option": "{{index}} Seçeneğini Girin",
"add_an_option": "Bir seçenek ekle",
"location_already_exists": "Bu Konum zaten mevcut. Lütfen yeni bir konum seçin",
"radio": "Radio",
"google_meet_warning": "Google Meet'i kullanmak için hedef takviminizi Google Takvimi olarak ayarlamanız gerekir",
"individual": "Birey",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Seçili tarihlerde müsait değil olarak işaretle",
"date_overrides_add_btn": "Üzerine Yazma Ekle",
"date_overrides_update_btn": "Üzerine Yazmayı Güncelle",
"date_successfully_added": "Veri üzerine yazma başarıyla eklendi",
"event_type_duplicate_copy_text": "{{slug}}-kopya",
"set_as_default": "Varsayılan olarak ayarla",
"hide_eventtype_details": "Etkinlik türü ayrıntılarını gizle",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "Katılması gereken organizatör sayısı",
"hosts": "Organizatörler",
"upgrade_to_enable_feature": "Bu özelliği etkinleştirmek için bir ekip oluşturmanız gerekiyor. Ekip oluşturmak için tıklayın.",
"orgs_upgrade_to_enable_feature": "Bu özelliği etkinleştirmek için kurumsal planımıza geçmeniz gerekiyor.",
"new_attendee": "Yeni Katılımcı",
"awaiting_approval": "Onay Bekleniyor",
"requires_google_calendar": "Bu uygulama, Google Takvim bağlantısı gerektirir",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "Rezervasyon sayfasında göster",
"get_started_zapier_templates": "Zapier şablonlarını kullanmaya başlayın",
"team_is_unpublished": "{{team}} paylaşılmadı",
"org_is_unpublished_description": "Bu kuruluş bağlantısı şu anda kullanılamıyor. Lütfen kuruluş sahibiyle iletişime geçin veya ondan bir bağlantı paylaşmasını isteyin.",
"team_is_unpublished_description": "Bu {{entity}} bağlantısı şu anda kullanılamıyor. Lütfen {{entity}} sahibiyle iletişime geçin veya ondan bir bağlantı paylaşmasını isteyin.",
"team_member": "Ekip üyesi",
"a_routing_form": "Yönlendirme Formu",
@ -1877,6 +1895,7 @@
"edit_invite_link": "Bağlantı ayarlarını düzenle",
"invite_link_copied": "Davet bağlantısı kopyalandı",
"invite_link_deleted": "Davet bağlantısı silindi",
"api_key_deleted": "API Anahtarı silindi",
"invite_link_updated": "Davet bağlantısı ayarları kaydedildi",
"link_expires_after": "Bağlantıların sona erme süresini şuna ayarla...",
"one_day": "1 gün",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "Katılımcının soyadı",
"attendee_first_name_info": "Rezervasyon yaptıran kişinin adı",
"attendee_last_name_info": "Rezervasyon yaptıran kişinin soyadı",
"your_monthly_digest": "Aylık Özetiniz",
"member_name": "Üye Adı",
"most_popular_events": "En Popüler Etkinlikler",
"summary_of_events_for_your_team_for_the_last_30_days": "{{teamName}} ekibinize ait son 30 gündeki popüler etkinliklerin özetini burada bulabilirsiniz",
"me": "Ben",
"monthly_digest_email": "Aylık Özet E-postası",
"monthly_digest_email_for_teams": "Ekipler için aylık özet e-postası",
"verify_team_tooltip": "Katılımcılara mesaj göndermeyi etkinleştirmek için ekibinizi doğrulayın",
"member_removed": "Üye kaldırıldı",
"my_availability": "Müsaitlik Durumum",
@ -2039,13 +2064,41 @@
"team_no_event_types": "Bu ekipte etkinlik türü yok",
"seat_options_doesnt_multiple_durations": "Koltuk seçeneği birden fazla süreyi desteklemiyor",
"include_calendar_event": "Takvim etkinliğini dahil et",
"oAuth": "OAuth",
"recently_added": "Son eklenen",
"no_members_found": "Üye bulunamadı",
"event_setup_length_error": "Etkinlik Kurulumu: Süre en az 1 dakika olmalıdır.",
"availability_schedules": "Müsaitlik Planları",
"unauthorized": "Yetkisiz",
"access_cal_account": "{{clientName}}, {{appName}} hesabınıza erişmek istiyor",
"select_account_team": "Bir hesap veya ekip seçin",
"allow_client_to": "Bu işlem {{clientName}} adlı kullanıcıya şu izinleri verecektir:",
"associate_with_cal_account": "Kişisel bilgilerinizi {{clientName}} kullanıcısındaki bilgilerle ilişkilendirmek",
"see_personal_info": "Herkese açık hale getirdiğiniz kişisel bilgiler de dâhil olmak üzere kişisel bilgilerinizi görmek",
"see_primary_email_address": "Birincil e-posta adresinizi görmek",
"connect_installed_apps": "Yüklü uygulamalarınıza bağlanmak",
"access_event_type": "Etkinlik türlerinizi okumak, düzenlemek, silmek",
"access_availability": "Müsaitlik durumunuzu okuma, düzenleme, silme",
"access_bookings": "Rezervasyonlarınızı okuma, düzenleme, silme",
"allow_client_to_do": "{{clientName}} adlı kullanıcıya bu işlemler için izin verilsin mi?",
"oauth_access_information": "İzin ver'i tıklayarak, bu uygulamanın bilgilerinizi hizmet şartlarına ve gizlilik politikasına uygun şekilde kullanmasına izin vermiş olursunuz. Bu erişim iznini {{appName}} Uygulama Mağazası'ndan kaldırabilirsiniz.",
"allow": "İzin ver",
"view_only_edit_availability_not_onboarded": "Bu kullanıcı katılımı tamamlamadı. Katılımı tamamlayana kadar uygunluk durumlarını ayarlayamazsınız.",
"view_only_edit_availability": "Bu kullanıcının müsaitlik durumunu görüntülüyorsunuz. Yalnızca kendi uygunluk durumunuzu düzenleyebilirsiniz.",
"you_can_override_calendar_in_advanced_tab": "Bu ayarı, her olay türündeki Gelişmiş ayarlardan olay bazında geçersiz kılabilirsiniz.",
"edit_users_availability": "Kullanıcının müsaitlik durumunu düzenleyin: {{username}}",
"resend_invitation": "Davetiyeyi yeniden gönder",
"invitation_resent": "Davetiye tekrar gönderildi.",
"add_client": "Müşteri ekle",
"copy_client_secret_info": "Gizli bilgiyi kopyaladıktan sonra artık görüntüleyemeyeceksiniz",
"add_new_client": "Yeni Müşteri Ekle",
"this_app_is_not_setup_already": "Bu uygulama henüz kurulmadı",
"as_csv": "CSV olarak",
"overlay_my_calendar": "Takvimimin üzerine yaz",
"overlay_my_calendar_toc": "Takviminize bağlanarak gizlilik politikamızı ve kullanım koşullarımızı kabul etmiş olursunuz. Erişimi istediğiniz zaman iptal edebilirsiniz.",
"view_overlay_calendar_events": "Rezervasyonların çakışmasını önlemek için takvim etkinliklerinizi görüntüleyin.",
"lock_timezone_toggle_on_booking_page": "Rezervasyon sayfasında saat dilimini kilitle",
"description_lock_timezone_toggle_on_booking_page": "Rezervasyon sayfasında saat dilimini kilitlemek kişisel etkinlikler için kullanışlıdır.",
"extensive_whitelabeling": "Özel işe alıştırma ve mühendislik desteği",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -849,6 +849,7 @@
"next_step": "Пропустити крок",
"prev_step": "Попередній крок",
"install": "Установити",
"install_paid_app": "Підписатися",
"installed": "Установлено",
"active_install_one": "{{count}} активна інсталяція",
"active_install_other": "Активних інсталяцій: {{count}}",

View File

@ -849,6 +849,7 @@
"next_step": "Bỏ qua bước",
"prev_step": "Bước trước",
"install": "Cài đặt",
"install_paid_app": "Đăng ký",
"installed": "Đã cài đặt",
"active_install_one": "{{count}} cài đặt hoạt động",
"active_install_other": "{{count}} cài đặt hoạt động",
@ -1222,6 +1223,7 @@
"organizer_name_variable": "Tên nhà tổ chức",
"app_upgrade_description": "Để sử dụng tính năng này, bạn cần nâng cấp lên tài khoản Pro.",
"invalid_number": "Số điện thoại không hợp lệ",
"invalid_url_error_message": "URL không hợp lệ cho {{label}}. URL mẫu: {{sampleUrl}}",
"navigate": "Điều hướng",
"open": "Mở",
"close": "Đóng",
@ -1296,6 +1298,7 @@
"select_calendars": "Chọn lịch nào mà bạn muốn kiểm tra xung đột nhằm tránh đặt lịch hẹn trùng.",
"check_for_conflicts": "Kiểm tra xung đột",
"view_recordings": "Xem các bản ghi âm",
"check_for_recordings": "Kiểm tra phần ghi âm",
"adding_events_to": "Thêm các sự kiện vào",
"follow_system_preferences": "Làm theo các tuỳ chọn của hệ thống",
"custom_brand_colors": "Màu thương hiệu tuỳ chỉnh",
@ -1605,6 +1608,7 @@
"options": "Tuỳ chọn",
"enter_option": "Nhập vào tuỳ chọn {{index}}",
"add_an_option": "Thêm một tuỳ chọn",
"location_already_exists": "Vị trí này đã tồn tại. Vui lòng chọn vị trí mới",
"radio": "Nút radio",
"google_meet_warning": "Để dùng Google Meet, bạn phải đặt lịch đích là một Google Calendar",
"individual": "Cá nhân",
@ -1624,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "Đánh dấu bận trong một số ngày đã chọn",
"date_overrides_add_btn": "Thêm ngày ghi đè",
"date_overrides_update_btn": "Cập nhận ngày ghi đè",
"date_successfully_added": "Đã thêm thành công phần ghi đè ngày",
"event_type_duplicate_copy_text": "{{slug}}-bansao",
"set_as_default": "Đặt làm mặc định",
"hide_eventtype_details": "Ẩn chi tiết của loại sự kiện",
@ -2023,7 +2028,13 @@
"attendee_last_name_variable": "Họ người tham dự",
"attendee_first_name_info": "Tên người tham gia lịch hẹn",
"attendee_last_name_info": "Họ người tham gia lịch hẹn",
"your_monthly_digest": "Tin nhanh hằng tháng",
"member_name": "Tên thành viên",
"most_popular_events": "Những sự kiện phổ biến nhất",
"summary_of_events_for_your_team_for_the_last_30_days": "Đây là tóm tắt những sự kiện phổ biến cho nhóm {{teamName}} của bạn trong 30 ngày qua",
"me": "Tôi",
"monthly_digest_email": "Email tin nhanh hằng tháng",
"monthly_digest_email_for_teams": "Email tin nhanh hằng tháng cho các nhóm",
"verify_team_tooltip": "Xác minh nhóm của bạn để kích hoạt chức năng gửi tin nhắn cho người tham dự",
"member_removed": "Thành xiên đã được xoá bỏ",
"my_availability": "Tình trạng trống lịch của tôi",
@ -2053,13 +2064,41 @@
"team_no_event_types": "Nhóm này không có loại sự kiện nào",
"seat_options_doesnt_multiple_durations": "Tuỳ chọn ghế ngồi không hỗ trợ nhiều khoảng thời gian",
"include_calendar_event": "Bao gồm sự kiện lịch",
"oAuth": "OAuth",
"recently_added": "Đã thêm vào gần đây",
"no_members_found": "Không tìm thấy thành viên nào",
"event_setup_length_error": "Thiết lập sự kiện: Thời lượng phải ít nhất 1 phút.",
"availability_schedules": "Lịch trống",
"unauthorized": "Chưa được uỷ quyền",
"access_cal_account": "{{clientName}} muốn truy cập tài khoản {{appName}} của bạn",
"select_account_team": "Chọn tài khoản hoặc nhóm",
"allow_client_to": "Thao tác này sẽ cho phép {{clientName}} thực hiện",
"associate_with_cal_account": "Liên kết bạn với thông tin cá nhân của bạn từ {{clientName}}",
"see_personal_info": "Xem thông tin cá nhân của bạn, bao gồm mọi thông tin cá nhân mà bạn đặt hiển thị công khai",
"see_primary_email_address": "Xem địa chỉ email chính của bạn",
"connect_installed_apps": "Kết nối với những ứng dụng đã cài đặt",
"access_event_type": "Đọc, sửa, xoá những loại sự kiện của bạn",
"access_availability": "Đọc, sửa, xoá tình trạng trống lịch của bạn",
"access_bookings": "Đọc, sửa, xoá các lịch hẹn của bạn",
"allow_client_to_do": "Cho phép {{clientName}} làm điều này?",
"oauth_access_information": "Bằng cách nhấn vào cho phép, bạn cho phép ứng dụng này sử dụng thông tin của bạn theo các điều khoản dịch vụ và chính sách quyền riêng tư. Bạn có thể xoá bỏ quyền truy cập trong App Store của {{appName}}.",
"allow": "Cho phép",
"view_only_edit_availability_not_onboarded": "Người dùng chưa hoàn thành việc gia nhập. Bạn sẽ không thể đặt tình trạng lịch trống cho đến khi họ hoàn thành việc gia nhập.",
"view_only_edit_availability": "Bạn đang xem tình trạng trống lịch của người dùng này. Bạn chỉ có thể sửa tình trạng trống lịch của riêng bạn.",
"you_can_override_calendar_in_advanced_tab": "Bạn có thể ghi đè cái này trên cơ sở từng sự kiện trong thiết lập Nâng cao ở từng loại sự kiện.",
"edit_users_availability": "Sửa tình trạng trống lịch của người dùng: {{username}}",
"resend_invitation": "Gửi lại lời mời",
"invitation_resent": "Lời mời đã được gửi lại.",
"add_client": "Thêm khách hàng",
"copy_client_secret_info": "Sau khi sao chép bí mật, bạn sẽ không thể xem nó được nữa",
"add_new_client": "Thêm khách hàng mới",
"this_app_is_not_setup_already": "Ứng dụng này chưa được thiết lập",
"as_csv": "ở dạng CSV",
"overlay_my_calendar": "Phủ lên lịch của tôi",
"overlay_my_calendar_toc": "Bằng cách kết nối với lịch của bạn, bạn chấp thuận chính sách quyền riêng tư và điều khoản sử dụng của chúng tôi. Bạn có thể rút quyền truy cập bất kì lúc nào.",
"view_overlay_calendar_events": "Xem những sự kiện trong lịch để ngăn việc đặt lịch xung đột.",
"lock_timezone_toggle_on_booking_page": "Khoá múi giờ trên trang lịch hẹn",
"description_lock_timezone_toggle_on_booking_page": "Để khoá múi giờ trên trang lịch hẹn, hữu dụng cho những sự kiện đích thân tham dự.",
"extensive_whitelabeling": "Hướng dẫn bắt đầu và hỗ trợ kỹ thuật chuyên biệt",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -849,6 +849,8 @@
"next_step": "跳过步骤",
"prev_step": "上一步",
"install": "安装",
"install_paid_app": "订阅",
"start_paid_trial": "开始免费试用",
"installed": "已安装",
"active_install_one": "{{count}} 个活动安装",
"active_install_other": "{{count}} 个活动安装",

View File

@ -268,6 +268,7 @@
"set_availability": "設定開放時間",
"availability_settings": "可預約時間設定",
"continue_without_calendar": "在沒有行事曆的情況下繼續",
"continue_with": "繼續使用 {{appName}}",
"connect_your_calendar": "連結行事曆",
"connect_your_video_app": "連接視訊應用程式",
"connect_your_video_app_instructions": "連接視訊應用程式,以便用於活動類型。",
@ -288,6 +289,8 @@
"when": "時間",
"where": "地點",
"add_to_calendar": "加到行事曆",
"add_to_calendar_description": "設定您有新預約時要新增活動的應用程式。",
"add_events_to": "新增活動至",
"add_another_calendar": "新增另一個行事曆",
"other": "其它",
"email_sign_in_subject": "您的「{{appName}}」登入連結",
@ -422,6 +425,7 @@
"booking_created": "已建立預約",
"booking_rejected": "預約已遭拒",
"booking_requested": "預約已提出",
"booking_payment_initiated": "已發起預約付款",
"meeting_ended": "會議已結束",
"form_submitted": "表單已提交",
"booking_paid": "預約已付款",
@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "使用者尚未設定任何活動類型。",
"edit_logo": "編輯標誌",
"upload_a_logo": "上傳標誌",
"upload_logo": "上傳標誌",
"remove_logo": "移除標誌",
"enable": "開啟",
"code": "代碼",
@ -568,6 +573,7 @@
"your_team_name": "取團隊名稱",
"team_updated_successfully": "成功更新團隊",
"your_team_updated_successfully": "更新團隊成功。",
"your_org_updated_successfully": "您的組織已成功更新。",
"about": "關於",
"team_description": "請提供關於團隊的簡介,這會顯示在團隊網址的頁面上。",
"org_description": "請提供關於組織的簡介,這會顯示在組織的網頁上。",
@ -599,6 +605,7 @@
"hide_book_a_team_member": "隱藏「預約團隊成員」按鈕",
"hide_book_a_team_member_description": "隱藏您公開頁面中的「預約團隊成員」按鈕。",
"danger_zone": "危險區域",
"account_deletion_cannot_be_undone": "請注意,帳號一經刪除即無法還原。",
"back": "上一步",
"cancel": "取消",
"cancel_all_remaining": "取消所有剩餘活動",
@ -688,6 +695,7 @@
"people": "人員",
"your_email": "電子郵件",
"change_avatar": "更換大頭照",
"upload_avatar": "上傳大頭照",
"language": "語言",
"timezone": "時區",
"first_day_of_week": "每週的第一天",
@ -778,6 +786,7 @@
"disable_guests": "關閉賓客",
"disable_guests_description": "關閉在預約時,可以增加額外賓客。",
"private_link": "產生不公開的連結",
"enable_private_url": "啟用私人連結",
"private_link_label": "私人連結",
"private_link_hint": "您的私人連結會在每次使用後重新產生",
"copy_private_link": "複製不公開連結",
@ -840,6 +849,7 @@
"next_step": "跳過步驟",
"prev_step": "上個步驟",
"install": "安裝",
"install_paid_app": "訂閱",
"installed": "已安裝",
"active_install_one": "{{count}} 個有效安裝",
"active_install_other": "{{count}} 個有效安裝",
@ -1213,6 +1223,7 @@
"organizer_name_variable": "主辦者姓名",
"app_upgrade_description": "您必須升級至專業版帳號才能使用此功能。",
"invalid_number": "電話號碼無效",
"invalid_url_error_message": "{{label}} 網址無效。網址範例:{{sampleUrl}}",
"navigate": "導覽",
"open": "開啟",
"close": "關閉",
@ -1276,6 +1287,7 @@
"personal_cal_url": "我的個人 {{appName}} 網址",
"bio_hint": "一些關於自己的句子,會顯示在個人網址頁面。",
"user_has_no_bio": "此使用者尚未新增個人資料。",
"bio": "個人資料",
"delete_account_modal_title": "刪除帳號",
"confirm_delete_account_modal": "確定要刪除您的 {{appName}} 的帳號嗎?",
"delete_my_account": "刪除我的帳號",
@ -1286,6 +1298,7 @@
"select_calendars": "選擇您想要檢查衝突的行事曆來避免重複預約。",
"check_for_conflicts": "檢查衝突",
"view_recordings": "檢視錄製內容",
"check_for_recordings": "檢查錄製內容",
"adding_events_to": "新增活動至",
"follow_system_preferences": "遵循系統偏好設定",
"custom_brand_colors": "自訂品牌顏色",
@ -1530,6 +1543,7 @@
"problem_registering_domain": "註冊子網域時發生問題,請再試一次或聯絡管理員",
"team_publish": "發佈團隊",
"number_text_notifications": "電話號碼 (簡訊通知)",
"number_sms_notifications": "電話號碼 (簡訊通知)",
"attendee_email_variable": "與會者電子郵件",
"attendee_email_info": "預約人電子郵件",
"kbar_search_placeholder": "輸入指令或搜尋…",
@ -1594,6 +1608,7 @@
"options": "選項",
"enter_option": "輸入選項 {{index}}",
"add_an_option": "新增選項",
"location_already_exists": "此位置已存在。請選擇新位置",
"radio": "無線電",
"google_meet_warning": "如要使用 Google Meet您必須將目標行事曆設為 Google 日曆",
"individual": "個人",
@ -1613,6 +1628,7 @@
"date_overrides_mark_all_day_unavailable_other": "將所選日期標示不開放",
"date_overrides_add_btn": "新增覆寫",
"date_overrides_update_btn": "更新覆寫",
"date_successfully_added": "日期覆寫已成功新增",
"event_type_duplicate_copy_text": "{{slug}}-複製",
"set_as_default": "設為預設",
"hide_eventtype_details": "隱藏活動類型詳細資料",
@ -1639,6 +1655,7 @@
"minimum_round_robin_hosts_count": "須參加的主辦人數",
"hosts": "主辦人",
"upgrade_to_enable_feature": "您需要建立團隊才能啟用此功能。按一下即可建立團隊。",
"orgs_upgrade_to_enable_feature": "須升級至 Enterprise 方案才能啟用此功能。",
"new_attendee": "新參與者",
"awaiting_approval": "正在等待核准",
"requires_google_calendar": "此應用程式需要 Google 日曆連結",
@ -1743,6 +1760,7 @@
"show_on_booking_page": "在預約頁面上顯示",
"get_started_zapier_templates": "開始使用 Zapier 範本",
"team_is_unpublished": "已取消發佈 {{team}}",
"org_is_unpublished_description": "此組織連結目前無法使用。請聯絡組織擁有者,或請對方發佈。",
"team_is_unpublished_description": "此 {{entity}} 連結目前無法使用。請聯絡 {{entity}} 擁有者,或請對方發佈。",
"team_member": "團隊成員",
"a_routing_form": "引導表單",
@ -1877,6 +1895,7 @@
"edit_invite_link": "編輯連結設定",
"invite_link_copied": "邀請連結已複製",
"invite_link_deleted": "邀請連結已刪除",
"api_key_deleted": "API 金鑰已刪除",
"invite_link_updated": "邀請連結設定已儲存",
"link_expires_after": "將連結效期設為…",
"one_day": "1 天",
@ -2009,7 +2028,13 @@
"attendee_last_name_variable": "與會者姓氏",
"attendee_first_name_info": "預約人名字",
"attendee_last_name_info": "預約人姓氏",
"your_monthly_digest": "您的每月摘要",
"member_name": "成員姓名",
"most_popular_events": "最熱門活動",
"summary_of_events_for_your_team_for_the_last_30_days": "您的團隊「{{teamName}}」最近 30 天的熱門活動摘要如下",
"me": "我",
"monthly_digest_email": "每月摘要電子郵件",
"monthly_digest_email_for_teams": "團隊每月摘要電子郵件",
"verify_team_tooltip": "驗證您的團隊,已啟用傳送訊息給與會者的訊息",
"member_removed": "成員已移除",
"my_availability": "我的可預約時間",
@ -2039,13 +2064,41 @@
"team_no_event_types": "此團隊沒有任何活動類型",
"seat_options_doesnt_multiple_durations": "座位選項不支援多個持續時間",
"include_calendar_event": "包含行事曆活動",
"oAuth": "OAuth",
"recently_added": "最近新增",
"no_members_found": "找不到成員",
"event_setup_length_error": "活動設定:持續時間至少必須為 1 分鐘。",
"availability_schedules": "可預約時間行程表",
"unauthorized": "未授權",
"access_cal_account": "{{clientName}} 想要存取您的 {{appName}} 帳號",
"select_account_team": "選擇帳號或團隊",
"allow_client_to": "這麼做,{{clientName}} 即可",
"associate_with_cal_account": "將您和您在 {{clientName}} 的個人資料建立關聯",
"see_personal_info": "查看您的個人資料,包括您公開的所有個人資料",
"see_primary_email_address": "查看您的主要電子郵件地址",
"connect_installed_apps": "連結至您已安裝的應用程式",
"access_event_type": "讀取、編輯、刪除您的活動類型",
"access_availability": "讀取、編輯、刪除您的可預約時間",
"access_bookings": "讀取、編輯、刪除您的預約",
"allow_client_to_do": "允許 {{clientName}} 這麼做嗎?",
"oauth_access_information": "按一下「允許」後,即表示此應用程式可根據他們的服務和隱私政策使用您的資訊。您可在 {{appName}} App Store 中移除存取權限。",
"allow": "允許",
"view_only_edit_availability_not_onboarded": "此使用者尚未完成入門導覽。在對方完成入門導覽前,您將無法設定對方的可預約時間。",
"view_only_edit_availability": "您正在查看此使用者的可預約時間。您只能編輯自己的可預約時間。",
"you_can_override_calendar_in_advanced_tab": "您可在每個活動類型中的「進階」設定中根據活動覆寫此設定。",
"edit_users_availability": "編輯使用者的可預約時間:{{username}}",
"resend_invitation": "重新傳送邀請",
"invitation_resent": "邀請已重新傳送。",
"add_client": "新增用戶端",
"copy_client_secret_info": "密碼複製後即無法再查看",
"add_new_client": "新增客戶",
"this_app_is_not_setup_already": "此應用程式尚未完成設定",
"as_csv": "採 CSV 格式",
"overlay_my_calendar": "覆蓋我的行事曆",
"overlay_my_calendar_toc": "連接至您的行事曆後,即表示您接受我們的隱私政策和使用條款。您隨時可撤回存取權限。",
"view_overlay_calendar_events": "查看您的行事曆活動,避免預約發生衝突。",
"lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區",
"description_lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區,安排實體活動時非常實用。",
"extensive_whitelabeling": "專屬的入門和工程支援",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -3,7 +3,7 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import superjson from "superjson";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import prisma, { readonlyPrisma } from "@calcom/prisma";
import { createProxySSGHelpers } from "@calcom/trpc/react/ssg";
import { appRouter } from "@calcom/trpc/server/routers/_app";
@ -31,6 +31,7 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
transformer: superjson,
ctx: {
prisma,
insightsDb: readonlyPrisma,
session: null,
locale,
i18n: _i18n,

View File

@ -0,0 +1,275 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { describe } from "vitest";
import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import type { TRequestRescheduleInputSchema } from "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.schema";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { test } from "@calcom/web/test/fixtures/fixtures";
import {
createBookingScenario,
getGoogleCalendarCredential,
TestData,
getOrganizer,
getBooker,
getScenarioData,
getMockBookingAttendee,
getDate,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { expectBookingRequestRescheduledEmails } from "@calcom/web/test/utils/bookingScenario/expects";
import { getSampleUserInSession } from "../utils/bookingScenario/getSampleUserInSession";
import { setupAndTeardown } from "../utils/bookingScenario/setupAndTeardown";
export type CustomNextApiRequest = NextApiRequest & Request;
export type CustomNextApiResponse = NextApiResponse & Response;
describe("Handler: requestReschedule", () => {
setupAndTeardown();
describe("User Event Booking", () => {
test(`should be able to request-reschedule for a user booking
1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({
emails,
}) => {
const { requestRescheduleHandler } = await import(
"@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
);
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const bookingUid = "MOCKED_BOOKING_UID";
const eventTypeSlug = "event-type-1";
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slug: eventTypeSlug,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
uid: bookingUid,
eventTypeId: 1,
userId: 101,
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:15:00.000Z`,
attendees: [
getMockBookingAttendee({
id: 2,
name: booker.name,
email: booker.email,
// Booker's locale when the fresh booking happened earlier
locale: "hi",
// Booker's timezone when the fresh booking happened earlier
timeZone: "Asia/Kolkata",
}),
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
const loggedInUser = {
organizationId: null,
id: 101,
username: "reschedule-requester",
name: "Reschedule Requester",
email: "reschedule-requester@example.com",
};
await requestRescheduleHandler(
getTrpcHandlerData({
user: loggedInUser,
input: {
bookingId: bookingUid,
rescheduleReason: "",
},
})
);
expectBookingRequestRescheduledEmails({
booking: {
uid: bookingUid,
},
booker,
organizer: organizer,
loggedInUser,
emails,
bookNewTimePath: `/${organizer.username}/${eventTypeSlug}`,
});
});
});
describe("Team Event Booking", () => {
test(`should be able to request-reschedule for a team event booking
1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({
emails,
}) => {
const { requestRescheduleHandler } = await import(
"@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
);
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
teams: [
{
membership: {
accepted: true,
},
team: {
id: 1,
name: "Team 1",
slug: "team-1",
},
},
],
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const bookingUid = "MOCKED_BOOKING_UID";
const eventTypeSlug = "event-type-1";
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slug: eventTypeSlug,
slotInterval: 45,
teamId: 1,
schedulingType: SchedulingType.COLLECTIVE,
length: 45,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
uid: bookingUid,
eventTypeId: 1,
userId: 101,
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:15:00.000Z`,
attendees: [
getMockBookingAttendee({
id: 2,
name: booker.name,
email: booker.email,
// Booker's locale when the fresh booking happened earlier
locale: "hi",
// Booker's timezone when the fresh booking happened earlier
timeZone: "Asia/Kolkata",
}),
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
const loggedInUser = {
organizationId: null,
id: 101,
username: "reschedule-requester",
name: "Reschedule Requester",
email: "reschedule-requester@example.com",
};
await requestRescheduleHandler(
getTrpcHandlerData({
user: loggedInUser,
input: {
bookingId: bookingUid,
rescheduleReason: "",
},
})
);
expectBookingRequestRescheduledEmails({
booking: {
uid: bookingUid,
},
booker,
organizer: organizer,
loggedInUser,
emails,
bookNewTimePath: "/team/team-1/event-type-1",
});
});
test.todo("Verify that the email should go to organizer as well as the team members");
});
});
function getTrpcHandlerData({
input,
user,
}: {
input: TRequestRescheduleInputSchema;
user: Partial<Omit<NonNullable<TrpcSessionUser>, "id" | "email" | "username">> &
Pick<NonNullable<TrpcSessionUser>, "id" | "email" | "username">;
}) {
return {
ctx: {
user: {
...getSampleUserInSession(),
...user,
} satisfies TrpcSessionUser,
},
input: input,
};
}

View File

@ -99,15 +99,47 @@ describe("next.config.js - Org Rewrite", () => {
expect(orgUserRouteMatch("/abc")?.params).toContain({
user: "abc",
});
// Tests that something that starts with 'd' which could accidentally match /d route is correctly identified as a booking page
expect(orgUserRouteMatch("/designer")?.params).toContain({
user: "designer",
});
// Tests that something that starts with 'apps' which could accidentally match /apps route is correctly identified as a booking page
expect(orgUserRouteMatch("/apps-conflict-possibility")?.params).toContain({
user: "apps-conflict-possibility",
});
// Tests that something that starts with '_next' which could accidentally match /_next route is correctly identified as a booking page
expect(orgUserRouteMatch("/_next-candidate")?.params).toContain({
user: "_next-candidate",
});
// Tests that something that starts with 'public' which could accidentally match /public route is correctly identified as a booking page
expect(orgUserRouteMatch("/public-person")?.params).toContain({
user: "public-person",
});
});
it("Non booking pages", () => {
expect(orgUserTypeRouteMatch("/_next/def")).toEqual(false);
expect(orgUserTypeRouteMatch("/public/def")).toEqual(false);
expect(orgUserRouteMatch("/_next/")).toEqual(false);
expect(orgUserRouteMatch("/public/")).toEqual(false);
expect(orgUserRouteMatch("/event-types/")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types/")).toEqual(false);
expect(orgUserRouteMatch("/event-types/?abc=1")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types/?abc=1")).toEqual(false);
expect(orgUserRouteMatch("/event-types")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types")).toEqual(false);
expect(orgUserRouteMatch("/event-types?abc=1")).toEqual(false);
expect(orgUserTypeRouteMatch("/event-types?abc=1")).toEqual(false);
expect(orgUserTypeRouteMatch("/john/avatar.png")).toEqual(false);
expect(orgUserTypeRouteMatch("/cancel/abcd")).toEqual(false);
expect(orgUserTypeRouteMatch("/success/abcd")).toEqual(false);

View File

@ -2,15 +2,15 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store";
import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n";
import prismock from "../../../../../tests/libs/__mocks__/prisma";
import type { BookingReference, Attendee, Booking } from "@prisma/client";
import type { BookingReference, Attendee, Booking, Membership } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { WebhookTriggerEvents } from "@prisma/client";
import type Stripe from "stripe";
import type { getMockRequestDataForBooking } from "test/utils/bookingScenario/getMockRequestDataForBooking";
import { v4 as uuidv4 } from "uuid";
import "vitest-fetch-mock";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import type { getMockRequestDataForBooking } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking";
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
import type { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
@ -68,6 +68,14 @@ type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
credentials?: InputCredential[];
organizationId?: number | null;
selectedCalendars?: InputSelectedCalendar[];
teams?: {
membership: Partial<Membership>;
team: {
id: number;
name: string;
slug: string;
};
}[];
schedules: {
// Allows giving id in the input directly so that it can be referenced somewhere else as well
id?: number;
@ -98,6 +106,7 @@ export type InputEventType = {
schedulingType?: SchedulingType;
beforeEventBuffer?: number;
afterEventBuffer?: number;
teamId?: number | null;
requiresConfirmation?: boolean;
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
schedule?: InputUser["schedules"][number];
@ -373,6 +382,7 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma
allUsers: await prismock.user.findMany({
include: {
credentials: true,
teams: true,
schedules: {
include: {
availability: true,
@ -385,9 +395,32 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma
);
}
async function addTeamsToDb(teams: NonNullable<InputUser["teams"]>[number]["team"][]) {
log.silly("TestData: Creating Teams", JSON.stringify(teams));
await prismock.team.createMany({
data: teams,
});
const addedTeams = await prismock.team.findMany({
where: {
id: {
in: teams.map((team) => team.id),
},
},
});
log.silly(
"Added teams to Db",
safeStringify({
addedTeams,
})
);
return addedTeams;
}
async function addUsers(users: InputUser[]) {
const prismaUsersCreate = users.map((user) => {
const newUser = user;
const prismaUsersCreate = [];
for (let i = 0; i < users.length; i++) {
const newUser = users[i];
const user = users[i];
if (user.schedules) {
newUser.schedules = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -415,6 +448,22 @@ async function addUsers(users: InputUser[]) {
},
};
}
if (user.teams) {
const addedTeams = await addTeamsToDb(user.teams.map((team) => team.team));
newUser.teams = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
createMany: {
data: user.teams.map((team, index) => {
return {
teamId: addedTeams[index].id,
...team.membership,
};
}),
},
};
}
if (user.selectedCalendars) {
newUser.selectedCalendars = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -425,8 +474,8 @@ async function addUsers(users: InputUser[]) {
};
}
return newUser;
});
prismaUsersCreate.push(newUser);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
await addUsersToDb(prismaUsersCreate);
@ -739,6 +788,7 @@ export function getOrganizer({
selectedCalendars,
destinationCalendar,
defaultScheduleId,
teams,
}: {
name: string;
email: string;
@ -748,6 +798,7 @@ export function getOrganizer({
selectedCalendars?: InputSelectedCalendar[];
defaultScheduleId?: number | null;
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
teams?: InputUser["teams"];
}) {
return {
...TestData.users.example,
@ -760,6 +811,7 @@ export function getOrganizer({
selectedCalendars,
destinationCalendar,
defaultScheduleId,
teams,
};
}

View File

@ -1,6 +1,9 @@
import { createMocks } from "node-mocks-http";
import type { CustomNextApiRequest, CustomNextApiResponse } from "../fresh-booking.test";
import type {
CustomNextApiRequest,
CustomNextApiResponse,
} from "@calcom/features/bookings/lib/handleNewBooking/test/fresh-booking.test";
export function createMockNextJsRequest(...args: Parameters<typeof createMocks>) {
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);

View File

@ -8,7 +8,6 @@ import { expect } from "vitest";
import "vitest-fetch-mock";
import dayjs from "@calcom/dayjs";
import { DEFAULT_TIMEZONE_BOOKER } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
@ -17,7 +16,8 @@ import type { AppsStatus } from "@calcom/types/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
import type { InputEventType } from "./bookingScenario";
import type { InputEventType, getOrganizer } from "./bookingScenario";
import { DEFAULT_TIMEZONE_BOOKER } from "./getMockRequestDataForBooking";
// This is too complex at the moment, I really need to simplify this.
// Maybe we can replace the exact match with a partial match approach that would be easier to maintain but we would still need Dayjs to do the timezone conversion
@ -66,7 +66,7 @@ type ExpectedEmail = {
// };
ics?: {
filename: string;
iCalUID: string;
iCalUID?: string;
recurrence?: Recurrence;
};
/**
@ -96,7 +96,7 @@ expect.extend({
logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog }));
return {
pass: false,
message: () => `No email sent to ${to}`,
message: () => `No email sent to ${to}. All emails are ${JSON.stringify(emailsToLog)}`,
};
}
const ics = testEmail.icalEvent;
@ -105,9 +105,10 @@ expect.extend({
let isToAddressExpected = true;
const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true;
const isIcsUIDExpected = expectedEmail.ics
? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null)
: true;
const isIcsUIDExpected =
expectedEmail.ics && expectedEmail.ics.iCalUID
? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null)
: true;
const emailDom = parse(testEmail.html);
const actualEmailContent = {
@ -172,7 +173,7 @@ expect.extend({
if (expectedEmail.noIcs && ics) {
return {
pass: false,
message: () => `${isNot ? "" : "Not"} expected ics file`,
message: () => `${isNot ? "" : "Not"} expected ics file ${JSON.stringify(ics)}`,
};
}
@ -584,6 +585,7 @@ export function expectSuccessfulBookingRescheduledEmails({
`${booker.name} <${booker.email}>`
);
}
export function expectAwaitingPaymentEmails({
emails,
booker,
@ -630,6 +632,59 @@ export function expectBookingRequestedEmails({
);
}
export function expectBookingRequestRescheduledEmails({
emails,
loggedInUser,
booker,
booking,
bookNewTimePath,
organizer,
}: {
emails: Fixtures["emails"];
organizer: ReturnType<typeof getOrganizer>;
loggedInUser: {
email: string;
name: string;
};
booker: { email: string; name: string };
booking: { uid: string; urlOrigin?: string };
bookNewTimePath: string;
}) {
const bookingUrlOrigin = booking.urlOrigin || WEBAPP_URL;
expect(emails).toHaveEmail(
{
titleTag: "rescheduled_event_type_subject",
heading: "request_reschedule_booking",
subHeading: "request_reschedule_subtitle",
links: [
{
href: `${bookingUrlOrigin}${bookNewTimePath}?rescheduleUid=${booking.uid}`,
text: "Book a new time",
},
],
to: `${booker.email}`,
ics: {
filename: "event.ics",
},
},
`${booker.email}`
);
expect(emails).toHaveEmail(
{
titleTag: "rescheduled_event_type_subject",
heading: "request_reschedule_title_organizer",
subHeading: "request_reschedule_subtitle_organizer",
to: `${loggedInUser.email}`,
ics: {
filename: "event.ics",
},
},
`${loggedInUser.email}`
);
}
export function expectBookingRequestedWebhookToHaveBeenFired({
booker,
location,

View File

@ -0,0 +1,43 @@
import { UserPermissionRole } from "@calcom/prisma/client";
import { IdentityProvider } from "@calcom/prisma/enums";
export const getSampleUserInSession = function () {
return {
locale: "",
avatar: "",
organization: {
isOrgAdmin: false,
metadata: null,
},
defaultScheduleId: null,
name: "",
defaultBookerLayouts: null,
timeZone: "Asia/Kolkata",
selectedCalendars: [],
destinationCalendar: null,
emailVerified: new Date(),
allowDynamicBooking: false,
bio: "",
weekStart: "",
startTime: 0,
endTime: 0,
bufferTime: 0,
hideBranding: false,
timeFormat: 12,
twoFactorEnabled: false,
identityProvider: IdentityProvider.CAL,
brandColor: "#292929",
darkBrandColor: "#fafafa",
away: false,
metadata: null,
role: UserPermissionRole.USER,
disableImpersonation: false,
organizationId: null,
theme: "",
createdDate: new Date(),
trialEndsAt: new Date(),
completedOnboarding: false,
allowSEOIndexing: false,
receiveMonthlyDigestEmail: false,
};
};

View File

@ -8,10 +8,15 @@ import {
export function setupAndTeardown() {
beforeEach(() => {
// Required to able to generate token in email in some cases
//@ts-expect-error - It is a readonly variable
process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui";
//@ts-expect-error - It is a readonly variable
process.env.STRIPE_WEBHOOK_SECRET = "MOCK_STRIPE_WEBHOOK_SECRET";
// We are setting it in vitest.config.ts because otherwise it's too late to set it.
// process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY";
// Ensure that Rate Limiting isn't enforced for tests
delete process.env.UPSTASH_REDIS_REST_URL;
mockNoTranslations();
// mockEnableEmailFeature();
enableEmailFeature();
@ -19,7 +24,9 @@ export function setupAndTeardown() {
fetchMock.resetMocks();
});
afterEach(() => {
//@ts-expect-error - It is a readonly variable
delete process.env.CALENDSO_ENCRYPTION_KEY;
//@ts-expect-error - It is a readonly variable
delete process.env.STRIPE_WEBHOOK_SECRET;
delete process.env.DAILY_API_KEY;
globalThis.testEmails = [];

View File

@ -67,10 +67,10 @@ export const testWithAndWithoutOrg = (
_testWithAndWithoutOrg(description, fn, timeout, "run");
};
testWithAndWithoutOrg.only = ((description, fn) => {
_testWithAndWithoutOrg(description, fn, "only");
testWithAndWithoutOrg.only = ((description, fn, timeout) => {
_testWithAndWithoutOrg(description, fn, timeout, "only");
}) as typeof _testWithAndWithoutOrg;
testWithAndWithoutOrg.skip = ((description, fn) => {
_testWithAndWithoutOrg(description, fn, "skip");
testWithAndWithoutOrg.skip = ((description, fn, timeout) => {
_testWithAndWithoutOrg(description, fn, timeout, "skip");
}) as typeof _testWithAndWithoutOrg;

View File

@ -4,5 +4,13 @@
"path": "/api/cron/calendar-cache-cleanup",
"schedule": "0 5 * * *"
}
]
],
"functions": {
"pages/api/trpc/public/[trpc].ts": {
"memory": 768
},
"pages/api/trpc/slots/[trpc].ts": {
"memory": 768
}
}
}

View File

@ -58,9 +58,12 @@ COPY --from=build /app/yarn.lock ./yarn.lock
COPY --from=build /app/packages/config ./packages/config
COPY --from=build /app/packages/tsconfig ./packages/tsconfig
COPY --from=build /app/packages/types ./packages/types
COPY --from=build /app/apps/web/next.config.js ./apps/web/next.config.js
COPY --from=build /app/apps/web/next-i18next.config.js ./apps/web/next-i18next.config.js
COPY --from=build /app/apps/web/public/static/locales ./apps/web/public/static/locales
COPY --from=build /app/apps/web/package.json ./apps/web/package.json
# Expose port 80
EXPOSE 80
# Start cmd, called when docker image is mounted
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]

View File

@ -90,6 +90,7 @@ export default function AppCard({
{app?.isInstalled || app.credentialOwner ? (
<div className="ml-auto flex items-center">
<Switch
data-testid="app-switch"
disabled={!app.enabled || disabled}
onCheckedChange={(enabled) => {
if (switchOnClick) {

View File

@ -15,25 +15,36 @@ export async function checkInstalled(slug: string, userId: number) {
}
}
type InstallationArgs = {
appType: string;
userId: number;
slug: string;
key?: Prisma.InputJsonValue;
teamId?: number;
subscriptionId?: string | null;
paymentStatus?: string | null;
billingCycleStart?: number | null;
};
export async function createDefaultInstallation({
appType,
userId,
slug,
key = {},
teamId,
}: {
appType: string;
userId: number;
slug: string;
key?: Prisma.InputJsonValue;
teamId?: number;
}) {
billingCycleStart,
paymentStatus,
subscriptionId,
}: InstallationArgs) {
const installation = await prisma.credential.create({
data: {
type: appType,
key,
...(teamId ? { teamId } : { userId }),
appId: slug,
subscriptionId,
paymentStatus,
billingCycleStart,
},
});
if (!installation) {

View File

@ -0,0 +1,92 @@
import type Stripe from "stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getStripeCustomerIdFromUserId, stripe } from "./stripe";
interface RedirectArgs {
userId: number;
appSlug: string;
appPaidMode: string;
priceId: string;
trialDays?: number;
}
export const withPaidAppRedirect = async ({
appSlug,
appPaidMode,
priceId,
userId,
trialDays,
}: RedirectArgs) => {
const redirect_uri = `${WEBAPP_URL}/api/integrations/${appSlug}/callback?checkoutId={CHECKOUT_SESSION_ID}`;
const stripeCustomerId = await getStripeCustomerIdFromUserId(userId);
const checkoutSession = await stripe.checkout.sessions.create({
success_url: redirect_uri,
cancel_url: redirect_uri,
mode: appPaidMode === "subscription" ? "subscription" : "payment",
payment_method_types: ["card"],
allow_promotion_codes: true,
customer: stripeCustomerId,
line_items: [
{
quantity: 1,
price: priceId,
},
],
client_reference_id: userId.toString(),
...(trialDays
? {
subscription_data: {
trial_period_days: trialDays,
trial_settings: { end_behavior: { missing_payment_method: "cancel" } },
},
}
: undefined),
});
return checkoutSession.url;
};
export const withStripeCallback = async (
checkoutId: string,
appSlug: string,
callback: (args: { checkoutSession: Stripe.Checkout.Session }) => Promise<{ url: string }>
): Promise<{ url: string }> => {
if (!checkoutId) {
return {
url: `/apps/installed?error=${encodeURIComponent(
JSON.stringify({ message: "No Stripe Checkout Session ID" })
)}`,
};
}
const checkoutSession = await stripe.checkout.sessions.retrieve(checkoutId);
if (!checkoutSession) {
return {
url: `/apps/installed?error=${encodeURIComponent(
JSON.stringify({ message: "Unknown Stripe Checkout Session ID" })
)}`,
};
}
if (checkoutSession.payment_status !== "paid") {
return {
url: `/apps/installed?error=${encodeURIComponent(
JSON.stringify({ message: "Stripe Payment not processed" })
)}`,
};
}
if (checkoutSession.mode === "subscription" && checkoutSession.subscription) {
await stripe.subscriptions.update(checkoutSession.subscription.toString(), {
metadata: {
appSlug,
},
});
}
// Execute the callback if all checks pass
return callback({ checkoutSession });
};

View File

@ -0,0 +1,74 @@
import { Prisma } from "@prisma/client";
import Stripe from "stripe";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
export async function getStripeCustomerIdFromUserId(userId: number) {
// Get user
const user = await prisma.user.findUnique({
where: {
id: userId,
},
select: {
email: true,
name: true,
metadata: true,
},
});
if (!user?.email) throw new HttpError({ statusCode: 404, message: "User email not found" });
const customerId = await getStripeCustomerId(user);
return customerId;
}
const userType = Prisma.validator<Prisma.UserArgs>()({
select: {
email: true,
metadata: true,
},
});
export type UserType = Prisma.UserGetPayload<typeof userType>;
/** This will retrieve the customer ID from Stripe or create it if it doesn't exists yet. */
export async function getStripeCustomerId(user: UserType): Promise<string> {
let customerId: string | null = null;
if (user?.metadata && typeof user.metadata === "object" && "stripeCustomerId" in user.metadata) {
customerId = (user?.metadata as Prisma.JsonObject).stripeCustomerId as string;
} else {
/* We fallback to finding the customer by email (which is not optimal) */
const customersResponse = await stripe.customers.list({
email: user.email,
limit: 1,
});
if (customersResponse.data[0]?.id) {
customerId = customersResponse.data[0].id;
} else {
/* Creating customer on Stripe and saving it on prisma */
const customer = await stripe.customers.create({ email: user.email });
customerId = customer.id;
}
await prisma.user.update({
where: {
email: user.email,
},
data: {
metadata: {
...(user.metadata as Prisma.JsonObject),
stripeCustomerId: customerId,
},
},
});
}
return customerId;
}
const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY || "";
export const stripe = new Stripe(stripePrivateKey, {
apiVersion: "2020-08-27",
});

View File

@ -3,12 +3,16 @@ import type { Booking, Payment, PaymentOption, Prisma } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import type z from "zod";
import { ErrorCode } from "@calcom/lib/errorCodes";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
import { albyCredentialKeysSchema } from "./albyCredentialKeysSchema";
const log = logger.getSubLogger({ prefix: ["payment-service:alby"] });
export class PaymentService implements IAbstractPaymentService {
private credentials: z.infer<typeof albyCredentialKeysSchema> | null;
@ -36,7 +40,7 @@ export class PaymentService implements IAbstractPaymentService {
},
});
if (!booking || !this.credentials?.account_lightning_address) {
throw new Error();
throw new Error("Alby: Booking or Lightning address not found");
}
const uid = uuidv4();
@ -80,8 +84,8 @@ export class PaymentService implements IAbstractPaymentService {
}
return paymentData;
} catch (error) {
console.error(error);
throw new Error("Payment could not be created");
log.error("Alby: Payment could not be created", bookingId);
throw new Error(ErrorCode.PaymentCreationFailure);
}
}
async update(): Promise<Payment> {

View File

@ -29,6 +29,7 @@ export const appDataSchema = eventTypeAppCardZod.merge(
currency: z.string(),
paymentOption: z.string().optional(),
enabled: z.boolean().optional(),
credentialId: z.number().optional(),
})
);
export const appKeysSchema = z.object({

View File

@ -7,46 +7,63 @@ import { apiKeysRouter } from "@calcom/trpc/server/routers/viewer/apiKeys/_route
import checkSession from "../../_utils/auth";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { checkInstalled, createDefaultInstallation } from "../../_utils/installation";
import { withPaidAppRedirect } from "../../_utils/paid-apps";
import appConfig from "../config.json";
const trialEndDate = new Date(Date.UTC(2023, 11, 1));
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const session = checkSession(req);
const slug = appConfig.slug;
const appType = appConfig.type;
const ctx = await createContext({ req, res });
const caller = apiKeysRouter.createCaller(ctx);
// if date is in the future, we install normally.
if (new Date() < trialEndDate) {
const ctx = await createContext({ req, res });
const caller = apiKeysRouter.createCaller(ctx);
const apiKey = await caller.create({
note: "Cal.ai",
expiresAt: null,
appId: "cal-ai",
});
const apiKey = await caller.create({
note: "Cal.ai",
expiresAt: null,
appId: "cal-ai",
});
await checkInstalled(slug, session.user.id);
await createDefaultInstallation({
appType,
userId: session.user.id,
slug,
key: {
apiKey,
},
});
await fetch(
`${process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://cal.ai"}/api/onboard`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
await checkInstalled(appConfig.slug, session.user.id);
await createDefaultInstallation({
appType: appConfig.type,
userId: session.user.id,
slug: appConfig.slug,
key: {
apiKey,
},
body: JSON.stringify({
userId: session.user.id,
}),
}
);
});
return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "cal-ai" }) };
await fetch(
`${process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://cal.ai"}/api/onboard`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: session.user.id,
}),
}
);
return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "cal-ai" }) };
}
const redirectUrl = await withPaidAppRedirect({
appPaidMode: appConfig.paid.mode,
appSlug: appConfig.slug,
userId: session.user.id,
priceId: appConfig.paid.priceId,
});
if (!redirectUrl) {
return res.status(500).json({ message: "Failed to create Stripe checkout session" });
}
return { url: redirectUrl };
}
export default defaultResponder(getHandler);

View File

@ -0,0 +1,65 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { apiKeysRouter } from "@calcom/trpc/server/routers/viewer/apiKeys/_router";
import checkSession from "../../_utils/auth";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { checkInstalled, createDefaultInstallation } from "../../_utils/installation";
import { withStripeCallback } from "../../_utils/paid-apps";
import appConfig from "../config.json";
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const session = checkSession(req);
const slug = appConfig.slug;
const appType = appConfig.type;
const { checkoutId } = req.query as { checkoutId: string };
if (!checkoutId) {
return { url: `/apps/installed?error=${JSON.stringify({ message: "No Stripe Checkout Session ID" })}` };
}
const { url } = await withStripeCallback(checkoutId, slug, async ({ checkoutSession }) => {
const ctx = await createContext({ req, res });
const caller = apiKeysRouter.createCaller(ctx);
const apiKey = await caller.create({
note: "Cal.ai",
expiresAt: null,
appId: "cal-ai",
});
await checkInstalled(slug, session.user.id);
await createDefaultInstallation({
appType,
userId: session.user.id,
slug,
key: {
apiKey,
},
subscriptionId: checkoutSession.subscription?.toString(),
billingCycleStart: new Date().getDate(),
paymentStatus: "active",
});
await fetch(
`${process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://cal.ai"}/api/onboard`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: session.user.id,
}),
}
);
return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "cal-ai" }) };
});
return res.redirect(url);
}
export default defaultResponder(getHandler);

View File

@ -0,0 +1,5 @@
import { defaultHandler } from "@calcom/lib/server";
export default defaultHandler({
GET: import("./_getCallback"),
});

View File

@ -1 +1,2 @@
export { default as add } from "./add";
export { default as callback } from "./callback";

View File

@ -3,7 +3,6 @@
"name": "Cal.ai",
"slug": "cal-ai",
"type": "cal-ai_automation",
"trial": 14,
"logo": "icon.png",
"url": "https://cal.ai",
"variant": "automation",
@ -14,5 +13,10 @@
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"dirName": "cal-ai"
"dirName": "cal-ai",
"paid": {
"priceInUsd": 25,
"priceId": "price_1O1ziDH8UDiwIftkDHp3MCTP",
"mode": "subscription"
}
}

View File

@ -428,10 +428,11 @@ export const getTranslatedLocation = (
return translatedLocation;
};
export const getOrganizerInputLocationsType = () => {
const locationTypes: DefaultEventLocationType["type"] | EventLocationTypeFromApp["type"][] = [];
const locations = locationsTypes.filter((location) => !!location.organizerInputType);
locations?.forEach((l) => locationTypes.push(l.type));
export const getOrganizerInputLocationTypes = () => {
const result: DefaultEventLocationType["type"] | EventLocationTypeFromApp["type"][] = [];
return locationTypes;
const locations = locationsTypes.filter((location) => !!location.organizerInputType);
locations?.forEach((l) => result.push(l.type));
return result;
};

View File

@ -25,7 +25,8 @@
"@calcom/zoomvideo": "*",
"lodash": "^4.17.21",
"qs-stringify": "^1.2.1",
"react-i18next": "^12.2.0"
"react-i18next": "^12.2.0",
"stripe": "^14.3.0"
},
"devDependencies": {
"@calcom/types": "*"

View File

@ -4,12 +4,16 @@ import z from "zod";
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
import { paymentOptionEnum } from "../zod";
const log = logger.getSubLogger({ prefix: ["payment-service:paypal"] });
export const paypalCredentialKeysSchema = z.object({
client_id: z.string(),
secret_key: z.string(),
@ -87,8 +91,8 @@ export class PaymentService implements IAbstractPaymentService {
}
return paymentData;
} catch (error) {
console.error(error);
throw new Error("Payment could not be created");
log.error("Paypal: Payment could not be created for bookingId", bookingId);
throw new Error(ErrorCode.PaymentCreationFailure);
}
}
async update(): Promise<Payment> {
@ -166,8 +170,8 @@ export class PaymentService implements IAbstractPaymentService {
}
return paymentData;
} catch (error) {
console.error(error);
throw new Error("Payment could not be created");
log.error("Paypal: Payment method could not be collected for bookingId", bookingId);
throw new Error("Paypal: Payment method could not be collected");
}
}
chargeCard(

View File

@ -1,6 +1,7 @@
import { z } from "zod";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import config from "../config.json";
const shimmerAppKeysSchema = z.object({
api_key: z.string(),
@ -8,6 +9,6 @@ const shimmerAppKeysSchema = z.object({
});
export const getShimmerAppKeys = async () => {
const appKeys = await getAppKeysFromSlug("shimmer-video");
const appKeys = await getAppKeysFromSlug(config.slug);
return shimmerAppKeysSchema.parse(appKeys);
};

View File

@ -3,6 +3,7 @@ import z from "zod";
import { getCustomerAndCheckoutSession } from "@calcom/app-store/stripepayment/lib/getCustomerAndCheckoutSession";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
@ -22,7 +23,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { callbackUrl, checkoutSessionId } = querySchema.parse(req.query);
const { stripeCustomer, checkoutSession } = await getCustomerAndCheckoutSession(checkoutSessionId);
if (!stripeCustomer) return { message: "Stripe customer not found or deleted" };
if (!stripeCustomer)
throw new HttpError({
statusCode: 404,
message: "Stripe customer not found or deleted",
url: req.url,
method: req.method,
});
// first let's try to find user by metadata stripeCustomerId
let user = await prisma.user.findFirst({
@ -43,10 +50,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
});
}
if (!user)
throw new HttpError({ statusCode: 404, message: "User not found", url: req.url, method: req.method });
if (checkoutSession.payment_status === "paid" && stripeCustomer.metadata.username) {
try {
if (!user) return { message: "User not found" };
await prisma.user.update({
data: {
username: stripeCustomer.metadata.username,
@ -61,10 +69,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
});
} catch (error) {
console.error(error);
return {
throw new HttpError({
statusCode: 400,
url: req.url,
method: req.method,
message:
"We have received your payment. Your premium username could still not be reserved. Please contact support@cal.com and mention your premium username",
};
});
}
}
callbackUrl.searchParams.set("paymentStatus", checkoutSession.payment_status);

View File

@ -4,7 +4,9 @@ import { v4 as uuidv4 } from "uuid";
import z from "zod";
import { sendAwaitingPaymentEmail } from "@calcom/emails";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
@ -14,6 +16,8 @@ import { createPaymentLink } from "./client";
import { retrieveOrCreateStripeCustomerByEmail } from "./customer";
import type { StripePaymentData, StripeSetupIntentData } from "./server";
const log = logger.getSubLogger({ prefix: ["payment-service:stripe"] });
export const stripeCredentialKeysSchema = z.object({
stripe_user_id: z.string(),
default_currency: z.string(),
@ -129,7 +133,8 @@ export class PaymentService implements IAbstractPaymentService {
return paymentData;
} catch (error) {
console.error(`Payment could not be created for bookingId ${bookingId}`, error);
throw new Error("Payment could not be created");
log.error("Stripe: Payment could not be created", bookingId, JSON.stringify(error));
throw new Error("payment_not_created_error");
}
}
@ -199,8 +204,12 @@ export class PaymentService implements IAbstractPaymentService {
return paymentData;
} catch (error) {
console.error(`Payment method could not be collected for bookingId ${bookingId}`, error);
throw new Error("Payment could not be created");
log.error(
"Stripe: Payment method could not be collected for bookingId",
bookingId,
JSON.stringify(error)
);
throw new Error("Stripe: Payment method could not be collected");
}
}
@ -277,8 +286,8 @@ export class PaymentService implements IAbstractPaymentService {
return paymentData;
} catch (error) {
console.error(`Could not charge card for payment ${payment.id}`, error);
throw new Error("Payment could not be created");
log.error("Stripe: Could not charge card for payment", _bookingId, JSON.stringify(error));
throw new Error(ErrorCode.ChargeCardFailure);
}
}
@ -369,7 +378,7 @@ export class PaymentService implements IAbstractPaymentService {
await this.stripe.paymentIntents.cancel(payment.externalId, { stripeAccount });
return true;
} catch (e) {
console.error(e);
log.error("Stripe: Unable to delete Payment in stripe of paymentId", paymentId, JSON.stringify(e));
return false;
}
}

View File

@ -142,10 +142,19 @@ export function getAppFromLocationValue(type: string): AppMeta | undefined {
* @param concurrentMeetings - from app metadata
* @returns - true if app supports team install
*/
export function doesAppSupportTeamInstall(
appCategories: string[],
concurrentMeetings: boolean | undefined = undefined
) {
export function doesAppSupportTeamInstall({
appCategories,
concurrentMeetings = undefined,
isPaid,
}: {
appCategories: string[];
concurrentMeetings: boolean | undefined;
isPaid: boolean;
}) {
// Paid apps can't be installed on team level - That isn't supported
if (isPaid) {
return false;
}
return !appCategories.some(
(category) =>
category === "calendar" ||

View File

@ -0,0 +1,2 @@
export { CalProvider } from "./index";
export * from "../types";

View File

@ -0,0 +1,15 @@
import type { ReactNode } from "react";
import { createContext, useContext } from "react";
type CalProviderProps = {
apiKey: string;
children: ReactNode;
};
const ApiKeyContext = createContext("");
export const useApiKey = () => useContext(ApiKeyContext);
export function CalProvider({ apiKey, children }: CalProviderProps) {
return <ApiKeyContext.Provider value={apiKey}>{children}</ApiKeyContext.Provider>;
}

View File

@ -1 +1,2 @@
export { Booker } from "./booker/Booker";
export { CalProvider } from "./cal-provider/index";

View File

@ -206,7 +206,7 @@ export const getBusyCalendarTimes = async (
selectedCalendars: SelectedCalendar[]
) => {
let results: EventBusyDate[][] = [];
const months = getMonths(dateFrom, dateTo);
// const months = getMonths(dateFrom, dateTo);
try {
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
@ -348,6 +348,19 @@ export const updateEvent = async (
})
: undefined;
if (!updatedResult) {
logger.error(
"updateEvent failed",
safeStringify({
success,
bookingRefUid,
credential: getPiiFreeCredential(credential),
originalEvent: getPiiFreeCalendarEvent(calEvent),
calError,
})
);
}
if (Array.isArray(updatedResult)) {
calWarnings = updatedResult.flatMap((res) => res.additionalInfo?.calWarnings ?? []);
} else {
@ -388,10 +401,11 @@ export const deleteEvent = async ({
if (calendar) {
return calendar.deleteEvent(bookingRefUid, event, externalCalendarId);
} else {
log.warn(
log.error(
"Could not do deleteEvent - No calendar adapter found",
safeStringify({
credential: getPiiFreeCredential(credential),
event,
})
);
}

View File

@ -5,12 +5,15 @@ import { v5 as uuidv5 } from "uuid";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { CalendarEventClass } from "./class";
const log = logger.getSubLogger({ prefix: ["builders", "CalendarEvent", "builder"] });
const translator = short();
const userSelect = Prisma.validator<Prisma.UserArgs>()({
select: {
@ -149,6 +152,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
} catch (error) {
throw new Error("Error while getting eventType");
}
log.debug("getEventFromEventId.resultEventType", safeStringify(resultEventType));
return resultEventType;
}
@ -244,7 +248,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
let slug = "";
if (isTeam && eventType?.team?.slug) {
slug = `/team/${eventType.team?.slug}`;
slug = `team/${eventType.team?.slug}/${eventType.slug}`;
} else if (isDynamic) {
const dynamicSlug = isDynamic ? `${booking.dynamicGroupSlugRef}/${booking.dynamicEventSlugRef}` : "";
slug = dynamicSlug;

View File

@ -1,7 +1,11 @@
import { Booking } from "@prisma/client";
import type { Booking } from "@prisma/client";
import { CalendarEventBuilder } from "./builder";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import type { CalendarEventBuilder } from "./builder";
const log = logger.getSubLogger({ prefix: ["builders", "CalendarEvent", "director"] });
export class CalendarEventDirector {
private builder!: CalendarEventBuilder;
private existingBooking!: Partial<Booking>;
@ -44,6 +48,10 @@ export class CalendarEventDirector {
this.builder.setDescription(this.builder.eventType.description);
this.builder.setNotes(this.existingBooking.description);
this.builder.buildRescheduleLink(this.existingBooking, this.builder.eventType);
log.debug(
"buildForRescheduleEmail",
safeStringify({ existingBooking: this.existingBooking, builder: this.builder })
);
} else {
throw new Error("buildForRescheduleEmail.missing.params.required");
}

View File

@ -164,7 +164,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
const user = initialData?.user || (await getUser(where));
if (!user) throw new HttpError({ statusCode: 404, message: "No user found" });
if (!user) throw new HttpError({ statusCode: 404, message: "No user found in getUserAvailability" });
log.debug(
"getUserAvailability for user",
safeStringify({ user: { id: user.id }, slot: { dateFrom, dateTo } })

View File

@ -28,7 +28,6 @@ import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc";
import { Alert, Button, EmptyScreen, Form, showToast } from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
@ -153,6 +152,7 @@ export const BookEventFormChild = ({
const verifiedEmail = useBookerStore((state) => state.verifiedEmail);
const setVerifiedEmail = useBookerStore((state) => state.setVerifiedEmail);
const bookingSuccessRedirect = useBookingSuccessRedirect();
const [responseVercelIdHeader, setResponseVercelIdHeader] = useState<string | null>(null);
const router = useRouter();
const { t, i18n } = useLocale();
@ -220,7 +220,12 @@ export const BookEventFormChild = ({
booking: responseData,
});
},
onError: () => {
onError: (err, _, ctx) => {
// TODO:
// const vercelId = ctx?.meta?.headers?.get("x-vercel-id");
// if (vercelId) {
// setResponseVercelIdHeader(vercelId);
// }
errorRef && errorRef.current?.scrollIntoView({ behavior: "smooth" });
},
});
@ -390,7 +395,8 @@ export const BookEventFormChild = ({
bookingForm.formState.errors["globalError"],
createBookingMutation,
createRecurringBookingMutation,
t
t,
responseVercelIdHeader
)}
/>
</div>
@ -438,16 +444,19 @@ const getError = (
bookingMutation: UseMutationResult<any, any, any, any>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
recurringBookingMutation: UseMutationResult<any, any, any, any>,
t: TFunction
t: TFunction,
responseVercelIdHeader: string | null
) => {
if (globalError) return globalError.message;
const error = bookingMutation.error || recurringBookingMutation.error;
return error instanceof HttpError || error instanceof Error ? (
<>{t("can_you_try_again")}</>
return error.message ? (
<>
{responseVercelIdHeader ?? ""} {t(error.message)}
</>
) : (
"Unknown error"
<>{t("can_you_try_again")}</>
);
};

View File

@ -1,7 +1,7 @@
import { useFormContext } from "react-hook-form";
import type { LocationObject } from "@calcom/app-store/locations";
import { getOrganizerInputLocationsType } from "@calcom/app-store/locations";
import { getOrganizerInputLocationTypes } from "@calcom/app-store/locations";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField";
@ -107,14 +107,14 @@ export const BookingFields = ({
}
if (field?.options) {
const organzierInputTypes = getOrganizerInputLocationsType();
const organzierInputObj: Record<string, number> = {};
const organizerInputTypes = getOrganizerInputLocationTypes();
const organizerInputObj: Record<string, number> = {};
field.options.forEach((f) => {
if (f.value in organzierInputObj) {
organzierInputObj[f.value]++;
if (f.value in organizerInputObj) {
organizerInputObj[f.value]++;
} else {
organzierInputObj[f.value] = 1;
organizerInputObj[f.value] = 1;
}
});
@ -122,7 +122,7 @@ export const BookingFields = ({
return {
...field,
value:
organzierInputTypes.includes(field.value) && organzierInputObj[field.value] > 1
organizerInputTypes.includes(field.value) && organizerInputObj[field.value] > 1
? field.label
: field.value,
};

View File

@ -12,9 +12,14 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
} from "../../ee/workflows/lib/allowDisablingStandardEmails";
const log = logger.getSubLogger({ prefix: ["[handleConfirmation] book:user"] });
export async function handleConfirmation(args: {
@ -31,10 +36,17 @@ export async function handleConfirmation(args: {
length: number;
price: number;
requiresConfirmation: boolean;
metadata?: Prisma.JsonValue;
title: string;
teamId?: number | null;
parentId?: number | null;
workflows?: {
workflow: Workflow & {
steps: WorkflowStep[];
};
}[];
} | null;
metadata?: Prisma.JsonValue;
eventTypeId: number | null;
smsReminderNumber: string | null;
userId: number | null;
@ -45,6 +57,7 @@ export async function handleConfirmation(args: {
const eventManager = new EventManager(user);
const scheduleResult = await eventManager.create(evt);
const results = scheduleResult.results;
const metadata: AdditionalInformation = {};
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
@ -54,8 +67,6 @@ export async function handleConfirmation(args: {
log.error(`Booking ${user.username} failed`, JSON.stringify({ error, results }));
} else {
const metadata: AdditionalInformation = {};
if (results.length) {
// TODO: Handle created event metadata more elegantly
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
@ -63,7 +74,34 @@ export async function handleConfirmation(args: {
metadata.entryPoints = results[0].createdEvent?.entryPoints;
}
try {
await sendScheduledEmails({ ...evt, additionalInformation: metadata });
const eventType = booking.eventType;
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
let isHostConfirmationEmailsDisabled = false;
let isAttendeeConfirmationEmailDisabled = false;
const workflows = eventType?.workflows?.map((workflow) => workflow.workflow);
if (workflows) {
isHostConfirmationEmailsDisabled =
eventTypeMetadata?.disableStandardEmails?.confirmation?.host || false;
isAttendeeConfirmationEmailDisabled =
eventTypeMetadata?.disableStandardEmails?.confirmation?.attendee || false;
if (isHostConfirmationEmailsDisabled) {
isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails(workflows);
}
if (isAttendeeConfirmationEmailDisabled) {
isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails(workflows);
}
}
await sendScheduledEmails(
{ ...evt, additionalInformation: metadata },
undefined,
isHostConfirmationEmailsDisabled,
isAttendeeConfirmationEmailDisabled
);
} catch (error) {
log.error(error);
}
@ -97,6 +135,8 @@ export async function handleConfirmation(args: {
} | null;
}[] = [];
const videoCallUrl = metadata.hangoutLink ? metadata.hangoutLink : evt.videoCallData?.url || "";
if (recurringEventId) {
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
// bookings as confirmed. Prisma updateMany does not support relations, so doing this in two steps for now.
@ -118,6 +158,10 @@ export async function handleConfirmation(args: {
create: scheduleResult.referencesToCreate,
},
paid,
metadata: {
...(typeof recurringBooking.metadata === "object" ? recurringBooking.metadata : {}),
videoCallUrl,
},
},
select: {
eventType: {
@ -169,6 +213,7 @@ export async function handleConfirmation(args: {
references: {
create: scheduleResult.referencesToCreate,
},
metadata: { ...(typeof booking.metadata === "object" ? booking.metadata : {}), videoCallUrl },
},
select: {
eventType: {
@ -218,8 +263,6 @@ export async function handleConfirmation(args: {
const eventTypeSlug = updatedBookings[index].eventType?.slug || "";
const isFirstBooking = index === 0;
const videoCallUrl =
bookingMetadataSchema.parse(updatedBookings[index].metadata || {})?.videoCallUrl || "";
await scheduleWorkflowReminders({
workflows: updatedBookings[index]?.eventType?.workflows || [],

View File

@ -6,6 +6,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { cloneDeep } from "lodash";
import type { NextApiRequest } from "next";
import short, { uuid } from "short-uuid";
import type { Logger } from "tslog";
import { v5 as uuidv5 } from "uuid";
import z from "zod";
@ -52,6 +53,7 @@ import { cancelScheduledJobs, scheduleTrigger } from "@calcom/features/webhooks/
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
@ -362,7 +364,8 @@ async function ensureAvailableUsers(
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
users: IsFixedAwareUser[];
},
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType }
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
loggerWithEventDetails: Logger<unknown>
) {
const availableUsers: IsFixedAwareUser[] = [];
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
@ -401,6 +404,25 @@ async function ensureAvailableUsers(
}
let foundConflict = false;
let dateRangeForBooking = false;
//check if event time is within the date range
for (const dateRange of dateRanges) {
if (
(dayjs.utc(input.dateFrom).isAfter(dateRange.start) ||
dayjs.utc(input.dateFrom).isSame(dateRange.start)) &&
(dayjs.utc(input.dateTo).isBefore(dateRange.end) || dayjs.utc(input.dateTo).isSame(dateRange.end))
) {
dateRangeForBooking = true;
break;
}
}
if (!dateRangeForBooking) {
continue;
}
try {
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
} catch {
@ -414,7 +436,8 @@ async function ensureAvailableUsers(
}
}
if (!availableUsers.length) {
throw new Error("No available users found.");
loggerWithEventDetails.error(`No available users found.`);
throw new Error(ErrorCode.NoAvailableUsersFound);
}
return availableUsers;
}
@ -537,7 +560,7 @@ async function getBookingData({
return true;
};
if (!reqBodyWithEnd(reqBody)) {
throw new Error("Internal Error.");
throw new Error(ErrorCode.RequestBodyWithouEnd);
}
// reqBody.end is no longer an optional property.
if ("customInputs" in reqBody) {
@ -672,10 +695,11 @@ async function handler(
const fullName = getFullName(bookerName);
// Why are we only using "en" locale
const tGuests = await getTranslation("en", "common");
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" });
const isTeamEventType =
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
@ -916,7 +940,8 @@ async function handler(
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
}
},
loggerWithEventDetails
);
const luckyUsers: typeof users = [];
@ -946,7 +971,7 @@ async function handler(
if (
availableUsers.filter((user) => user.isFixed).length !== users.filter((user) => user.isFixed).length
) {
throw new Error("Some of the hosts are unavailable for booking.");
throw new Error(ErrorCode.HostsUnavailableForBooking);
}
// Pushing fixed user before the luckyUser guarantees the (first) fixed user as the organizer.
users = [...availableUsers.filter((user) => user.isFixed), ...luckyUsers];
@ -1264,7 +1289,7 @@ async function handler(
booking.attendees.find((attendee) => attendee.email === invitee[0].email) &&
dayjs.utc(booking.startTime).format() === evt.startTime
) {
throw new HttpError({ statusCode: 409, message: "Already signed up for this booking." });
throw new HttpError({ statusCode: 409, message: ErrorCode.AlreadySignedUpForBooking });
}
// There are two paths here, reschedule a booking with seats and booking seats without reschedule
@ -2516,7 +2541,7 @@ async function handler(
...evt,
...{ metadata: metadataFromEvent, eventType: { slug: eventType.slug } },
},
isNotConfirmed: evt.requiresConfirmation || false,
isNotConfirmed: !isConfirmedByDefault,
isRescheduleEvent: !!rescheduleUid,
isFirstRecurringEvent: true,
hideBranding: !!eventType.owner?.hideBranding,
@ -2675,7 +2700,7 @@ const findBookingQuery = async (bookingId: number) => {
// This should never happen but it's just typescript safe
if (!foundBooking) {
throw new Error("Internal Error.");
throw new Error("Internal Error. Couldn't find booking");
}
// Don't leak any sensitive data

View File

@ -1,8 +1,7 @@
import { describe } from "vitest";
import { test } from "@calcom/web/test/fixtures/fixtures";
import { setupAndTeardown } from "./lib/setupAndTeardown";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
describe("handleNewBooking", () => {
setupAndTeardown();

View File

@ -13,6 +13,7 @@ import { describe, expect } from "vitest";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { resetTestEmails } from "@calcom/lib/testEmails";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
@ -37,6 +38,7 @@ import {
mockVideoAppToCrashOnCreateMeeting,
BookingLocations,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
import {
expectWorkflowToBeTriggered,
expectSuccessfulBookingCreationEmails,
@ -49,11 +51,9 @@ import {
expectBrokenIntegrationEmails,
expectSuccessfulCalendarEventCreationInCalendar,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
import { setupAndTeardown } from "./lib/setupAndTeardown";
import { testWithAndWithoutOrg } from "./lib/test";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
import { testWithAndWithoutOrg } from "@calcom/web/test/utils/bookingScenario/test";
export type CustomNextApiRequest = NextApiRequest & Request;
@ -940,7 +940,7 @@ describe("handleNewBooking", () => {
});
await handleNewBooking(req);
expectBrokenIntegrationEmails({ booker, organizer, emails });
expectBrokenIntegrationEmails({ organizer, emails });
expectBookingCreatedWebhookToHaveBeenFired({
booker,
@ -1025,7 +1025,7 @@ describe("handleNewBooking", () => {
});
await expect(async () => await handleNewBooking(req)).rejects.toThrowError(
"No available users found"
ErrorCode.NoAvailableUsersFound
);
},
timeout
@ -1112,7 +1112,7 @@ describe("handleNewBooking", () => {
});
await expect(async () => await handleNewBooking(req)).rejects.toThrowError(
"No available users found"
ErrorCode.NoAvailableUsersFound
);
},
timeout
@ -1240,7 +1240,7 @@ describe("handleNewBooking", () => {
* NOTE: We might want to think about making the bookings get ACCEPTED automatically if the booker is the organizer of the event-type. This is a design decision it seems for now.
*/
test(
`should make a fresh booking in PENDING state even when the booker is the organizer of the event-type
`should make a fresh booking in PENDING state even when the booker is the organizer of the event-type
1. Should create a booking in the database with status PENDING
2. Should send emails to the booker as well as organizer for booking request and awaiting approval
3. Should trigger BOOKING_REQUESTED webhook

View File

@ -1,8 +1,7 @@
import { describe } from "vitest";
import { test } from "@calcom/web/test/fixtures/fixtures";
import { setupAndTeardown } from "./lib/setupAndTeardown";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
describe("handleNewBooking", () => {
setupAndTeardown();

View File

@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid";
import { describe, expect } from "vitest";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import logger from "@calcom/lib/logger";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
@ -16,6 +17,7 @@ import {
mockCalendarToHaveNoBusySlots,
getDate,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
import {
expectWorkflowToBeTriggered,
expectSuccessfulBookingCreationEmails,
@ -23,10 +25,8 @@ import {
expectBookingCreatedWebhookToHaveBeenFired,
expectSuccessfulCalendarEventCreationInCalendar,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
import { setupAndTeardown } from "./lib/setupAndTeardown";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
const DAY_IN_MS = 1000 * 60 * 60 * 24;
@ -369,7 +369,7 @@ describe("handleNewBooking", () => {
}),
});
expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow("No available users found");
expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow(ErrorCode.NoAvailableUsersFound);
// Actually the first booking goes through in this case but the status is still a failure. We should do a dry run to check if booking is possible for the 2 slots and if yes, then only go for the actual booking otherwise fail the recurring bookign
},
timeout

View File

@ -24,6 +24,7 @@ import {
getMockFailingAppStatus,
getMockPassingAppStatus,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
import {
expectWorkflowToBeTriggered,
expectBookingToBeInDatabase,
@ -37,10 +38,8 @@ import {
expectSuccessfulCalendarEventDeletionInCalendar,
expectSuccessfulVideoMeetingDeletionInCalendar,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
import { setupAndTeardown } from "./lib/setupAndTeardown";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
// Local test runs sometime gets too slow
const timeout = process.env.CI ? 5000 : 20000;

View File

@ -4,6 +4,7 @@ import { describe, expect } from "vitest";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
@ -22,6 +23,7 @@ import {
BookingLocations,
getZoomAppCredential,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
import {
expectWorkflowToBeTriggered,
expectSuccessfulBookingCreationEmails,
@ -30,10 +32,8 @@ import {
expectSuccessfulCalendarEventCreationInCalendar,
expectSuccessfulVideoMeetingCreation,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { createMockNextJsRequest } from "../lib/createMockNextJsRequest";
import { getMockRequestDataForBooking } from "../lib/getMockRequestDataForBooking";
import { setupAndTeardown } from "../lib/setupAndTeardown";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
export type CustomNextApiRequest = NextApiRequest & Request;
@ -354,7 +354,7 @@ describe("handleNewBooking", () => {
await expect(async () => {
await handleNewBooking(req);
}).rejects.toThrowError("Some of the hosts are unavailable for booking");
}).rejects.toThrowError(ErrorCode.HostsUnavailableForBooking);
},
timeout
);
@ -667,7 +667,7 @@ describe("handleNewBooking", () => {
await expect(async () => {
await handleNewBooking(req);
}).rejects.toThrowError("No available users found.");
}).rejects.toThrowError(ErrorCode.NoAvailableUsersFound);
},
timeout
);

View File

@ -6,12 +6,19 @@ import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import logger from "@calcom/lib/logger";
import { defaultHandler } from "@calcom/lib/server";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders";
import {
getAllRemindersToCancel,
getAllRemindersToDelete,
getAllUnscheduledReminders,
} from "../lib/getWorkflowReminders";
import { getiCalEventAsString } from "../lib/getiCalEventAsString";
import type { VariablesType } from "../lib/reminders/templates/customTemplate";
import customTemplate from "../lib/reminders/templates/customTemplate";
@ -38,45 +45,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false;
// delete batch_ids with already past scheduled date from scheduled_sends
const pageSize = 90;
let pageNumber = 0;
const remindersToDelete: { referenceId: string | null }[] = await getAllRemindersToDelete();
const deletePromises: Promise<any>[] = [];
while (true) {
const remindersToDelete = await prisma.workflowReminder.findMany({
where: {
method: WorkflowMethods.EMAIL,
cancelled: true,
scheduledDate: {
lte: dayjs().toISOString(),
},
},
skip: pageNumber * pageSize,
take: pageSize,
select: {
referenceId: true,
},
for (const reminder of remindersToDelete) {
const deletePromise = client.request({
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
method: "DELETE",
});
if (remindersToDelete.length === 0) {
break;
}
for (const reminder of remindersToDelete) {
const deletePromise = client.request({
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
method: "DELETE",
});
deletePromises.push(deletePromise);
}
pageNumber++;
deletePromises.push(deletePromise);
}
Promise.allSettled(deletePromises).then((results) => {
results.forEach((result) => {
if (result.status === "rejected") {
console.log(`Error deleting batch id from scheduled_sends: ${result.reason}`);
logger.error(`Error deleting batch id from scheduled_sends: ${result.reason}`);
}
});
});
@ -92,312 +76,221 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
pageNumber = 0;
const remindersToCancel: { referenceId: string | null; id: number }[] = await getAllRemindersToCancel();
const allPromisesCancelReminders: Promise<any>[] = [];
const cancelUpdatePromises: Promise<any>[] = [];
while (true) {
const remindersToCancel = await prisma.workflowReminder.findMany({
where: {
cancelled: true,
scheduled: true, //if it is false then they are already cancelled
scheduledDate: {
lte: dayjs().add(1, "hour").toISOString(),
},
},
skip: pageNumber * pageSize,
take: pageSize,
select: {
referenceId: true,
id: true,
for (const reminder of remindersToCancel) {
const cancelPromise = client.request({
url: "/v3/user/scheduled_sends",
method: "POST",
body: {
batch_id: reminder.referenceId,
status: "cancel",
},
});
if (remindersToCancel.length === 0) {
break;
}
const updatePromise = prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: false, // to know which reminder already got cancelled (to avoid error from cancelling the same reminders again)
},
});
for (const reminder of remindersToCancel) {
const cancelPromise = client.request({
url: "/v3/user/scheduled_sends",
method: "POST",
body: {
batch_id: reminder.referenceId,
status: "cancel",
},
});
const updatePromise = prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: false, // to know which reminder already got cancelled (to avoid error from cancelling the same reminders again)
},
});
allPromisesCancelReminders.push(cancelPromise, updatePromise);
}
pageNumber++;
cancelUpdatePromises.push(cancelPromise, updatePromise);
}
Promise.allSettled(allPromisesCancelReminders).then((results) => {
Promise.allSettled(cancelUpdatePromises).then((results) => {
results.forEach((result) => {
if (result.status === "rejected") {
console.log(`Error cancelling scheduled_sends: ${result.reason}`);
logger.error(`Error cancelling scheduled_sends: ${result.reason}`);
}
});
});
// schedule all unscheduled reminders within the next 72 hours
pageNumber = 0;
const sendEmailPromises: Promise<any>[] = [];
while (true) {
const unscheduledReminders = await prisma.workflowReminder.findMany({
where: {
method: WorkflowMethods.EMAIL,
scheduled: false,
scheduledDate: {
lte: dayjs().add(72, "hour").toISOString(),
},
OR: [{ cancelled: false }, { cancelled: null }],
},
skip: pageNumber * pageSize,
take: pageSize,
select: {
id: true,
scheduledDate: true,
workflowStep: {
select: {
action: true,
sendTo: true,
reminderBody: true,
emailSubject: true,
template: true,
sender: true,
includeCalendarEvent: true,
},
},
booking: {
select: {
startTime: true,
endTime: true,
location: true,
description: true,
user: {
select: {
email: true,
name: true,
timeZone: true,
locale: true,
username: true,
timeFormat: true,
hideBranding: true,
},
},
metadata: true,
uid: true,
customInputs: true,
responses: true,
attendees: true,
eventType: {
select: {
bookingFields: true,
title: true,
slug: true,
recurringEvent: true,
},
},
},
},
},
});
const unscheduledReminders: PartialWorkflowReminder[] = await getAllUnscheduledReminders();
if (!unscheduledReminders.length && pageNumber === 0) {
res.status(200).json({ message: "No Emails to schedule" });
return;
if (!unscheduledReminders.length) {
res.status(200).json({ message: "No Emails to schedule" });
return;
}
for (const reminder of unscheduledReminders) {
if (!reminder.workflowStep || !reminder.booking) {
continue;
}
try {
let sendTo;
if (unscheduledReminders.length === 0) {
break;
}
for (const reminder of unscheduledReminders) {
if (!reminder.workflowStep || !reminder.booking) {
continue;
switch (reminder.workflowStep.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = reminder.booking.user?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = reminder.booking.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = reminder.workflowStep.sendTo;
}
try {
let sendTo;
switch (reminder.workflowStep.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = reminder.booking.user?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = reminder.booking.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = reminder.workflowStep.sendTo;
}
const name =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].name
: reminder.booking.user?.name;
const name =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].name
: reminder.booking.user?.name;
const attendeeName =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.user?.name
: reminder.booking.attendees[0].name;
const attendeeName =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.user?.name
: reminder.booking.attendees[0].name;
const timeZone =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].timeZone
: reminder.booking.user?.timeZone;
const timeZone =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking.attendees[0].timeZone
: reminder.booking.user?.timeZone;
const locale =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ||
reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE
? reminder.booking.attendees[0].locale
: reminder.booking.user?.locale;
const locale =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ||
reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE
? reminder.booking.attendees[0].locale
: reminder.booking.user?.locale;
let emailContent = {
emailSubject: reminder.workflowStep.emailSubject || "",
emailBody: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
};
let emailContent = {
emailSubject: reminder.workflowStep.emailSubject || "",
emailBody: `<body style="white-space: pre-wrap;">${
reminder.workflowStep.reminderBody || ""
}</body>`,
let emailBodyEmpty = false;
if (reminder.workflowStep.reminderBody) {
const { responses } = getCalEventResponses({
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
booking: reminder.booking,
});
const variables: VariablesType = {
eventName: reminder.booking.eventType?.title || "",
organizerName: reminder.booking.user?.name || "",
attendeeName: reminder.booking.attendees[0].name,
attendeeEmail: reminder.booking.attendees[0].email,
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
timeZone: timeZone,
location: reminder.booking.location || "",
additionalNotes: reminder.booking.description,
responses: responses,
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
};
const emailLocale = locale || "en";
const emailSubject = customTemplate(
reminder.workflowStep.emailSubject || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).text;
emailContent.emailSubject = emailSubject;
emailContent.emailBody = customTemplate(
reminder.workflowStep.reminderBody || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).html;
let emailBodyEmpty = false;
if (reminder.workflowStep.reminderBody) {
const { responses } = getCalEventResponses({
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
booking: reminder.booking,
});
const variables: VariablesType = {
eventName: reminder.booking.eventType?.title || "",
organizerName: reminder.booking.user?.name || "",
attendeeName: reminder.booking.attendees[0].name,
attendeeEmail: reminder.booking.attendees[0].email,
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
timeZone: timeZone,
location: reminder.booking.location || "",
additionalNotes: reminder.booking.description,
responses: responses,
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
};
const emailLocale = locale || "en";
const emailSubject = customTemplate(
reminder.workflowStep.emailSubject || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).text;
emailContent.emailSubject = emailSubject;
emailContent.emailBody = customTemplate(
emailBodyEmpty =
customTemplate(
reminder.workflowStep.reminderBody || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
!!reminder.booking.user?.hideBranding
).html;
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat)
).text.length === 0;
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
emailContent = emailReminderTemplate(
false,
reminder.workflowStep.action,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
reminder.booking.startTime.toISOString() || "",
reminder.booking.endTime.toISOString() || "",
reminder.booking.eventType?.title || "",
timeZone || "",
attendeeName || "",
name || "",
!!reminder.booking.user?.hideBranding
);
}
emailBodyEmpty =
customTemplate(
reminder.workflowStep.reminderBody || "",
variables,
emailLocale,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat)
).text.length === 0;
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
emailContent = emailReminderTemplate(
false,
reminder.workflowStep.action,
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
reminder.booking.startTime.toISOString() || "",
reminder.booking.endTime.toISOString() || "",
reminder.booking.eventType?.title || "",
timeZone || "",
attendeeName || "",
name || "",
!!reminder.booking.user?.hideBranding
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
const batchIdResponse = await client.request({
url: "/v3/mail/batch",
method: "POST",
});
const batchId = batchIdResponse[1].batch_id;
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
sendEmailPromises.push(
sgMail.send({
to: sendTo,
from: {
email: senderEmail,
name: reminder.workflowStep.sender || "Cal.com",
},
subject: emailContent.emailSubject,
html: emailContent.emailBody,
batchId: batchId,
sendAt: dayjs(reminder.scheduledDate).unix(),
replyTo: reminder.booking.user?.email || senderEmail,
mailSettings: {
sandboxMode: {
enable: sandboxMode,
},
},
attachments: reminder.workflowStep.includeCalendarEvent
? [
{
content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"),
filename: "event.ics",
type: "text/calendar; method=REQUEST",
disposition: "attachment",
contentId: uuidv4(),
},
]
: undefined,
})
);
}
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
const batchIdResponse = await client.request({
url: "/v3/mail/batch",
method: "POST",
});
const batchId = batchIdResponse[1].batch_id;
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
sendEmailPromises.push(
sgMail.send({
to: sendTo,
from: {
email: senderEmail,
name: reminder.workflowStep.sender || "Cal.com",
},
subject: emailContent.emailSubject,
html: emailContent.emailBody,
batchId: batchId,
sendAt: dayjs(reminder.scheduledDate).unix(),
replyTo: reminder.booking.user?.email || senderEmail,
mailSettings: {
sandboxMode: {
enable: sandboxMode,
},
},
attachments: reminder.workflowStep.includeCalendarEvent
? [
{
content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"),
filename: "event.ics",
type: "text/calendar; method=REQUEST",
disposition: "attachment",
contentId: uuidv4(),
},
]
: undefined,
})
);
}
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: true,
referenceId: batchId,
},
});
}
} catch (error) {
console.log(`Error scheduling Email with error ${error}`);
await prisma.workflowReminder.update({
where: {
id: reminder.id,
},
data: {
scheduled: true,
referenceId: batchId,
},
});
}
} catch (error) {
logger.error(`Error scheduling Email with error ${error}`);
}
pageNumber++;
}
Promise.allSettled(sendEmailPromises).then((results) => {
results.forEach((result) => {
if (result.status === "rejected") {
console.log("Email sending failed", result.reason);
logger.error("Email sending failed", result.reason);
}
});
});
res.status(200).json({ message: "Emails scheduled" });
res.status(200).json({ message: `${unscheduledReminders.length} Emails scheduled` });
}
export default defaultHandler({

View File

@ -0,0 +1,164 @@
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import type { EventType, Prisma, User, WorkflowReminder, WorkflowStep } from "@calcom/prisma/client";
import { WorkflowMethods } from "@calcom/prisma/enums";
type PartialWorkflowStep = Partial<WorkflowStep> | null;
type Booking = Prisma.BookingGetPayload<{
include: {
attendees: true;
};
}>;
type PartialBooking =
| (Pick<
Booking,
| "startTime"
| "endTime"
| "location"
| "description"
| "metadata"
| "customInputs"
| "responses"
| "uid"
| "attendees"
> & { eventType: Partial<EventType> | null } & { user: Partial<User> | null })
| null;
export type PartialWorkflowReminder = Pick<WorkflowReminder, "id" | "scheduledDate"> & {
booking: PartialBooking | null;
} & { workflowStep: PartialWorkflowStep };
async function getWorkflowReminders<T extends Prisma.WorkflowReminderSelect>(
filter: Prisma.WorkflowReminderWhereInput,
select: T
): Promise<Array<Prisma.WorkflowReminderGetPayload<{ select: T }>>> {
const pageSize = 90;
let pageNumber = 0;
const filteredWorkflowReminders: Array<Prisma.WorkflowReminderGetPayload<{ select: T }>> = [];
while (true) {
const newFilteredWorkflowReminders = await prisma.workflowReminder.findMany({
where: filter,
select: select,
skip: pageNumber * pageSize,
take: pageSize,
});
if (newFilteredWorkflowReminders.length === 0) {
break;
}
filteredWorkflowReminders.push(
...(newFilteredWorkflowReminders as Array<Prisma.WorkflowReminderGetPayload<{ select: T }>>)
);
pageNumber++;
}
return filteredWorkflowReminders;
}
type RemindersToDeleteType = { referenceId: string | null };
export async function getAllRemindersToDelete(): Promise<RemindersToDeleteType[]> {
const whereFilter: Prisma.WorkflowReminderWhereInput = {
method: WorkflowMethods.EMAIL,
cancelled: true,
scheduledDate: {
lte: dayjs().toISOString(),
},
};
const select: Prisma.WorkflowReminderSelect = {
referenceId: true,
};
const remindersToDelete = await getWorkflowReminders(whereFilter, select);
return remindersToDelete;
}
type RemindersToCancelType = { referenceId: string | null; id: number };
export async function getAllRemindersToCancel(): Promise<RemindersToCancelType[]> {
const whereFilter: Prisma.WorkflowReminderWhereInput = {
cancelled: true,
scheduled: true, //if it is false then they are already cancelled
scheduledDate: {
lte: dayjs().add(1, "hour").toISOString(),
},
};
const select: Prisma.WorkflowReminderSelect = {
referenceId: true,
id: true,
};
const remindersToCancel = await getWorkflowReminders(whereFilter, select);
return remindersToCancel;
}
export async function getAllUnscheduledReminders(): Promise<PartialWorkflowReminder[]> {
const whereFilter: Prisma.WorkflowReminderWhereInput = {
method: WorkflowMethods.EMAIL,
scheduled: false,
scheduledDate: {
lte: dayjs().add(72, "hour").toISOString(),
},
OR: [{ cancelled: false }, { cancelled: null }],
};
const select: Prisma.WorkflowReminderSelect = {
id: true,
scheduledDate: true,
workflowStep: {
select: {
action: true,
sendTo: true,
reminderBody: true,
emailSubject: true,
template: true,
sender: true,
includeCalendarEvent: true,
},
},
booking: {
select: {
startTime: true,
endTime: true,
location: true,
description: true,
user: {
select: {
email: true,
name: true,
timeZone: true,
locale: true,
username: true,
timeFormat: true,
hideBranding: true,
},
},
metadata: true,
uid: true,
customInputs: true,
responses: true,
attendees: true,
eventType: {
select: {
bookingFields: true,
title: true,
slug: true,
recurringEvent: true,
},
},
},
},
};
const unscheduledReminders = (await getWorkflowReminders(whereFilter, select)) as PartialWorkflowReminder[];
return unscheduledReminders;
}

View File

@ -1,6 +1,6 @@
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { prisma } from "@calcom/prisma";
import { readonlyPrisma as prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import type { RawDataInput } from "./raw-data.schema";

View File

@ -35,7 +35,7 @@ const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, rawIn
membershipWhereConditional["teamId"] = parse.data.teamId;
}
const membership = await ctx.prisma.membership.findFirst({
const membership = await ctx.insightsDb.membership.findFirst({
where: membershipWhereConditional,
});
@ -43,7 +43,7 @@ const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, rawIn
// So that would mean ctx.user.organization is present
if ((parse.data.isAll && ctx.user.organizationId) || (!membership && ctx.user.organizationId)) {
//Look for membership type in organizationId
const membershipOrg = await ctx.prisma.membership.findFirst({
const membershipOrg = await ctx.insightsDb.membership.findFirst({
where: {
userId: ctx.user.id,
teamId: ctx.user.organizationId,
@ -152,7 +152,7 @@ export const insightsRouter = router({
}
if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) {
const teamsFromOrg = await ctx.prisma.team.findMany({
const teamsFromOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: ctx.user.organizationId,
},
@ -168,7 +168,7 @@ export const insightsRouter = router({
in: [ctx.user.organizationId, ...teamsFromOrg.map((t) => t.id)],
},
};
const usersFromOrg = await ctx.prisma.membership.findMany({
const usersFromOrg = await ctx.insightsDb.membership.findMany({
where: {
team: teamConditional,
accepted: true,
@ -197,7 +197,7 @@ export const insightsRouter = router({
}
if (teamId && !isAll) {
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId: teamId,
accepted: true,
@ -348,7 +348,7 @@ export const insightsRouter = router({
let whereConditional: Prisma.BookingTimeStatusWhereInput = {};
if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) {
const teamsFromOrg = await ctx.prisma.team.findMany({
const teamsFromOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: user.organizationId,
},
@ -357,7 +357,7 @@ export const insightsRouter = router({
},
});
const usersFromOrg = await ctx.prisma.membership.findMany({
const usersFromOrg = await ctx.insightsDb.membership.findMany({
where: {
teamId: {
in: [ctx.user.organizationId, ...teamsFromOrg.map((t) => t.id)],
@ -388,7 +388,7 @@ export const insightsRouter = router({
}
if (teamId && !isAll) {
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId,
accepted: true,
@ -537,7 +537,7 @@ export const insightsRouter = router({
};
if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) {
const teamsFromOrg = await ctx.prisma.team.findMany({
const teamsFromOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: user.organizationId,
},
@ -546,7 +546,7 @@ export const insightsRouter = router({
},
});
const usersFromOrg = await ctx.prisma.membership.findMany({
const usersFromOrg = await ctx.insightsDb.membership.findMany({
where: {
teamId: {
in: [ctx.user.organizationId, ...teamsFromOrg.map((t) => t.id)],
@ -578,7 +578,7 @@ export const insightsRouter = router({
}
if (teamId && !isAll) {
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId,
accepted: true,
@ -615,7 +615,7 @@ export const insightsRouter = router({
bookingWhere.userId = memberUserId;
}
const bookingsFromSelected = await ctx.prisma.bookingTimeStatus.groupBy({
const bookingsFromSelected = await ctx.insightsDb.bookingTimeStatus.groupBy({
by: ["eventTypeId"],
where: bookingWhere,
_count: {
@ -639,7 +639,7 @@ export const insightsRouter = router({
},
};
const eventTypesFrom = await ctx.prisma.eventType.findMany({
const eventTypesFrom = await ctx.insightsDb.eventType.findMany({
select: {
id: true,
title: true,
@ -752,7 +752,7 @@ export const insightsRouter = router({
}
if (isAll && ctx.user.isOwnerAdminOfParentTeam && ctx.user.organizationId) {
const teamsFromOrg = await ctx.prisma.team.findMany({
const teamsFromOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: ctx.user?.organizationId,
},
@ -777,7 +777,7 @@ export const insightsRouter = router({
}
if (teamId && !isAll) {
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId,
accepted: true,
@ -832,7 +832,7 @@ export const insightsRouter = router({
const startDate = dayjs(date).startOf(startOfEndOf);
const endDate = dayjs(date).endOf(startOfEndOf);
const bookingsInTimeRange = await ctx.prisma.bookingTimeStatus.findMany({
const bookingsInTimeRange = await ctx.insightsDb.bookingTimeStatus.findMany({
select: {
eventLength: true,
},
@ -896,7 +896,7 @@ export const insightsRouter = router({
if (isAll && user.isOwnerAdminOfParentTeam && user.organizationId) {
delete bookingWhere.teamId;
const teamsFromOrg = await ctx.prisma.team.findMany({
const teamsFromOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: user?.organizationId,
},
@ -904,7 +904,7 @@ export const insightsRouter = router({
id: true,
},
});
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId: {
in: [user?.organizationId, ...teamsFromOrg.map((t) => t.id)],
@ -931,7 +931,7 @@ export const insightsRouter = router({
}
if (teamId && !isAll) {
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId,
accepted: true,
@ -956,7 +956,7 @@ export const insightsRouter = router({
];
}
const bookingsFromTeam = await ctx.prisma.bookingTimeStatus.groupBy({
const bookingsFromTeam = await ctx.insightsDb.bookingTimeStatus.groupBy({
by: ["userId"],
where: bookingWhere,
_count: {
@ -977,7 +977,7 @@ export const insightsRouter = router({
return [];
}
const usersFromTeam = await ctx.prisma.user.findMany({
const usersFromTeam = await ctx.insightsDb.user.findMany({
where: {
id: {
in: userIds as number[],
@ -1030,7 +1030,7 @@ export const insightsRouter = router({
if (isAll && user.isOwnerAdminOfParentTeam) {
delete bookingWhere.teamId;
const teamsFromOrg = await ctx.prisma.team.findMany({
const teamsFromOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: user?.organizationId,
},
@ -1038,7 +1038,7 @@ export const insightsRouter = router({
id: true,
},
});
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId: {
in: teamsFromOrg.map((t) => t.id),
@ -1066,7 +1066,7 @@ export const insightsRouter = router({
}
if (teamId && !isAll) {
const usersFromTeam = await ctx.prisma.membership.findMany({
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId,
accepted: true,
@ -1089,7 +1089,7 @@ export const insightsRouter = router({
];
}
const bookingsFromTeam = await ctx.prisma.bookingTimeStatus.groupBy({
const bookingsFromTeam = await ctx.insightsDb.bookingTimeStatus.groupBy({
by: ["userId"],
where: bookingWhere,
_count: {
@ -1109,7 +1109,7 @@ export const insightsRouter = router({
if (userIds.length === 0) {
return [];
}
const usersFromTeam = await ctx.prisma.user.findMany({
const usersFromTeam = await ctx.insightsDb.user.findMany({
where: {
id: {
in: userIds as number[],
@ -1138,7 +1138,7 @@ export const insightsRouter = router({
const user = ctx.user;
// Fetch user data
const userData = await ctx.prisma.user.findUnique({
const userData = await ctx.insightsDb.user.findUnique({
where: {
id: user.id,
},
@ -1167,7 +1167,7 @@ export const insightsRouter = router({
// Validate if user belongs to org as admin/owner
if (user.organizationId) {
const teamsFromOrg = await ctx.prisma.team.findMany({
const teamsFromOrg = await ctx.insightsDb.team.findMany({
where: {
parentId: user.organizationId,
},
@ -1178,7 +1178,7 @@ export const insightsRouter = router({
logo: true,
},
});
const orgTeam = await ctx.prisma.team.findUnique({
const orgTeam = await ctx.insightsDb.team.findUnique({
where: {
id: user.organizationId,
},
@ -1214,7 +1214,7 @@ export const insightsRouter = router({
}
// Look if user it's admin/owner in multiple teams
const belongsToTeams = await ctx.prisma.membership.findMany({
const belongsToTeams = await ctx.insightsDb.membership.findMany({
where: membershipConditional,
include: {
team: {
@ -1255,7 +1255,7 @@ export const insightsRouter = router({
}
if (isAll && user.organizationId && user.isOwnerAdminOfParentTeam) {
const usersInTeam = await ctx.prisma.membership.findMany({
const usersInTeam = await ctx.insightsDb.membership.findMany({
where: {
team: {
parentId: user.organizationId,
@ -1271,7 +1271,7 @@ export const insightsRouter = router({
return usersInTeam.map((membership) => membership.user);
}
const membership = await ctx.prisma.membership.findFirst({
const membership = await ctx.insightsDb.membership.findFirst({
where: {
userId: user.id,
teamId,
@ -1292,7 +1292,7 @@ export const insightsRouter = router({
return [membership.user];
}
const usersInTeam = await ctx.prisma.membership.findMany({
const usersInTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId,
accepted: true,

View File

@ -95,12 +95,13 @@ export const tips = [
},
{
id: 12,
thumbnailUrl: "https://ph-files.imgix.net/46d376e1-f897-40fc-9921-c64de971ee13.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=390&h=220&fit=max&dpr=2",
thumbnailUrl:
"https://ph-files.imgix.net/46d376e1-f897-40fc-9921-c64de971ee13.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=390&h=220&fit=max&dpr=2",
mediaLink: "https://go.cal.com/cal-ai",
title: "Cal.ai",
description: "Your personal AI scheduling assistant",
href: "https://go.cal.com/cal-ai",
}
},
];
const reversedTips = tips.slice(0).reverse();

Some files were not shown because too many files have changed in this diff Show More