Merge branch 'util/typed-query' into feature/booking-filters
|
@ -87,6 +87,7 @@ TWILIO_TOKEN=
|
|||
TWILIO_MESSAGING_SID=
|
||||
TWILIO_PHONE_NUMBER=
|
||||
NEXT_PUBLIC_SENDER_ID=
|
||||
TWILIO_VERIFY_SID=
|
||||
|
||||
# This is used so we can bypass emails in auth flows for E2E testing
|
||||
# Set it to "1" if you need to run E2E tests locally
|
||||
|
|
|
@ -429,6 +429,8 @@ following
|
|||
12. Leave all other fields as they are
|
||||
13. Complete setup and click ‘View my new Messaging Service’
|
||||
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
||||
15. Create a verify service
|
||||
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
|
||||
|
||||
<!-- LICENSE -->
|
||||
|
||||
|
|
2
apps/api
|
@ -1 +1 @@
|
|||
Subproject commit 8b74f463f454cf84e0f39bf78ff7d0f245014caa
|
||||
Subproject commit c129586336b287d7b93c516435d905337951d2d2
|
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx",
|
||||
"../../../packages/features/**/*.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
|
@ -50,6 +51,21 @@ module.exports = {
|
|||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: true, // Enable modules to help you using className
|
||||
},
|
||||
},
|
||||
],
|
||||
include: path.resolve(__dirname, "../src"),
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -15,7 +15,11 @@ export const parameters = {
|
|||
},
|
||||
};
|
||||
|
||||
addDecorator((storyFn) => <I18nextProvider i18n={i18n}>{storyFn()}</I18nextProvider>);
|
||||
addDecorator((storyFn) => (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>{storyFn()}</div>
|
||||
</I18nextProvider>
|
||||
));
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
|
||||
|
||||
.sbdocs {
|
||||
font-family: 'Inter var' !important;
|
||||
padding: 0!important;
|
||||
|
@ -36,7 +38,7 @@
|
|||
|
||||
/** Docs table **/
|
||||
.custom-args-wrapper{
|
||||
max-height: 600px;
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
margin-bottom: 1rem;
|
||||
|
|
|
@ -66,7 +66,9 @@ const Component = ({
|
|||
}
|
||||
);
|
||||
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
||||
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
|
||||
// Such apps, can only be installed once.
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
"@vercel/og": "^0.0.19",
|
||||
"@vercel/og": "^0.0.21",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"async": "^3.2.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
|
|
@ -1,32 +1,48 @@
|
|||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Button, Icon, showToast } from "@calcom/ui";
|
||||
|
||||
export default function Error500() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
<Head>
|
||||
<title>Something unexpected occurred | {APP_NAME}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="m-auto text-center">
|
||||
<h1 className="font-cal text-[250px] text-gray-900">
|
||||
5
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src="/error.svg" className="-mt-10 inline w-60" alt="0" />
|
||||
}
|
||||
0
|
||||
</h1>
|
||||
<h2 className="mb-2 -mt-16 text-3xl text-gray-600">It's not you, it's us.</h2>
|
||||
<p className="mb-4 max-w-2xl text-gray-500">
|
||||
<div className="rtl: m-auto rounded-md bg-white p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-6xl text-black">500</h1>
|
||||
<h2 className="mt-6 text-2xl font-medium text-black">It's not you, it's us.</h2>
|
||||
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600">
|
||||
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
||||
away for you.
|
||||
</p>
|
||||
{router.query.error && (
|
||||
<div className="mb-8 flex flex-col">
|
||||
<p className="mb-4 max-w-2xl text-sm text-gray-600">
|
||||
Please provide the following text when contacting support to better help you:
|
||||
</p>
|
||||
<pre className="w-full max-w-2xl whitespace-normal break-words rounded-md bg-gray-200 p-4 text-gray-900">
|
||||
{router.query.error}
|
||||
<br />
|
||||
<Button
|
||||
color="secondary"
|
||||
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(router.query.error as string);
|
||||
showToast("Link copied!", "success");
|
||||
}}>
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||
Go back
|
||||
|
|
|
@ -13,10 +13,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
title: "30min between Pro Example and pro@example.com",
|
||||
description: null,
|
||||
additionalNotes: "asdasdas",
|
||||
customInputs: {
|
||||
"Custom input 01": "sadasdasdsadasd",
|
||||
"Custom input 02": "asdasdasd",
|
||||
},
|
||||
startTime: "2022-06-03T09:00:00-06:00",
|
||||
endTime: "2022-06-03T09:30:00-06:00",
|
||||
organizer: {
|
||||
|
@ -32,26 +28,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
timeZone: "America/Chihuahua",
|
||||
language,
|
||||
},
|
||||
{
|
||||
email: "pro@example.com",
|
||||
name: "pro@example.com",
|
||||
timeZone: "America/Chihuahua",
|
||||
language,
|
||||
},
|
||||
{
|
||||
email: "pro@example.com",
|
||||
name: "pro@example.com",
|
||||
timeZone: "America/Chihuahua",
|
||||
language,
|
||||
},
|
||||
],
|
||||
location: "Zoom video",
|
||||
destinationCalendar: null,
|
||||
hideCalendarNotes: false,
|
||||
uid: "bwPWLpjYrx4rZf6MCZdKgE",
|
||||
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
||||
metadata: {},
|
||||
cancellationReason: "It got late",
|
||||
paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" },
|
||||
recurringEvent: null,
|
||||
};
|
||||
|
||||
|
@ -60,10 +42,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||
res.write(
|
||||
renderEmail("DisabledAppEmail", {
|
||||
appName: "Stripe",
|
||||
appType: ["payment"],
|
||||
t,
|
||||
renderEmail("OrganizerRequestEmail", {
|
||||
attendee: evt.attendees[0],
|
||||
calEvent: evt,
|
||||
})
|
||||
);
|
||||
res.end();
|
||||
|
|
|
@ -53,6 +53,8 @@ export default async function handler(req: NextApiRequest) {
|
|||
interFontMedium,
|
||||
]);
|
||||
const ogConfig = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{ name: "inter", data: interFontData, weight: 400 },
|
||||
{ name: "inter", data: interFontMediumData, weight: 500 },
|
||||
|
@ -71,7 +73,7 @@ export default async function handler(req: NextApiRequest) {
|
|||
meetingImage: searchParams.get("meetingImage"),
|
||||
imageType,
|
||||
});
|
||||
return new ImageResponse(
|
||||
const img = new ImageResponse(
|
||||
(
|
||||
<Meeting
|
||||
title={title}
|
||||
|
@ -80,7 +82,9 @@ export default async function handler(req: NextApiRequest) {
|
|||
/>
|
||||
),
|
||||
ogConfig
|
||||
);
|
||||
) as { body: Buffer };
|
||||
|
||||
return new Response(img.body, { status: 200 });
|
||||
}
|
||||
case "app": {
|
||||
const { name, description, slug } = appSchema.parse({
|
||||
|
@ -89,7 +93,11 @@ export default async function handler(req: NextApiRequest) {
|
|||
slug: searchParams.get("slug"),
|
||||
imageType,
|
||||
});
|
||||
return new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig) as {
|
||||
body: Buffer;
|
||||
};
|
||||
|
||||
return new Response(img.body, { status: 200 });
|
||||
}
|
||||
|
||||
case "generic": {
|
||||
|
@ -99,7 +107,11 @@ export default async function handler(req: NextApiRequest) {
|
|||
imageType,
|
||||
});
|
||||
|
||||
return new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
|
||||
body: Buffer;
|
||||
};
|
||||
|
||||
return new Response(img.body, { status: 200 });
|
||||
}
|
||||
|
||||
default:
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { GetStaticPaths, GetStaticProps } from "next";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||
import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||
import { availabilityAsString } from "@calcom/lib/availability";
|
||||
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import type { Schedule as ScheduleType } from "@calcom/types/schedule";
|
||||
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
SkeletonText,
|
||||
Switch,
|
||||
TimezoneSelect,
|
||||
Tooltip,
|
||||
VerticalDivider,
|
||||
} from "@calcom/ui";
|
||||
|
||||
|
@ -29,7 +31,7 @@ import { HttpError } from "@lib/core/http/error";
|
|||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
import EditableHeading from "@components/ui/EditableHeading";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const querySchema = z.object({
|
||||
schedule: stringOrNumber,
|
||||
|
@ -38,31 +40,59 @@ const querySchema = z.object({
|
|||
type AvailabilityFormValues = {
|
||||
name: string;
|
||||
schedule: ScheduleType;
|
||||
dateOverrides: { ranges: TimeRange[] }[];
|
||||
timeZone: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
const { remove, append, update, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||
name: "dateOverrides",
|
||||
});
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<h3 className="font-medium leading-6 text-gray-900">
|
||||
{t("date_overrides")}{" "}
|
||||
<Tooltip content={t("date_overrides_info")}>
|
||||
<span className="inline-block">
|
||||
<Icon.FiInfo />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
|
||||
<div className="mt-1 space-y-2">
|
||||
<DateOverrideList
|
||||
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||
remove={remove}
|
||||
update={update}
|
||||
items={fields}
|
||||
workingHours={workingHours}
|
||||
/>
|
||||
<DateOverrideInputDialog
|
||||
workingHours={workingHours}
|
||||
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||
onChange={(ranges) => append({ ranges })}
|
||||
Trigger={
|
||||
<Button color="secondary" StartIcon={Icon.FiPlus} data-testid="add-override">
|
||||
Add an override
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Availability({ schedule }: { schedule: number }) {
|
||||
const { t, i18n } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const me = useMeQuery();
|
||||
const { timeFormat } = me.data || { timeFormat: null };
|
||||
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
|
||||
|
||||
const form = useForm<AvailabilityFormValues>();
|
||||
const { control, reset } = form;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && data) {
|
||||
reset({
|
||||
name: data?.schedule?.name,
|
||||
schedule: data.availability,
|
||||
timeZone: data.timeZone,
|
||||
isDefault: data.isDefault,
|
||||
});
|
||||
}
|
||||
}, [data, isLoading, reset]);
|
||||
|
||||
const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule });
|
||||
const form = useForm<AvailabilityFormValues>({ defaultValues });
|
||||
const { control } = form;
|
||||
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
|
||||
if (prevDefaultId && currentDefaultId) {
|
||||
|
@ -73,7 +103,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
|
||||
}
|
||||
}
|
||||
utils.viewer.availability.schedule.get.setData({ scheduleId: data.schedule.id }, data);
|
||||
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
|
||||
utils.viewer.availability.list.invalidate();
|
||||
showToast(
|
||||
t("availability_updated_successfully", {
|
||||
|
@ -98,12 +128,16 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => <EditableHeading isReady={!isLoading} {...field} />}
|
||||
render={({ field }) => (
|
||||
<EditableHeading isReady={!isLoading} {...field} data-testid="availablity-title" />
|
||||
)}
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
data ? (
|
||||
data.schedule.availability.map((availability) => (
|
||||
data.schedule.availability
|
||||
.filter((availability) => !!availability.days.length)
|
||||
.map((availability) => (
|
||||
<span key={availability.id}>
|
||||
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
|
||||
<br />
|
||||
|
@ -147,15 +181,16 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||
<Form
|
||||
form={form}
|
||||
id="availability-form"
|
||||
handleSubmit={async (values) => {
|
||||
handleSubmit={async ({ dateOverrides, ...values }) => {
|
||||
updateMutation.mutate({
|
||||
scheduleId: schedule,
|
||||
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
||||
...values,
|
||||
});
|
||||
}}
|
||||
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||
<div className="flex-1">
|
||||
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
|
||||
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
|
||||
<div className=" py-5 pr-4 sm:p-6">
|
||||
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
|
||||
{t("change_start_end")}
|
||||
</h3>
|
||||
|
@ -171,6 +206,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
|
||||
</div>
|
||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
||||
|
@ -211,24 +247,19 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const params = querySchema.safeParse(ctx.params);
|
||||
const ssg = await ssgInit(ctx);
|
||||
const ssr = await ssrInit(ctx);
|
||||
|
||||
if (!params.success) return { notFound: true };
|
||||
|
||||
const scheduleId = params.data.schedule;
|
||||
await ssr.viewer.availability.schedule.get.fetch({ scheduleId });
|
||||
await ssr.viewer.availability.defaultValues.fetch({ scheduleId });
|
||||
return {
|
||||
props: {
|
||||
schedule: params.data.schedule,
|
||||
trpcState: ssg.dehydrate(),
|
||||
schedule: scheduleId,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
revalidate: 10, // seconds
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Shell, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
type User = RouterOutputs["viewer"]["me"];
|
||||
|
||||
export interface IBusySlot {
|
||||
|
@ -16,7 +16,9 @@ export interface IBusySlot {
|
|||
|
||||
const AvailabilityView = ({ user }: { user: User }) => {
|
||||
const { t } = useLocale();
|
||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
||||
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
|
||||
const selectedDate = dayjs(date);
|
||||
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
|
||||
|
||||
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
||||
{
|
||||
|
@ -30,6 +32,17 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||
}
|
||||
);
|
||||
|
||||
const overrides =
|
||||
data?.dateOverrides.reduce((acc, override) => {
|
||||
if (
|
||||
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
|
||||
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
|
||||
)
|
||||
return acc;
|
||||
acc.push({ ...override, source: "Date override" });
|
||||
return acc;
|
||||
}, [] as IBusySlot[]) || [];
|
||||
|
||||
return (
|
||||
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
|
@ -37,9 +50,9 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||
<input
|
||||
type="date"
|
||||
className="inline h-8 border-none p-0"
|
||||
defaultValue={selectedDate.format("YYYY-MM-DD")}
|
||||
defaultValue={formattedSelectedDate}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) setSelectedDate(dayjs(e.target.value));
|
||||
if (e.target.value) setSelectedDate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
|
||||
|
@ -49,21 +62,28 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
{(() => {
|
||||
if (isLoading)
|
||||
return (
|
||||
<>
|
||||
<SkeletonText className="block h-16 w-full" />
|
||||
<SkeletonText className="block h-16 w-full" />
|
||||
</>
|
||||
) : data && data.busy.length > 0 ? (
|
||||
data.busy
|
||||
);
|
||||
|
||||
if (data && (data.busy.length > 0 || overrides.length > 0))
|
||||
return [...data.busy, ...overrides]
|
||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||
.map((slot: IBusySlot) => (
|
||||
<div
|
||||
key={dayjs(slot.start).format("HH:mm")}
|
||||
className="overflow-hidden rounded-md bg-neutral-100">
|
||||
className="overflow-hidden rounded-md bg-neutral-100"
|
||||
data-testid="troubleshooter-busy-time">
|
||||
<div className="px-4 py-5 text-black sm:p-6">
|
||||
{t("calendar_shows_busy_between")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={dayjs(slot.start).format("HH:mm")}>
|
||||
<span
|
||||
className="font-medium text-neutral-800"
|
||||
title={dayjs(slot.start).format("HH:mm")}>
|
||||
{dayjs(slot.start).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("and")}{" "}
|
||||
|
@ -76,12 +96,13 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
));
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md bg-neutral-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||
|
|
|
@ -0,0 +1,450 @@
|
|||
import { BookingStatus } from "@prisma/client";
|
||||
import { createHmac } from "crypto";
|
||||
import { instance } from "gaxios";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { getEventLocationValue, getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getRecurringWhen } from "@calcom/emails/src/components/WhenInfo";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||
import { processBookingConfirmation } from "@calcom/lib/server/queries/bookings/confirm";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, Icon, TextArea } from "@calcom/ui";
|
||||
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
|
||||
enum DirectAction {
|
||||
"accept" = "accept",
|
||||
"reject" = "reject",
|
||||
}
|
||||
const actionSchema = z.nativeEnum(DirectAction);
|
||||
|
||||
const refineParse = (result: z.SafeParseReturnType<any, any>, context: z.RefinementCtx) => {
|
||||
if (result.success === false) {
|
||||
result.error.issues.map((issue) => context.addIssue(issue));
|
||||
}
|
||||
};
|
||||
|
||||
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
|
||||
const pageErrors = {
|
||||
signature_mismatch: "Direct link signature doesn't match signed data",
|
||||
booking_not_found: "Direct link booking not found",
|
||||
user_not_found: "Direct link booking user not found",
|
||||
};
|
||||
|
||||
const requestSchema = z.object({
|
||||
link: z
|
||||
.array(z.string())
|
||||
.max(4)
|
||||
.superRefine((data, ctx) => {
|
||||
refineParse(actionSchema.safeParse(data[0]), ctx);
|
||||
const signedData = `${data[1]}/${data[2]}`;
|
||||
const sig = createHmac("sha1", CALENDSO_ENCRYPTION_KEY).update(signedData).digest("base64");
|
||||
if (data[3] !== sig) {
|
||||
ctx.addIssue({
|
||||
message: pageErrors.signature_mismatch,
|
||||
code: "custom",
|
||||
});
|
||||
console.log(signedData, data, data[3], "==", sig);
|
||||
}
|
||||
}),
|
||||
reason: z.string().optional(),
|
||||
});
|
||||
|
||||
function bookingContent(status: BookingStatus | undefined | null) {
|
||||
switch (status) {
|
||||
case BookingStatus.PENDING:
|
||||
// Trying to reject booking without reason
|
||||
return {
|
||||
iconColor: "gray",
|
||||
Icon: Icon.FiCalendar,
|
||||
titleKey: "event_awaiting_approval",
|
||||
subtitleKey: "someone_requested_an_event",
|
||||
};
|
||||
case BookingStatus.ACCEPTED:
|
||||
// Booking was acepted successfully
|
||||
return {
|
||||
iconColor: "green",
|
||||
Icon: Icon.FiCheck,
|
||||
titleKey: "booking_confirmed",
|
||||
subtitleKey: "emailed_you_and_any_other_attendees",
|
||||
};
|
||||
case BookingStatus.REJECTED:
|
||||
// Booking was rejected successfully
|
||||
return {
|
||||
iconColor: "red",
|
||||
Icon: Icon.FiX,
|
||||
titleKey: "booking_rejection_success",
|
||||
subtitleKey: "emailed_you_and_any_other_attendees",
|
||||
};
|
||||
default:
|
||||
// Booking was already accepted or rejected
|
||||
return {
|
||||
iconColor: "yellow",
|
||||
Icon: Icon.FiAlertTriangle,
|
||||
titleKey: "booking_already_accepted_rejected",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Directlink({ booking, reason, status }: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const acceptPath = router.asPath.replace("reject", "accept");
|
||||
const rejectPath = router.asPath.replace("accept", "reject");
|
||||
const [cancellationReason, setCancellationReason] = useState("");
|
||||
function getRecipientStart(format: string) {
|
||||
return dayjs(booking.startTime).tz(booking?.user?.timeZone).format(format);
|
||||
}
|
||||
|
||||
function getRecipientEnd(format: string) {
|
||||
return dayjs(booking.endTime).tz(booking?.user?.timeZone).format(format);
|
||||
}
|
||||
const organizer = {
|
||||
...booking.attendees[0],
|
||||
language: {
|
||||
translate: t,
|
||||
locale: booking.attendees[0].locale ?? "en",
|
||||
},
|
||||
};
|
||||
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(booking.location)
|
||||
? booking.location[0]
|
||||
: // If there is no location set then we default to Cal Video
|
||||
"integrations:daily";
|
||||
const locationToDisplay = getSuccessPageLocationMessage(location, t);
|
||||
const content = bookingContent(status);
|
||||
const recurringInfo = getRecurringWhen({
|
||||
recurringEvent: booking.eventType?.recurringEvent,
|
||||
attendee: organizer,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
title={t(content.titleKey)}
|
||||
description=""
|
||||
nextSeoProps={{
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
}}
|
||||
/>
|
||||
<div className="dark:bg-darkgray-50 desktop-transparent min-h-screen bg-gray-100 px-4">
|
||||
<main className="mx-auto max-w-3xl">
|
||||
<div className="z-50 overflow-y-auto ">
|
||||
<div className="flex items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<div
|
||||
className="main dark:bg-darkgray-100 inline-block transform overflow-hidden rounded-lg border bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 sm:my-[68px] sm:w-full sm:max-w-xl sm:py-8 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-full sm:mx-auto bg-${content.iconColor}-100`}>
|
||||
<content.Icon className={`h-5 w-5 text-${content.iconColor}-600`} />
|
||||
</div>
|
||||
<div className="mt-6 mb-8 last:mb-0 sm:text-center">
|
||||
<h3
|
||||
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
|
||||
id="modal-headline">
|
||||
{t(content.titleKey)}
|
||||
</h3>
|
||||
{content.subtitleKey && (
|
||||
<div className="mt-3">
|
||||
<p className="text-neutral-600 dark:text-gray-300">{t(content.subtitleKey)}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t border-[#e1e1e1] pt-8 text-left text-[#313131] dark:text-gray-300">
|
||||
<div className="col-span-3 font-medium sm:col-span-1">{t("what")}</div>
|
||||
<div className="col-span-3 mb-6 last:mb-0 sm:col-span-2">{booking.title}</div>
|
||||
<div className="col-span-3 font-medium sm:col-span-1">{t("when")}</div>
|
||||
<div className="col-span-3 mb-6 last:mb-0 sm:col-span-2">
|
||||
{recurringInfo !== "" && (
|
||||
<>
|
||||
{recurringInfo}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{booking.eventType.recurringEvent?.count ? `${t("starting")} ` : ""}
|
||||
{t(getRecipientStart("dddd").toLowerCase())},{" "}
|
||||
{t(getRecipientStart("MMMM").toLowerCase())} {getRecipientStart("D, YYYY")}
|
||||
<br />
|
||||
{getRecipientStart("h:mma")} - {getRecipientEnd("h:mma")}{" "}
|
||||
<span style={{ color: "#888888" }}>({booking.attendees[0].timeZone})</span>
|
||||
</div>
|
||||
{(booking?.user || booking?.attendees) && (
|
||||
<>
|
||||
<div className="col-span-3 font-medium sm:col-span-1">{t("who")}</div>
|
||||
<div className="col-span-3 last:mb-0 sm:col-span-2">
|
||||
<>
|
||||
{booking?.user && (
|
||||
<div className="mb-3">
|
||||
<p>{booking.user.name}</p>
|
||||
<p className="text-[#888888]">{booking.user.email}</p>
|
||||
</div>
|
||||
)}
|
||||
{booking?.attendees.map((attendee) => (
|
||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
||||
{attendee.name && <p>{attendee.name}</p>}
|
||||
<p className="text-[#888888]">{attendee.email}</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{locationToDisplay && (
|
||||
<>
|
||||
<div className="col-span-3 mt-6 font-medium sm:col-span-1">{t("where")}</div>
|
||||
<div className="col-span-3 mt-6 sm:col-span-2">
|
||||
{locationToDisplay.startsWith("http") ? (
|
||||
<a title="Meeting Link" href={locationToDisplay}>
|
||||
{locationToDisplay}
|
||||
</a>
|
||||
) : (
|
||||
locationToDisplay
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{booking?.description && (
|
||||
<>
|
||||
<div className="col-span-3 mt-9 font-medium sm:col-span-1">
|
||||
{t("additional_notes")}
|
||||
</div>
|
||||
<div className="col-span-3 mb-2 mt-9 sm:col-span-2">
|
||||
<p>{booking.description}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{status === BookingStatus.REJECTED && reason && (
|
||||
<>
|
||||
<div className="col-span-3 mt-9 font-medium sm:col-span-1">
|
||||
{t("rejection_reason")}
|
||||
</div>
|
||||
<div className="col-span-3 mb-2 mt-9 sm:col-span-2">
|
||||
<p>{reason}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{status === BookingStatus.PENDING && reason === undefined && (
|
||||
<>
|
||||
<hr className="mt-6" />
|
||||
<div className="mt-5 text-left sm:mt-6">
|
||||
<label className="font-medium text-[#313131] dark:text-white">
|
||||
{`${t("rejection_reason")} (${t("optional").toLowerCase()})`}
|
||||
</label>
|
||||
<TextArea
|
||||
value={cancellationReason}
|
||||
onChange={(e) => setCancellationReason(e.target.value)}
|
||||
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex flex-col-reverse rtl:space-x-reverse">
|
||||
<div className="ml-auto flex w-full justify-end space-x-4">
|
||||
<Button
|
||||
color="secondary"
|
||||
className="hidden text-center sm:block"
|
||||
href={acceptPath}>
|
||||
{t("booking_accept_intent")}
|
||||
</Button>
|
||||
<Button
|
||||
className="hidden sm:block"
|
||||
onClick={async () => {
|
||||
router.push(
|
||||
`${rejectPath}?reason=${encodeURIComponent(cancellationReason)}`
|
||||
);
|
||||
}}>
|
||||
{t("rejection_confirmation")}
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="block text-center sm:hidden"
|
||||
href={acceptPath}>
|
||||
{t("accept")}
|
||||
</Button>
|
||||
<Button
|
||||
className="block sm:hidden"
|
||||
onClick={async () => {
|
||||
router.push(
|
||||
`${rejectPath}?reason=${encodeURIComponent(cancellationReason)}`
|
||||
);
|
||||
}}>
|
||||
{t("reject")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const parsedQuery = requestSchema.safeParse(context.query);
|
||||
|
||||
// Parsing error, showing error 500 with message
|
||||
if (parsedQuery.success === false) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${parsedQuery.error.errors[0].message.concat(
|
||||
" accessing " + context.resolvedUrl
|
||||
)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
link: [action, email, bookingUid],
|
||||
reason,
|
||||
} = parsedQuery.data;
|
||||
|
||||
const isAccept = action === DirectAction.accept;
|
||||
|
||||
const bookingRaw = await prisma?.booking.findFirst({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
location: true,
|
||||
description: true,
|
||||
id: true,
|
||||
recurringEventId: true,
|
||||
status: true,
|
||||
title: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventType: {
|
||||
select: {
|
||||
recurringEvent: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
locale: true,
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
destinationCalendar: true,
|
||||
credentials: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Booking not found, showing error 500 with message
|
||||
if (!bookingRaw) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${pageErrors.booking_not_found.concat(" accessing " + context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const booking = {
|
||||
...bookingRaw,
|
||||
startTime: bookingRaw.startTime.toString(),
|
||||
endTime: bookingRaw.endTime.toString(),
|
||||
eventType: {
|
||||
...bookingRaw.eventType,
|
||||
recurringEvent: parseRecurringEvent(bookingRaw?.eventType?.recurringEvent),
|
||||
},
|
||||
attendees: bookingRaw?.attendees.map((att) => ({
|
||||
...att,
|
||||
language: {
|
||||
locale: att.locale ?? "en",
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
// Booking user not found, showing error 500 with message
|
||||
if (booking.user === null) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${pageErrors.user_not_found.concat(" accessing " + context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Booking already accepted or rejected
|
||||
if (booking.status !== BookingStatus.PENDING) {
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Trying to reject booking without reason
|
||||
if (!isAccept && reason === undefined) {
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Booking good to be accepted or rejected, proceeding to mark it
|
||||
let result: { status: BookingStatus | undefined } = { status: undefined };
|
||||
try {
|
||||
result = await processBookingConfirmation(
|
||||
{
|
||||
bookingId: booking.id,
|
||||
user: booking.user,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
confirmed: action === DirectAction.accept,
|
||||
rejectionReason: reason,
|
||||
},
|
||||
prisma
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof TRPCError) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${e.message.concat(" accessing " + context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
status: result.status,
|
||||
reason: context.query.reason ?? null,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -7,6 +7,7 @@ import React, { Fragment, useEffect, useState } from "react";
|
|||
import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
|
||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import isCalcom from "@calcom/lib/isCalcom";
|
||||
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import {
|
||||
Badge,
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
showToast,
|
||||
Switch,
|
||||
Tooltip,
|
||||
TipBanner,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
|
@ -587,6 +589,7 @@ const EventTypesPage = () => {
|
|||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => (
|
||||
<>
|
||||
{isCalcom && <TipBanner />}
|
||||
{data.eventTypeGroups.map((group, index) => (
|
||||
<Fragment key={group.profile.slug}>
|
||||
{/* hide list heading when there is only one (current user) */}
|
||||
|
@ -605,7 +608,6 @@ const EventTypesPage = () => {
|
|||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
|
||||
<EmbedDialog />
|
||||
</>
|
||||
|
|
|
@ -10,7 +10,7 @@ function AdminAppsView() {
|
|||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("apps")} description={t("apps_description")} />
|
||||
<Meta title={t("apps")} description={t("admin_apps_description")} />
|
||||
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Availablity tests", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await page.goto("/availability");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="schedules"]');
|
||||
});
|
||||
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test("Date Overrides", async ({ page }) => {
|
||||
await test.step("Can add a date override", async () => {
|
||||
await page.locator('[data-testid="schedules"] > li a').click();
|
||||
await page.locator('[data-testid="add-override"]').click();
|
||||
await page.locator('[id="modal-title"]').waitFor();
|
||||
await page.locator('[data-testid="incrementMonth"]').click();
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
|
||||
await page.locator('[data-testid="date-override-mark-unavailable"]').click();
|
||||
await page.locator('[data-testid="add-override-submit-btn"]').click();
|
||||
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(1);
|
||||
await page.locator('[form="availability-form"][type="submit"]').click();
|
||||
});
|
||||
|
||||
await test.step("Date override is displayed in troubleshooter", async () => {
|
||||
const response = await page.waitForResponse("**/api/trpc/viewer.availability.schedule.update?batch=1");
|
||||
const json = await response.json();
|
||||
// @ts-expect-error trust me bro
|
||||
const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date);
|
||||
const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`;
|
||||
await page.goto(troubleshooterURL);
|
||||
await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("Availablity pages", async ({ page }) => {
|
||||
await test.step("Can add a new schedule", async () => {
|
||||
await page.locator('[data-testid="new-schedule"]').click();
|
||||
await page.locator('[id="name"]').fill("More working hours");
|
||||
page.locator('[type="submit"]').click();
|
||||
await expect(page.locator("[data-testid=availablity-title]")).toHaveValue("More working hours");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -67,6 +67,7 @@ test("add webhook & test that creating an event triggers a webhook call", async
|
|||
body.payload.eventTypeId = dynamic;
|
||||
body.payload.videoCallData = dynamic;
|
||||
body.payload.appsStatus = dynamic;
|
||||
body.payload.metadata.videoCallUrl = dynamic;
|
||||
|
||||
// if we change the shape of our webhooks, we can simply update this by clicking `u`
|
||||
// console.log("BODY", body);
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between Nameless and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Nameless","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
|
||||
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between Nameless and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Nameless","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{"videoCallUrl":"[redacted/dynamic]"},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
|
|
@ -0,0 +1,5 @@
|
|||
<svg stroke="#888888" fill="none" stroke-width="2.5" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</svg>
|
After Width: | Height: | Size: 360 B |
After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 35 KiB |
|
@ -52,6 +52,7 @@
|
|||
"still_waiting_for_approval": "يوجد حدث لا يزال في انتظار الموافقة",
|
||||
"event_is_still_waiting": "لا يزال طلب الحدث في الانتظار: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"no_more_results": "لا يوجد مزيد من النتائج",
|
||||
"no_results": "لا نتائج",
|
||||
"load_more_results": "تحميل المزيد من النتائج",
|
||||
"integration_meeting_id": "مُعرّف اجتماع {{integrationName}}: {{meetingId}}",
|
||||
"confirmed_event_type_subject": "تم التأكيد: {{eventType}} مع {{name}} في {{date}}",
|
||||
|
@ -248,6 +249,7 @@
|
|||
"add_to_calendar": "إضافة إلى التقويم",
|
||||
"add_another_calendar": "إضافة تقويم آخر",
|
||||
"other": "آخر",
|
||||
"email_sign_in_subject": "رابط تسجيل الدخول بك لـ{{appName}}",
|
||||
"emailed_you_and_attendees": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة للتقويم عبر البريد الإلكتروني تتضمن كل التفاصيل.",
|
||||
"emailed_you_and_attendees_recurring": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة تقويم عبر البريد الإلكتروني لأول هذه الأحداث المتكررة.",
|
||||
"emailed_you_and_any_other_attendees": "تم إرسال هذه المعلومات إليك وإلى الحضور الآخرين عبر البريد الإلكتروني.",
|
||||
|
@ -383,7 +385,6 @@
|
|||
"go_to_billing_portal": "انتقل إلى بوابة الفوترة",
|
||||
"need_anything_else": "هل تحتاج إلى أي شيء آخر؟",
|
||||
"further_billing_help": "إذا كنت تحتاج إلى أي مساعدة إضافية بخصوص الفوترة، فإن فريق الدعم لدينا في انتظارك لتقديم المساعدة.",
|
||||
"contact_our_support_team": "الاتصال بفريق الدعم لدينا",
|
||||
"uh_oh": "عذرًا!",
|
||||
"no_event_types_have_been_setup": "لم يقم هذا المستخدم بإعداد أي أنواع للحدث حتى الآن.",
|
||||
"edit_logo": "تعديل الشعار",
|
||||
|
@ -420,6 +421,7 @@
|
|||
"current_incorrect_password": "كلمة المرور الحالية غير صحيحة",
|
||||
"password_hint_caplow": "مزيج من الحروف الكبيرة والحروف الصغيرة",
|
||||
"password_hint_min": "8 أحرف كحد أدنى",
|
||||
"password_hint_admin_min": "15 محرفًا كحد أدنى",
|
||||
"password_hint_num": "يحتوي على رقم واحد على الأقل",
|
||||
"invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من 7 أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة",
|
||||
"incorrect_password": "كلمة المرور غير صحيحة.",
|
||||
|
@ -463,11 +465,14 @@
|
|||
"booking_confirmation": "قم بتأكيد {{eventTypeTitle}} مع {{profileName}}",
|
||||
"booking_reschedule_confirmation": "أعد جدولة {{eventTypeTitle}} مع {{profileName}}",
|
||||
"in_person_meeting": "الرابط أو الاجتماع الشخصي",
|
||||
"attendeeInPerson": "شخصيًا (عنوان من سيحضر)",
|
||||
"inPerson": "شخصيًا (عنوان المنظم)",
|
||||
"link_meeting": "رابط الاجتماع",
|
||||
"phone_call": "رقم هاتف الحضور",
|
||||
"your_number": "رقم هاتفك",
|
||||
"phone_number": "رقم الهاتف",
|
||||
"attendee_phone_number": "رقم هاتف الحضور",
|
||||
"organizer_phone_number": "رقم هاتف المنظم",
|
||||
"host_phone_number": "رقم هاتفك",
|
||||
"enter_phone_number": "أدخل رقم الهاتف",
|
||||
"reschedule": "إعادة الجدولة",
|
||||
|
@ -554,6 +559,10 @@
|
|||
"collective": "جماعي",
|
||||
"collective_description": "جدولة الاجتماعات عندما يكون جميع أعضاء الفريق المحددين متاحين.",
|
||||
"duration": "المدة",
|
||||
"available_durations": "المدد المتاحة",
|
||||
"default_duration": "المدة الافتراضية",
|
||||
"default_duration_no_options": "يرجى اختيار المدد المتاحة أولاً",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"minutes": "الدقائق",
|
||||
"round_robin": "الترتيب الدوري",
|
||||
"round_robin_description": "نقل الاجتماعات بشكل دوري بين أعضاء الفريق المتعددين.",
|
||||
|
@ -619,6 +628,7 @@
|
|||
"teams": "الفرق",
|
||||
"team": "فريق",
|
||||
"team_billing": "الفوترة الخاصة بالفريق",
|
||||
"team_billing_description": "إدارة الفواتير لفريقك",
|
||||
"upgrade_to_flexible_pro_title": "لقد قمنا بتغيير الفوترة الخاصة بالفرق",
|
||||
"upgrade_to_flexible_pro_message": "يوجد أعضاء في فريقك من دون مقاعد. قم بترقية خطة Pro خاصتك لتوفير المقاعد الناقصة.",
|
||||
"changed_team_billing_info": "بدءًا من يناير 2022، أصبحنا نفرض الرسوم على مقاعد أعضاء الفريق بشكل منفرد. حاليًا، أعضاء فريقك الذين كانت لديهم خطة Pro مجانية أصبحوا في فترة تجريبية مدتها 14 يومًا. وبمجرد انتهاء الفترة التجريبية الخاصة بهم، سيجري إخفاء هؤلاء الأعضاء من فريقك ما لم تقم بالترقية الآن.",
|
||||
|
@ -694,6 +704,7 @@
|
|||
"hide_event_type": "إخفاء نوع الحدث",
|
||||
"edit_location": "تعديل الموقع",
|
||||
"into_the_future": "في المستقبل",
|
||||
"when_booked_with_less_than_notice": "عند الحجز بأقل من إشعار <time></time>",
|
||||
"within_date_range": "خلال فترة زمنية",
|
||||
"indefinitely_into_future": "إلى أجل غير مسمى في المستقبل",
|
||||
"add_new_custom_input_field": "إضافة خانة إدخال مخصص جديدة",
|
||||
|
@ -713,6 +724,8 @@
|
|||
"delete_account_confirmation_message": "هل تريد بالتأكيد حذف حسابك على {{appName}}؟ أي شخص شاركت رابط حسابك معه لن يستطيع بعد الآن الحجز باستخدامه، وستفقد أي تفضيلات حفظتها.",
|
||||
"integrations": "التكاملات",
|
||||
"apps": "التطبيقات",
|
||||
"apps_description": "تمكين تطبيقات مثيل Cal الخاص بك",
|
||||
"apps_listing": "قائمة التطبيقات",
|
||||
"category_apps": "تطبيقات {{category}}",
|
||||
"app_store": "متجر التطبيقات",
|
||||
"app_store_description": "الربط بين الناس، والتكنولوجيا، ومكان العمل.",
|
||||
|
@ -736,6 +749,7 @@
|
|||
"toggle_calendars_conflict": "قم بتبديل التقويمات التي تريد التحقق من وجود تضاربات فيها لمنع حدوث حجز مزدوج.",
|
||||
"select_destination_calendar": "إنشاء أحداث في",
|
||||
"connect_additional_calendar": "ربط رزنامة إضافية",
|
||||
"calendar_updated_successfully": "تم تحديث الرزنامة بنجاح",
|
||||
"conferencing": "المؤتمرات عبر الفيديو",
|
||||
"calendar": "التقويم",
|
||||
"payments": "المدفوعات",
|
||||
|
@ -768,6 +782,7 @@
|
|||
"trending_apps": "التطبيقات الرائجة",
|
||||
"explore_apps": "تطبيقات {{category}}",
|
||||
"installed_apps": "التطبيقات المثبتة",
|
||||
"free_to_use_apps": "متاح",
|
||||
"no_category_apps": "لا توجد تطبيقات {{category}}",
|
||||
"no_category_apps_description_calendar": "إضافة تطبيق تقويم للتحقق من أي تضارب لمنع أي حجوزات مزدوجة",
|
||||
"no_category_apps_description_conferencing": "جرب إضافة تطبيق مؤتمرات لإنشاء مكالمات فيديو مع عملائك",
|
||||
|
@ -806,6 +821,8 @@
|
|||
"verify_wallet": "تأكيد المحفظة",
|
||||
"connect_metamask": "توصيل Metamask",
|
||||
"create_events_on": "إنشاء أحداث في",
|
||||
"enterprise_license": "هذه هي ميزة للمؤسسات",
|
||||
"enterprise_license_description": "لتمكين هذه الميزة، احصل على مفتاح نشر في وحدة التحكم {{consoleUrl}} وأضفه إلى وحدة التحكم الخاصة بك. nv باسم CALCOM_LICENSE_KEY. إذا كان لدى فريقك بالفعل ترخيص، يرجى الاتصال بـ {{supportMail}} للحصول على المساعدة.",
|
||||
"missing_license": "الترخيص مفقود",
|
||||
"signup_requires": "يلزم ترخيص تجاري",
|
||||
"signup_requires_description": "لا تقدم شركة {{companyName}} حاليًا إصدارًا مجانيًا مفتوح المصدر لصفحة التسجيل. للحصول على حق الوصول الكامل إلى مكونات التسجيل، يجب أن تحصل على ترخيص تجاري. للاستخدام الشخصي، نوصي باستخدام منصة Prisma Data أو أي واجهة أخرى من واجهات Postgres لإنشاء الحسابات.",
|
||||
|
@ -897,6 +914,7 @@
|
|||
"user_impersonation_heading": "انتحال شخصية مستخدم",
|
||||
"user_impersonation_description": "يسمح لفريق الدعم الخاص بنا بتسجيل الدخول مؤقتًا نيابة عنك لمساعدتنا على حل أي مشاكل تبلغ عنها بسرعة.",
|
||||
"team_impersonation_description": "يسمح لأعضاء فريقك بتسجيل الدخول نيابة عنك مؤقتًا.",
|
||||
"allow_booker_to_select_duration": "السماح لمن يقوم بالحجز بتحديد المدة",
|
||||
"impersonate_user_tip": "يتم مراجعة جميع استخدامات هذه الميزة.",
|
||||
"impersonating_user_warning": "انتحال اسم مستخدم \"{{user}}\".",
|
||||
"impersonating_stop_instructions": "<0>انقر هنا لإيقاف </0>.",
|
||||
|
@ -1014,6 +1032,9 @@
|
|||
"error_removing_app": "خطأ في إزالة التطبيق",
|
||||
"web_conference": "مؤتمر عبر الإنترنت",
|
||||
"requires_confirmation": "يتطلب التأكيد",
|
||||
"always_requires_confirmation": "دائمًا",
|
||||
"requires_confirmation_threshold": "يتطلب تأكيد إذا تم حجزه مع إشعار $t({{unit}}_timeUnit){{time}}",
|
||||
"may_require_confirmation": "قد يتطلب التأكيد",
|
||||
"nr_event_type_one": "{{count}} نوع حدث",
|
||||
"nr_event_type_other": "{{count}} من أنواع الحدث",
|
||||
"add_action": "إضافة إجراء",
|
||||
|
@ -1101,6 +1122,9 @@
|
|||
"event_limit_tab_description": "ما تواتر مرات الحجز",
|
||||
"event_advanced_tab_description": "إعدادات الرزنامة والمزيد...",
|
||||
"event_advanced_tab_title": "متقدم",
|
||||
"event_setup_multiple_duration_error": "إعداد الحدث: تستلزم فترات متعددة خيارًا واحدًا على الأقل.",
|
||||
"event_setup_multiple_duration_default_error": "إعداد الحدث: الرجاء تحديد مدة افتراضية صالحة.",
|
||||
"event_setup_booking_limits_error": "يجب أن تكون حدود الحجز بترتيب تصاعدي. [يوم،أسبوع،شهر،سنة]",
|
||||
"select_which_cal": "اختر أي رزنامة تريد إضافة الحجز إليها",
|
||||
"custom_event_name": "اسم حدث مخصص",
|
||||
"custom_event_name_description": "إنشاء أسماء أحداث مخصصة لعرضها في حدث الرزنامة",
|
||||
|
@ -1154,8 +1178,11 @@
|
|||
"invoices": "الفواتير",
|
||||
"embeds": "التضمينات",
|
||||
"impersonation": "انتحال الشخصية",
|
||||
"impersonation_description": "إعدادات إدارة انتحال شخصية مستخدم",
|
||||
"users": "المستخدمون",
|
||||
"profile_description": "إدارة الإعدادات لملفك الشخصي على {{appName}}",
|
||||
"users_description": "يمكنك هنا العثور على قائمة بجميع المستخدمين",
|
||||
"users_listing": "قائمة المستخدمين",
|
||||
"general_description": "إدارة الإعدادات للغة والمنطقة الزمنية الخاصة بك",
|
||||
"calendars_description": "قم بتهيئة كيفية تفاعل أنواع الأحداث مع التقويمات الخاصة بك",
|
||||
"appearance_description": "إدارة إعدادات مظهر الحجز الخاص بك",
|
||||
|
@ -1360,6 +1387,7 @@
|
|||
"number_sms_notifications": "رقم الهاتف (إشعارات الرسائل النصية)",
|
||||
"attendee_email_workflow": "اسم الحاضر",
|
||||
"attendee_email_info": "البريد الإلكتروني للشخص الحجز",
|
||||
"kbar_search_placeholder": "اكتب أمرًا أو بحثًا...",
|
||||
"invalid_credential": "لا! يبدو أن الصلاحية انتهت أو تم إلغاؤها. يرجى إعادة التثبيت مرة أخرى.",
|
||||
"choose_common_schedule_team_event": "اختر جدولاً زمنياً مشتركاً",
|
||||
"choose_common_schedule_team_event_description": "قم بتمكين هذا الخيار إذا كنت ترغب في استخدام جدول زمني مشترك بين المضيفين. عند تعطيل هذا الخيار، سيتم حجز كل مضيف على أساس جدوله الافتراضي.",
|
||||
|
@ -1370,5 +1398,44 @@
|
|||
"test_preview": "اختبار المعاينة",
|
||||
"route_to": "التوجيه إلى",
|
||||
"test_preview_description": "اختبار نموذج التوجيه الخاص بك دون تقديم أي بيانات",
|
||||
"test_routing": "اختبار التوجيه"
|
||||
"test_routing": "اختبار التوجيه",
|
||||
"payment_app_disabled": "قام المشرف بتعطيل تطبيق دفع",
|
||||
"edit_event_type": "تعديل نوع الحدث",
|
||||
"collective_scheduling": "جدولة جماعية",
|
||||
"make_it_easy_to_book": "سهّل على فريقك الحجز عندما يكون الجميع متاحين.",
|
||||
"find_the_best_person": "ابحث عن أفضل شخص متوفر، وقم بالتدوير بين أعضاء فريقك.",
|
||||
"fixed_round_robin": "روبين دائري ثابت",
|
||||
"add_one_fixed_attendee": "أضف حاضرًا ثابتًا وروبين مستدير من خلال عدد من الحاضرين.",
|
||||
"calcom_is_better_with_team": "Cal.com أفضل مع الفرق",
|
||||
"add_your_team_members": "أضف أعضاء فريقك إلى أنواع الأحداث الخاصة بك. استخدم الجدولة الجماعية لإدراج الجميع أو العثور على أنسب شخص مع جدولة روبن الدائرية.",
|
||||
"booking_limit_reached": "تم الوصول إلى الحد الأقصى للحجز لهذا النوع من الأحداث",
|
||||
"admin_has_disabled": "قام المشرف بتعطيل {{appName}}",
|
||||
"disabled_app_affects_event_type": "قام المشرف بتعطيل {{appName}} مما يؤثر على نوع الحدث {{eventType}} لديك",
|
||||
"disable_payment_app": "قام المشرف بتعطيل {{appName}} الذي يؤثر على نوع الحدث {{title}} لديك. ولا يزال بإمكان الحاضرين حجز هذا النوع من الأحداث، ولكن لن يُطالبوا بالدفع. يمكنك إخفاء نوع الحدث لمنع حدوث هذا إلى أن يقوم المشرف بتغيير طريقة الدفع لك.",
|
||||
"payment_disabled_still_able_to_book": "لا يزال بإمكان الحاضرين حجز هذا النوع من الأحداث، ولكن لن يُطالبوا بالدفع. يمكنك إخفاء نوع الحدث لمنع حدوث هذا إلى أن يقوم المشرف بتغيير طريقة الدفع لك.",
|
||||
"app_disabled_with_event_type": "عطّل المشرف {{appName}} مما يؤثر على نوع الحدث {{title}} لديك.",
|
||||
"app_disabled_video": "عطّل المشرف {{appName}} الذي قد يؤثر على أنواع الأحداث لديك. إذا كان لديك أنواع من الأحداث مع {{appName}} كموقع، سيكون افتراضيًا من أجل فيديو Cal.",
|
||||
"app_disabled_subject": "تم تعطيل {{appName}}",
|
||||
"navigate_installed_apps": "الذهاب إلى التطبيقات المثبتة",
|
||||
"disabled_calendar": "إذا كان لديك تقويم آخر مثبت، ستُضاف الحجوزات الجديدة إليه. إذا لم يكن الأمر كذلك، قم إذن بتوصيل تقويم جديد لكيلا تفوت أي حجوزات جديدة.",
|
||||
"enable_apps": "تمكين التطبيقات",
|
||||
"enable_apps_description": "تمكين التطبيقات التي يمكن للمستخدمين دمجها مع Cal.com",
|
||||
"app_is_enabled": "تم تمكين {{appName}}",
|
||||
"app_is_disabled": "تم تعطيل {{appName}}",
|
||||
"keys_have_been_saved": "تم حفظ المفاتيح",
|
||||
"disable_app": "تعطيل التطبيق",
|
||||
"disable_app_description": "يمكن لتعطيل هذا التطبيق أن يسبب مشاكل مع كيفية تفاعل المستخدمين مع Cal",
|
||||
"edit_keys": "تحرير المفاتيح",
|
||||
"admin_apps_description": "تمكين تطبيقات مثيل Cal الخاص بك",
|
||||
"no_available_apps": "لا توجد تطبيقات متاحة",
|
||||
"no_available_apps_description": "الرجاء التأكد من وجود تطبيقات في نشرك تحت 'حزم/متجر تطبيقات'",
|
||||
"no_apps": "لا توجد تطبيقات مفعلة في هذا المظهر من Cal",
|
||||
"apps_settings": "إعدادات التطبيقات",
|
||||
"fill_this_field": "يرجى ملء هذا الحقل",
|
||||
"options": "خيارات",
|
||||
"enter_option": "أدخل خيار {{index}}",
|
||||
"add_an_option": "إضافة خيار",
|
||||
"radio": "الراديو",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-نسخ",
|
||||
"set_as_default": "تعيين كافتراضي"
|
||||
}
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "Přejít na fakturační portál",
|
||||
"need_anything_else": "Potřebujete ještě něco?",
|
||||
"further_billing_help": "Pokud potřebujete další pomoc s fakturací, náš tým podpory je zde, aby vám pomohl.",
|
||||
"contact_our_support_team": "Kontaktovat tým podpory",
|
||||
"uh_oh": "Ale ne!",
|
||||
"no_event_types_have_been_setup": "Tento uživatel zatím nezaložil žádné typy událostí.",
|
||||
"edit_logo": "Upravit logo",
|
||||
|
|
|
@ -370,7 +370,6 @@
|
|||
"go_to_billing_portal": "Zum Rechnungsportal gehen",
|
||||
"need_anything_else": "Brauchen Sie etwas anderes?",
|
||||
"further_billing_help": "Wenn Sie weitere Hilfe bei der Rechnungsstellung benötigen, hilft Ihnen unser Support-Team gerne weiter.",
|
||||
"contact_our_support_team": "Kontaktieren Sie unser Support-Team",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "Dieser Benutzer hat noch keine Termintypen eingerichtet.",
|
||||
"edit_logo": "Logo bearbeiten",
|
||||
|
@ -1338,5 +1337,6 @@
|
|||
"test_preview": "Vorschau testen",
|
||||
"route_to": "Weiterleiten zu",
|
||||
"test_preview_description": "Testen Sie Ihr Weiterleitungsformular, ohne Daten zu senden",
|
||||
"test_routing": "Testweiterleitung"
|
||||
"test_routing": "Testweiterleitung",
|
||||
"admin_apps_description": "Hier findest du eine Auflistung deiner Apps"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"day_one": "{{count}} ημέρα",
|
||||
"day_other": "{{count}} ημέρες",
|
||||
"second_one": "{{count}} δευτερόλεπτο",
|
||||
"second_other": "{{count}} δευτερόλεπτα",
|
||||
"upgrade_now": "Αναβαθμίστε τώρα",
|
||||
"accept_invitation": "Αποδοχή Πρόσκλησης",
|
||||
"have_any_questions": "Έχετε ερωτήσεις? Είμαστε εδώ για να βοηθήσουμε.",
|
||||
"reset_password_subject": "{{appName}}: Οδηγίες επαναφοράς κωδικού πρόσβασης",
|
||||
"event_declined_subject": "Απορρίφθηκε: {{eventType}} με {{name}} στις {{date}}",
|
||||
"event_cancelled_subject": "Ακύρωση: {{eventType}} με {{name}} στις {{date}}",
|
||||
"need_to_reschedule_or_cancel": "Χρειάζεται να επαναπρογραμματίσετε ή να ακυρώσετε;",
|
||||
"cancellation_reason": "Λόγος ακύρωσης (προαιρετικό)",
|
||||
"rejection_reason": "Λόγος απόρριψης",
|
||||
"rejection_reason_title": "Απόρριψη του αιτήματος κράτησης;",
|
||||
"rejection_confirmation": "Απόρριψη κράτησης",
|
||||
"error_message": "Το μήνυμα σφάλματος ήταν: '{{errorMessage}}'",
|
||||
"refund_failed_subject": "Η επιστροφή χρημάτων απέτυχε: {{name}} - {{date}} - {{eventType}}",
|
||||
"refund_failed": "Η επιστροφή χρημάτων για την εκδήλωση {{eventType}} με {{userName}} στις {{date}} απέτυχε.",
|
||||
"a_refund_failed": "Αποτυχία επιστροφής χρημάτων",
|
||||
"awaiting_payment_subject": "Σε αναμονή Πληρωμής: {{eventType}} με {{name}} στις {{date}}",
|
||||
"meeting_awaiting_payment": "Η πληρωμή της συνάντησής σας εκκρεμεί",
|
||||
"help": "Βοήθεια",
|
||||
"price": "Τιμή",
|
||||
"payment": "Πληρωμή",
|
||||
"missing_card_fields": "Λείπουν τα πεδία της κάρτας",
|
||||
"pay_now": "Πληρωμή τώρα",
|
||||
"terms_summary": "Περίληψη όρων",
|
||||
"open_env": "Ανοίξτε το .env και συμφωνήστε με την Άδεια χρήσης μας",
|
||||
"env_changed": "Έχω αλλάξει το .env μου",
|
||||
"accept_license": "Αποδοχή Άδειας",
|
||||
"no_more_results": "Δεν υπάρχουν άλλα αποτελέσματα",
|
||||
"no_results": "Δεν υπάρχουν αποτελέσματα",
|
||||
"load_more_results": "Φόρτωση περισσότερων αποτελεσμάτων",
|
||||
"integration_meeting_id": "ID συνάντησης {{integrationName}} : {{meetingId}}",
|
||||
"confirmed_event_type_subject": "Επιβεβαίωση: {{eventType}} με {{name}} στις {{date}}",
|
||||
"new_event_request": "Νέο αίτημα εκδήλωσης: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"confirm_or_reject_request": "Επιβεβαιώστε ή απορρίψτε το αίτημα",
|
||||
"check_bookings_page_to_confirm_or_reject": "Ελέγξτε τη σελίδα κρατήσεων για να επιβεβαιώσετε ή να απορρίψετε την κράτηση.",
|
||||
"event_awaiting_approval": "Ένα γεγονός περιμένει την έγκρισή σας",
|
||||
"event_type": "Τύπος Συμβάντος",
|
||||
"meeting_password": "Συνθηματικό Συνάντησης",
|
||||
"meeting_url": "URL Συνάντησης",
|
||||
"meeting_request_rejected": "Το αίτημα συνάντησής σας απορρίφθηκε",
|
||||
"hi": "Γεια",
|
||||
"manage_this_team": "Διαχείριση αυτής της ομάδας",
|
||||
"team_info": "Πληροφορίες Ομάδας",
|
||||
"hidden_team_member_title": "Είστε κρυμμένοι σε αυτήν την ομάδα",
|
||||
"edit_webhook": "Επεξεργασία Webhook",
|
||||
"delete_webhook": "Διαγραφή Webhook",
|
||||
"webhook_enabled": "Ενεργοποιημένο Webhook",
|
||||
"webhook_disabled": "Απενεργοποιημένο Webhook",
|
||||
"webhook_response": "Απάντηση Webhook",
|
||||
"webhook_test": "Έλεγχος Webhook",
|
||||
"webhook_created_successfully": "Το Webhook δημιουργήθηκε επιτυχώς!",
|
||||
"webhook_updated_successfully": "Το Webhook ενημερώθηκε επιτυχώς!",
|
||||
"webhook_removed_successfully": "Το Webhook αφαιρέθηκε επιτυχώς!",
|
||||
"dismiss": "Παράβλεψη",
|
||||
"no_data_yet": "Δεν υπάρχουν δεδομένα ακόμη",
|
||||
"upcoming": "Επερχόμενα",
|
||||
"recurring": "Επαναλαμβανόμενα",
|
||||
"past": "Παρελθοντικά",
|
||||
"choose_a_file": "Επιλογή αρχείου...",
|
||||
"upload_image": "Μεταφόρτωση εικόνας",
|
||||
"username": "Όνομα χρήστη",
|
||||
"is_still_available": "είναι ακόμα διαθέσιμο.",
|
||||
"documentation": "Τεκμηρίωση",
|
||||
"blog": "Ιστολόγιο",
|
||||
"blog_description": "Διαβάστε τα τελευταία μας νέα και άρθρα",
|
||||
"popular_pages": "Δημοφιλείς σελίδες",
|
||||
"register_now": "Εγγραφή τώρα",
|
||||
"register": "Εγγραφή",
|
||||
"page_doesnt_exist": "Η σελίδα δεν υπάρχει.",
|
||||
"check_spelling_mistakes_or_go_back": "Έλεγχος για ορθογραφικά λάθη ή επιστροφή στην προηγούμενη σελίδα.",
|
||||
"404_page_not_found": "404: Η σελίδα δεν βρέθηκε.",
|
||||
"getting_started": "Ξεκινήστε",
|
||||
"already_have_an_account": "Έχετε ήδη λογαριασμό;",
|
||||
"create_account": "Δημιουργία Λογαριασμού",
|
||||
"confirm_password": "Επιβεβαίωση κωδικού πρόσβασης",
|
||||
"create_your_account": "Δημιουργία λογαριασμού",
|
||||
"sign_up": "Εγγραφή",
|
||||
"youve_been_logged_out": "Έχετε αποσυνδεθεί",
|
||||
"hope_to_see_you_soon": "Ελπίζουμε να σας ξαναδούμε σύντομα!",
|
||||
"please_try_again_and_contact_us": "Παρακαλούμε δοκιμάστε ξανά και επικοινωνήστε μαζί μας αν το πρόβλημα παραμένει.",
|
||||
"no_account_exists": "Δεν υπάρχει λογαριασμός που να ταιριάζει με τη διεύθυνση email.",
|
||||
"2fa_enter_six_digit_code": "Εισάγετε τον εξαψήφιο κωδικό από την εφαρμογή ελέγχου ταυτότητας παρακάτω.",
|
||||
"create_an_account": "Δημιουργία λογαριασμού",
|
||||
"dont_have_an_account": "Δεν έχετε λογαριασμό;",
|
||||
"sign_in_account": "Συνδεθείτε στο λογαριασμό σας",
|
||||
"sign_in": "Είσοδος",
|
||||
"connect": "Σύνδεση",
|
||||
"try_for_free": "Δοκιμάστε δωρεάν",
|
||||
"add_to_calendar": "Προσθήκη στο ημερολόγιο",
|
||||
"add_another_calendar": "Προσθήκη άλλου ημερολογίου",
|
||||
"meeting_is_scheduled": "Η συνάντηση έχει προγραμματιστεί",
|
||||
"submitted": "Η κράτησή σας έχει υποβληθεί",
|
||||
"reset_password": "Επαναφορά Κωδικού Πρόσβασης",
|
||||
"change_your_password": "Αλλαγή κωδικού πρόσβασης",
|
||||
"show_password": "Εμφάνιση κωδικού πρόσβασης",
|
||||
"hide_password": "Απόκρυψη κωδικού πρόσβασης",
|
||||
"try_again": "Δοκιμάστε ξανά",
|
||||
"sunday_time_error": "Μη έγκυρη ώρα την Κυριακή",
|
||||
"monday_time_error": "Μη έγκυρη ώρα τη Δευτέρα",
|
||||
"tuesday_time_error": "Μη έγκυρη ώρα την Τρίτη",
|
||||
"wednesday_time_error": "Μη έγκυρη ώρα την Τετάρτη",
|
||||
"thursday_time_error": "Μη έγκυρη ώρα την Πέμπτη",
|
||||
"friday_time_error": "Μη έγκυρη ώρα την Παρασκευή",
|
||||
"saturday_time_error": "Μη έγκυρη ώρα το Σάββατο",
|
||||
"no_meeting_found": "Δε βρέθηκε συνάντηση",
|
||||
"bookings": "Κρατήσεις"
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
"second_other": "{{count}} seconds",
|
||||
"upgrade_now": "Upgrade now",
|
||||
"accept_invitation": "Accept Invitation",
|
||||
"calcom_explained": "{{appName}} is the open source Calendly alternative putting you in control of your own data, workflow and appearance.",
|
||||
"calcom_explained": "{{appName}} provides scheduling infrastructure for absolutely everyone.",
|
||||
"have_any_questions": "Have questions? We're here to help.",
|
||||
"reset_password_subject": "{{appName}}: Reset password instructions",
|
||||
"event_declined_subject": "Declined: {{eventType}} with {{name}} at {{date}}",
|
||||
|
@ -303,6 +303,7 @@
|
|||
"cancelling_all_recurring": "These are all remaining instances in the recurring event.",
|
||||
"error_with_status_code_occured": "An error with status code {{status}} occurred.",
|
||||
"booking_already_cancelled": "This booking was already cancelled",
|
||||
"booking_already_accepted_rejected": "This booking was already accepted or rejected",
|
||||
"go_back_home": "Go back home",
|
||||
"or_go_back_home": "Or go back home",
|
||||
"no_availability": "Unavailable",
|
||||
|
@ -385,7 +386,8 @@
|
|||
"go_to_billing_portal": "Go to the billing portal",
|
||||
"need_anything_else": "Need anything else?",
|
||||
"further_billing_help": "If you need any further help with billing, our support team are here to help.",
|
||||
"contact_our_support_team": "Contact our support team",
|
||||
"contact": "Contact",
|
||||
"our_support_team": "our support team",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "This user hasn't set up any event types yet.",
|
||||
"edit_logo": "Edit logo",
|
||||
|
@ -918,7 +920,7 @@
|
|||
"allow_booker_to_select_duration": "Allow booker to select duration",
|
||||
"impersonate_user_tip": "All uses of this feature is audited.",
|
||||
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
||||
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
|
||||
"impersonating_stop_instructions": "Click here to stop",
|
||||
"event_location_changed": "Updated - Your event changed the location",
|
||||
"location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}",
|
||||
"current_location": "Current Location",
|
||||
|
@ -1326,6 +1328,8 @@
|
|||
"booking_confirmation_success": "Booking confirmation succeeded",
|
||||
"booking_rejection_success": "Booking rejection succeeded",
|
||||
"booking_confirmation_fail": "Booking confirmation failed",
|
||||
"booking_tentative": "This booking is tentative",
|
||||
"booking_accept_intent": "Oops, I want to accept",
|
||||
"we_wont_show_again": "We won't show this again",
|
||||
"couldnt_update_timezone": "We couldn't update the timezone",
|
||||
"updated_timezone_to": "Updated timezone to {{formattedCurrentTz}}",
|
||||
|
@ -1427,7 +1431,7 @@
|
|||
"disable_app": "Disable App",
|
||||
"disable_app_description": "Disabling this app could cause problems with how your users interact with Cal",
|
||||
"edit_keys": "Edit Keys",
|
||||
"apps_description": "Enable apps for your instance of Cal",
|
||||
"admin_apps_description": "Enable apps for your instance of Cal",
|
||||
"no_available_apps": "There are no available apps",
|
||||
"no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'",
|
||||
"no_apps": "There are no apps enabled in this instance of Cal",
|
||||
|
@ -1438,8 +1442,27 @@
|
|||
"add_an_option": "Add an option",
|
||||
"radio": "Radio",
|
||||
"individual":"Individual",
|
||||
"all_bookings_filter_label":"All Bookings",
|
||||
"date_overrides": "Date overrides",
|
||||
"date_overrides_subtitle": "Add dates when your availability changes from your daily hours.",
|
||||
"date_overrides_info": "Date overrides are archived automatically after the date has passed",
|
||||
"date_overrides_dialog_which_hours": "Which hours are you free?",
|
||||
"date_overrides_dialog_which_hours_unavailable": "Which hours are you busy?",
|
||||
"date_overrides_dialog_title": "Select the dates to override",
|
||||
"date_overrides_unavailable": "Unavailable all day",
|
||||
"date_overrides_mark_all_day_unavailable_one": "Mark unavailable (All day)",
|
||||
"date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates",
|
||||
"date_overrides_add_btn": "Add Override",
|
||||
"date_overrides_update_btn": "Update Override",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||
"set_as_default": "Set as default",
|
||||
"hide_eventtype_details": "Hide EventType Details",
|
||||
"all_bookings_filter_label":"All Bookings",
|
||||
"verification_code_sent": "Verification code sent",
|
||||
"verified_successfully": "Verified successfully",
|
||||
"wrong_code": "Wong verification code",
|
||||
"not_verified": "Not yet verified",
|
||||
"no_availability_in_month": "No availability in {{month}}",
|
||||
"view_next_month": "View next month",
|
||||
"send_code" : "Send code",
|
||||
"number_verified": "Number Verified"
|
||||
}
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "Ir al portal de facturación",
|
||||
"need_anything_else": "¿Necesita algo más?",
|
||||
"further_billing_help": "Si necesita más ayuda con la facturación, nuestro equipo de soporte está aquí para ayudarlo.",
|
||||
"contact_our_support_team": "Contacta con nuestro equipo de soporte",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "Este usuario aún no ha configurado ningún tipo de evento.",
|
||||
"edit_logo": "Cambiar la marca",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"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_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 types d'événements.",
|
||||
"edit_logo": "Modifier le logo",
|
||||
|
@ -562,6 +561,8 @@
|
|||
"duration": "Durée",
|
||||
"available_durations": "Durées disponibles",
|
||||
"default_duration": "Durée par défaut",
|
||||
"default_duration_no_options": "Veuillez d'abord choisir les durées disponibles",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"minutes": "Minutes",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Faites tourner les réunions entre plusieurs membres de l'équipe.",
|
||||
|
@ -627,6 +628,7 @@
|
|||
"teams": "Équipes",
|
||||
"team": "Équipe",
|
||||
"team_billing": "Facturation d'équipe",
|
||||
"team_billing_description": "Gérer la facturation pour votre équipe",
|
||||
"upgrade_to_flexible_pro_title": "Nous avons modifié la facturation pour les équipes",
|
||||
"upgrade_to_flexible_pro_message": "Des membres dans votre équipe n'ont pas de place. Mettez à niveau votre offre pro pour couvrir les places manquantes.",
|
||||
"changed_team_billing_info": "Depuis janvier 2022, nous facturons chaque place aux membres de l'équipe. Les membres de votre équipe qui ont eu la version Pro gratuitement disposent maintenant d'un essai de 14 jours. Une fois leur période d'essai expirée, ces membres seront cachés pour votre équipe, sauf si vous mettez à niveau maintenant.",
|
||||
|
@ -702,6 +704,7 @@
|
|||
"hide_event_type": "Masquer le type d'événement",
|
||||
"edit_location": "Modifier le lieu",
|
||||
"into_the_future": "dans le futur",
|
||||
"when_booked_with_less_than_notice": "Si réservé avec moins de <time></time> de préavis",
|
||||
"within_date_range": "Dans une plage de dates",
|
||||
"indefinitely_into_future": "Sans doute dans le futur",
|
||||
"add_new_custom_input_field": "Ajouter un nouveau champ de saisie personnalisé",
|
||||
|
@ -721,6 +724,7 @@
|
|||
"delete_account_confirmation_message": "Êtes-vous sûr de vouloir supprimer votre compte {{appName}} ? Toute personne avec qui vous avez partagé le lien de votre compte ne pourra plus réserver en utilisant ce lien et toutes les préférences que vous avez enregistrées seront perdues.",
|
||||
"integrations": "Intégrations",
|
||||
"apps": "Applications",
|
||||
"apps_listing": "Liste des applications",
|
||||
"category_apps": "Applications {{category}}",
|
||||
"app_store": "App Store",
|
||||
"app_store_description": "Connecter les personnes, la technologie et l'espace de travail.",
|
||||
|
@ -744,6 +748,7 @@
|
|||
"toggle_calendars_conflict": "Activer/désactiver les calendriers pour lesquels vous souhaiter vérifier les conflits afin d'éviter les doubles réservations.",
|
||||
"select_destination_calendar": "Créer des événements le",
|
||||
"connect_additional_calendar": "Connecter un calendrier supplémentaire",
|
||||
"calendar_updated_successfully": "Calendrier mis à jour avec succès",
|
||||
"conferencing": "Conférence",
|
||||
"calendar": "Calendrier",
|
||||
"payments": "Paiements",
|
||||
|
@ -776,6 +781,7 @@
|
|||
"trending_apps": "Applications populaires",
|
||||
"explore_apps": "{{category}} applications",
|
||||
"installed_apps": "Applications installées",
|
||||
"free_to_use_apps": "Gratuit",
|
||||
"no_category_apps": "Aucune application {{category}}",
|
||||
"no_category_apps_description_calendar": "Ajouter une application de calendrier pour vérifier les conflits et éviter les doubles réservations",
|
||||
"no_category_apps_description_conferencing": "Essayez d'ajouter une application de conférence pour interconnecter les appels vidéo avec vos clients",
|
||||
|
@ -814,6 +820,8 @@
|
|||
"verify_wallet": "Vérifier le portefeuille",
|
||||
"connect_metamask": "Connecter Metamask",
|
||||
"create_events_on": "Créer des événements le :",
|
||||
"enterprise_license": "Il s'agit d'une fonctionnalité d'entreprise",
|
||||
"enterprise_license_description": "Pour activer cette fonctionnalité, obtenez une clé de déploiement sur la console {{consoleUrl}} et ajoutez-la à votre .env en tant que CALCOM_LICENSE_KEY. Si votre équipe a déjà une licence, veuillez contacter {{supportMail}} pour obtenir de l'aide.",
|
||||
"missing_license": "Licence manquante",
|
||||
"signup_requires": "Licence commerciale requise",
|
||||
"signup_requires_description": "{{companyName}} ne propose pas actuellement de version open source gratuite de la page d'inscription. Pour obtenir un accès complet aux composants d'inscription, vous devez acquérir une licence commerciale. Pour une utilisation personnelle, nous recommandons la Plateforme de Données Prisma ou toute autre interface Postgres pour créer des comptes.",
|
||||
|
@ -905,6 +913,7 @@
|
|||
"user_impersonation_heading": "Connexion en tant qu'autre utilisateur",
|
||||
"user_impersonation_description": "Permet à notre équipe d'assistance de se connecter temporairement lorsque vous nous aidez à résoudre rapidement tous les problèmes que vous nous signalez.",
|
||||
"team_impersonation_description": "Permet aux administrateurs de votre équipe de se connecter temporairement en tant que vous-même.",
|
||||
"allow_booker_to_select_duration": "Autoriser l'organisateur à sélectionner la durée",
|
||||
"impersonate_user_tip": "Toutes les utilisations de cette fonctionnalité sont vérifiées.",
|
||||
"impersonating_user_warning": "Identification du nom d'utilisateur \"{{user}}\".",
|
||||
"impersonating_stop_instructions": "<0>Cliquez ici pour arrêter</0>.",
|
||||
|
@ -1022,6 +1031,9 @@
|
|||
"error_removing_app": "Erreur lors de la suppression de l'application",
|
||||
"web_conference": "Conférence en ligne",
|
||||
"requires_confirmation": "Nécessite une confirmation",
|
||||
"always_requires_confirmation": "Toujours",
|
||||
"requires_confirmation_threshold": "Nécessite une confirmation si réservé avec un préavis de < {{time}} $t({{unit}}_timeUnit)",
|
||||
"may_require_confirmation": "Peut nécessiter une confirmation",
|
||||
"nr_event_type_one": "{{count}} type d'événement",
|
||||
"nr_event_type_other": "{{count}} types d'événements",
|
||||
"add_action": "Ajouter une action",
|
||||
|
@ -1109,6 +1121,9 @@
|
|||
"event_limit_tab_description": "Fréquence de réservation",
|
||||
"event_advanced_tab_description": "Paramètres du calendrier & plus...",
|
||||
"event_advanced_tab_title": "Avancé",
|
||||
"event_setup_multiple_duration_error": "Configuration de l'événement : plusieurs durées requièrent au moins une option.",
|
||||
"event_setup_multiple_duration_default_error": "Configuration de l'événement: veuillez sélectionner une durée par défaut valide.",
|
||||
"event_setup_booking_limits_error": "Les limites de réservation doivent être en ordre croissant. [jour,semaine,mois,année]",
|
||||
"select_which_cal": "Sélectionnez le calendrier auquel ajouter des réservations",
|
||||
"custom_event_name": "Nom de l'événement personnalisé",
|
||||
"custom_event_name_description": "Créer des noms d'événements personnalisés à afficher sur l'événement du calendrier",
|
||||
|
@ -1162,8 +1177,11 @@
|
|||
"invoices": "Factures",
|
||||
"embeds": "Intègre",
|
||||
"impersonation": "Identification",
|
||||
"impersonation_description": "Paramètres de gestion de l'identité de l'utilisateur",
|
||||
"users": "Utilisateurs",
|
||||
"profile_description": "Gérer les paramètres de votre profil {{appName}}",
|
||||
"users_description": "Vous trouverez ici une liste de tous les utilisateurs",
|
||||
"users_listing": "Liste des utilisateurs",
|
||||
"general_description": "Gérer les paramètres pour votre langue et votre fuseau horaire",
|
||||
"calendars_description": "Configurez la manière dont vos types d'événements interagissent avec vos calendriers",
|
||||
"appearance_description": "Gérer les paramètres pour votre apparence de réservation",
|
||||
|
@ -1368,6 +1386,7 @@
|
|||
"number_sms_notifications": "Numéro de téléphone (notifications SMS)",
|
||||
"attendee_email_workflow": "E-mail du participant",
|
||||
"attendee_email_info": "E-mail de la personne ayant réservé",
|
||||
"kbar_search_placeholder": "Saisissez une commande ou une recherche...",
|
||||
"invalid_credential": "Oh non ! L'autorisation semble avoir expiré ou avoir été révoquée. Veuillez la réinstaller.",
|
||||
"choose_common_schedule_team_event": "Choisissez un horaire commun",
|
||||
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser un horaire commun entre les hôtes. Si désactivée, chaque hôte sera réservé en fonction de son planning par défaut.",
|
||||
|
@ -1378,5 +1397,8 @@
|
|||
"test_preview": "Tester l'aperçu",
|
||||
"route_to": "Router vers",
|
||||
"test_preview_description": "Tester votre formulaire de routage sans envoyer de données",
|
||||
"test_routing": "Tester le routage"
|
||||
"test_routing": "Tester le routage",
|
||||
"payment_app_disabled": "Un administrateur a désactivé une application de paiement",
|
||||
"edit_event_type": "Modifier le type d'événement",
|
||||
"admin_apps_description": "Activer les applications pour votre instance de Cal"
|
||||
}
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "מעבר אל פורטל החיובים",
|
||||
"need_anything_else": "צריך משהו נוסף?",
|
||||
"further_billing_help": "אם דרוש לך סיוע נוסף בענייני חיוב, צוות התמיכה שלנו כאן כדי לעזור.",
|
||||
"contact_our_support_team": "יצירת קשר עם צוות התמיכה",
|
||||
"uh_oh": "אוי, לא!",
|
||||
"no_event_types_have_been_setup": "משתמש זה עדיין לא הגדיר סוג אירוע.",
|
||||
"edit_logo": "עריכת לוגו",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "Vai al portale di fatturazione",
|
||||
"need_anything_else": "Hai bisogno di altro?",
|
||||
"further_billing_help": "Se hai bisogno di ulteriore aiuto per la fatturazione, il nostro team di supporto è qui per aiutarti.",
|
||||
"contact_our_support_team": "Contatta il nostro team di supporto",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "Questo utente non ha ancora impostato alcun tipo di evento.",
|
||||
"edit_logo": "Modifica logo",
|
||||
|
@ -1404,5 +1403,40 @@
|
|||
"edit_event_type": "Modifica tipo di evento",
|
||||
"collective_scheduling": "Pianificazione collettiva",
|
||||
"make_it_easy_to_book": "Facilita la prenotazione del tuo team quando tutti sono disponibili.",
|
||||
"find_the_best_person": "Trova la persona più adatta disponibile e passa tra i membri del team."
|
||||
"find_the_best_person": "Trova la persona più adatta disponibile e passa tra i membri del team.",
|
||||
"fixed_round_robin": "Round robin fisso",
|
||||
"add_one_fixed_attendee": "Aggiungi un partecipante fisso e fai intervenire a turno altri partecipanti.",
|
||||
"calcom_is_better_with_team": "Cal.com funziona meglio in team",
|
||||
"add_your_team_members": "Aggiungi i membri del tuo team ai tuoi tipi di eventi. Usa la pianificazione collettiva per includere tutti o trova la persona più adatta con la pianificazione round robin.",
|
||||
"booking_limit_reached": "È stato raggiunto il limite di prenotazione per questo tipo di evento",
|
||||
"admin_has_disabled": "Un amministratore ha disabilitato {{appName}}",
|
||||
"disabled_app_affects_event_type": "Un amministratore ha disabilitato {{appName}}, il che influenza il tuo tipo di evento {{eventType}}",
|
||||
"disable_payment_app": "L'amministratore ha disabilitato {{appName}}, il che influenza il tuo tipo di evento {{title}}. I partecipanti sono ancora in grado di prenotare questo tipo di evento, ma non gli verrà richiesto di pagare. Per evitarlo, è possibile nascondere questo tipo di evento fino a che l'amministratore non riattivi il tuo metodo di pagamento.",
|
||||
"payment_disabled_still_able_to_book": "I partecipanti sono ancora in grado di prenotare questo tipo di evento, ma non gli verrà richiesto di pagare. Per evitarlo, è possibile nascondere questo tipo di evento fino a che l'amministratore non riattivi il tuo metodo di pagamento.",
|
||||
"app_disabled_with_event_type": "L'amministratore ha disabilitato {{appName}}, il che influenza il tuo tipo di evento {{title}}.",
|
||||
"app_disabled_video": "L'amministratore ha disabilitato {{appName}}, il che potrebbe influenzare i tuoi tipi di eventi. Se hai dei tipi di eventi che utilizzano {{appName}} come luogo, allora verrà usato Cal Video come luogo predefinito.",
|
||||
"app_disabled_subject": "{{appName}} è stato disabilitato",
|
||||
"navigate_installed_apps": "Vai alle app installate",
|
||||
"disabled_calendar": "Se si dispone di un altro calendario installato, le nuove prenotazioni saranno aggiunte a quel calendario. Se così non fosse, connettere un nuovo calendario in modo da non perdere nessuna nuova prenotazione.",
|
||||
"enable_apps": "Abilita app",
|
||||
"enable_apps_description": "Abilita le app che gli utenti possono integrare con Cal.com",
|
||||
"app_is_enabled": "{{appName}} è abilitato",
|
||||
"app_is_disabled": "{{appName}} è disabilitato",
|
||||
"keys_have_been_saved": "Le chiavi sono state salvate",
|
||||
"disable_app": "Disabilita app",
|
||||
"disable_app_description": "La disattivazione di questa app potrebbe causare problemi con il modo in cui i tuoi utenti interagiscono con Cal",
|
||||
"edit_keys": "Modifica chiavi",
|
||||
"admin_apps_description": "Abilita le app per la tua istanza di Cal",
|
||||
"no_available_apps": "Nessuna app disponibile",
|
||||
"no_available_apps_description": "Assicurarsi che ci siano delle applicazioni nella propria distribuzione in 'packages/app-store'",
|
||||
"no_apps": "Non ci sono applicazioni abilitate in questa istanza di Cal",
|
||||
"apps_settings": "Impostazioni delle app",
|
||||
"fill_this_field": "Compilare il campo",
|
||||
"options": "Opzioni",
|
||||
"enter_option": "Immetti opzione {{index}}",
|
||||
"add_an_option": "Aggiungi un'opzione",
|
||||
"radio": "Radio",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copia",
|
||||
"set_as_default": "Imposta come predefinito",
|
||||
"hide_eventtype_details": "Nascondi dettagli tipo di evento"
|
||||
}
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "請求ポータルに移動",
|
||||
"need_anything_else": "他に必要なものはありませんか?",
|
||||
"further_billing_help": "請求に関するサポートが必要な場合は、サポートチームがお手伝いします。",
|
||||
"contact_our_support_team": "サポートチームに問い合わせる",
|
||||
"uh_oh": "おっと!",
|
||||
"no_event_types_have_been_setup": "このユーザーはまだイベントタイプを設定していません。",
|
||||
"edit_logo": "ロゴを編集",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "결제 포털로 이동",
|
||||
"need_anything_else": "다른 것이 필요하십니까?",
|
||||
"further_billing_help": "청구와 관련하여 추가 도움이 필요한 경우 지원 팀이 도와드리겠습니다.",
|
||||
"contact_our_support_team": "지원팀에 문의하기",
|
||||
"uh_oh": "오!",
|
||||
"no_event_types_have_been_setup": "사용자가 이벤트 타입을 설정하지 않았습니다.",
|
||||
"edit_logo": "로고 수정하기",
|
||||
|
@ -1427,6 +1426,7 @@
|
|||
"disable_app": "앱 비활성화",
|
||||
"disable_app_description": "이 앱을 비활성화하면 사용자가 Cal과 상호 작용하는 방식에 문제가 발생할 수 있습니다",
|
||||
"edit_keys": "키 편집",
|
||||
"admin_apps_description": "Cal 인스턴스용 앱 활성화",
|
||||
"no_available_apps": "사용 가능한 앱이 없습니다",
|
||||
"no_available_apps_description": "'packages/app-store' 아래 배포에 앱이 있는지 확인하세요",
|
||||
"no_apps": "Cal의 이 인스턴스에서 활성화된 앱이 없습니다",
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "Ga naar het facturatiepaneel",
|
||||
"need_anything_else": "Nog iets nodig?",
|
||||
"further_billing_help": "Als u verdere hulp nodig heeft bij facturering, is ons ondersteuningsteam er om u te helpen.",
|
||||
"contact_our_support_team": "Neem contact op met onze klantenservice",
|
||||
"uh_oh": "Oeps!",
|
||||
"no_event_types_have_been_setup": "Deze gebruiker heeft nog geen evenementen.",
|
||||
"edit_logo": "Bewerk logo",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "Gå til betalingsportalen",
|
||||
"need_anything_else": "Trenger du noe annet?",
|
||||
"further_billing_help": "Hvis du trenger ytterligere hjelp med betaling, er supportteamet vårt her for å hjelpe.",
|
||||
"contact_our_support_team": "Kontakt supportteamet vårt",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "Denne brukeren har ikke konfigurert noen hendelsestyper ennå.",
|
||||
"edit_logo": "Rediger logo",
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "Przejdź do portalu rozliczeniowego",
|
||||
"need_anything_else": "Potrzebujesz czegoś innego?",
|
||||
"further_billing_help": "Jeśli potrzebujesz dalszej pomocy przy rozliczaniu, nasz zespół wsparcia jest tutaj, aby pomóc.",
|
||||
"contact_our_support_team": "Skontaktuj się z naszym zespołem wsparcia",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "Ten użytkownik nie ustawił jeszcze żadnych typów wydarzeń.",
|
||||
"edit_logo": "Edytuj logo",
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "Ir para o portal de pagamento",
|
||||
"need_anything_else": "Precisa de algo mais?",
|
||||
"further_billing_help": "Se precisar de mais ajuda com o faturamento, o nosso time de suporte está aqui para ajudar.",
|
||||
"contact_our_support_team": "Fale com a nossa equipe de suporte",
|
||||
"uh_oh": "Oh oh!",
|
||||
"no_event_types_have_been_setup": "Este usuário ainda não definiu nenhum tipo de evento.",
|
||||
"edit_logo": "Editar logotipo",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "Ir para o portal de pagamento",
|
||||
"need_anything_else": "Precisa de algo mais?",
|
||||
"further_billing_help": "Se precisar de mais ajuda com a faturação, a nossa equipa de suporte está aqui para ajudar.",
|
||||
"contact_our_support_team": "Contacte a nossa equipa de suporte",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "Este utilizador ainda não configurou nenhum tipo de evento.",
|
||||
"edit_logo": "Editar logótipo",
|
||||
|
@ -1427,6 +1426,7 @@
|
|||
"disable_app": "Desativar a aplicação",
|
||||
"disable_app_description": "A desativação desta aplicação pode causar problemas na forma como os seus utilizadores interagem com o Cal",
|
||||
"edit_keys": "Editar chaves",
|
||||
"admin_apps_description": "Ativar aplicações para a sua instância do Cal",
|
||||
"no_available_apps": "Não existem aplicações disponíveis",
|
||||
"no_available_apps_description": "Por favor, certifique-se que existem aplicações na sua instalação em 'packages/app-store'",
|
||||
"no_apps": "Não existem aplicações ativas nesta instância do Cal",
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "Accesați portalul de facturare",
|
||||
"need_anything_else": "Aveți nevoie de altceva?",
|
||||
"further_billing_help": "Dacă ai nevoie de ajutor suplimentar pentru facturare, echipa noastră de asistență este aici pentru a te ajuta.",
|
||||
"contact_our_support_team": "Contactează echipa noastră de suport",
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "Acest utilizator nu a configurat încă niciun tip de eveniment.",
|
||||
"edit_logo": "Editare logo",
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "Перейти на платёжный портал",
|
||||
"need_anything_else": "Нужно что-нибудь еще?",
|
||||
"further_billing_help": "Если вам нужна дальнейшая помощь с оплатой счета, наша служба поддержки готова помочь.",
|
||||
"contact_our_support_team": "Свяжитесь с нашей службой поддержки",
|
||||
"uh_oh": "О нет!",
|
||||
"no_event_types_have_been_setup": "Этот пользователь еще не создал ни одного события.",
|
||||
"edit_logo": "Изменить логотип",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "Idite na portal naplate",
|
||||
"need_anything_else": "Treba li vam još nešto?",
|
||||
"further_billing_help": "Ako vam treba pomoć sa naplatom, naš tim podrške je tu da pomogne.",
|
||||
"contact_our_support_team": "Kontaktirjate naš tim podrške",
|
||||
"uh_oh": "O ne!",
|
||||
"no_event_types_have_been_setup": "Ovaj korisnik još nije podesio tipove događaja.",
|
||||
"edit_logo": "Uredi logo",
|
||||
|
@ -1427,6 +1426,7 @@
|
|||
"disable_app": "Onemogući aplikaciju",
|
||||
"disable_app_description": "Onemogućavanje ove aplikacije može da stvori probleme u komunikaciji vaših korisnika sa Cal-om",
|
||||
"edit_keys": "Izmeni ključeve",
|
||||
"admin_apps_description": "Omogućite aplikacije za vašu instancu Cal-a",
|
||||
"no_available_apps": "Nema dostupnih aplikacija",
|
||||
"no_available_apps_description": "Uverite se da ima aplikacija u vašoj fascikli za instalaciju pod „packages/app-store“",
|
||||
"no_apps": "Nema omogućenih aplikacija u ovoj instanci Cal-a",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "Gå till faktureringsportalen",
|
||||
"need_anything_else": "Behöver du något annat?",
|
||||
"further_billing_help": "Om du behöver ytterligare hjälp med fakturering finns vårt supportteam här för att hjälpa till.",
|
||||
"contact_our_support_team": "Kontakta vårt kundsupportteam",
|
||||
"uh_oh": "Åh nej!",
|
||||
"no_event_types_have_been_setup": "Den här användaren har inte skapat några händelsetyper ännu.",
|
||||
"edit_logo": "Ändra logotyp",
|
||||
|
@ -1427,6 +1426,7 @@
|
|||
"disable_app": "Inaktivera app",
|
||||
"disable_app_description": "Inaktivering av den här appen kan orsaka problem med hur dina användare interagerar med Cal",
|
||||
"edit_keys": "Redigera nycklar",
|
||||
"admin_apps_description": "Aktivera appar för din Cal-version",
|
||||
"no_available_apps": "Det finns inga tillgängliga appar",
|
||||
"no_available_apps_description": "Se till att det finns appar i din distribution under \"paket/app-store\"",
|
||||
"no_apps": "Det finns inga appar aktiverade i denna instans av Cal",
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "Fatura portalına git",
|
||||
"need_anything_else": "Başka bir şeye ihtiyacınız var mı?",
|
||||
"further_billing_help": "Faturalandırma konusunda daha fazla yardıma ihtiyacınız olursa destek ekibimiz size yardımcı olmaya hazır.",
|
||||
"contact_our_support_team": "Destek ekibimizle iletişime geçin",
|
||||
"uh_oh": "Hay aksi!",
|
||||
"no_event_types_have_been_setup": "Bu kullanıcı henüz etkinlik türü ayarlamadı.",
|
||||
"edit_logo": "Logoyu düzenle",
|
||||
|
@ -705,6 +704,7 @@
|
|||
"hide_event_type": "Etkinlik türünü gizle",
|
||||
"edit_location": "Konumu düzenle",
|
||||
"into_the_future": "gelecekte",
|
||||
"when_booked_with_less_than_notice": "<time></time> bildiriminden daha az süre ile rezervasyon yapıldığında",
|
||||
"within_date_range": "Bir tarih aralığında",
|
||||
"indefinitely_into_future": "Süresiz olarak gelecekte",
|
||||
"add_new_custom_input_field": "Yeni özel veri girdi alanı ekle",
|
||||
|
@ -1033,6 +1033,7 @@
|
|||
"web_conference": "Web konferansı",
|
||||
"requires_confirmation": "Onay gerekli",
|
||||
"always_requires_confirmation": "Her zaman",
|
||||
"requires_confirmation_threshold": "{{time}} $t({{unit}}_timeUnit) bildiriminden daha az süre ile rezervasyon yapıldığında onay gerekir",
|
||||
"may_require_confirmation": "Onay gerekebilir",
|
||||
"nr_event_type_one": "{{count}} etkinlik türü",
|
||||
"nr_event_type_other": "{{count}} etkinlik türü",
|
||||
|
@ -1401,11 +1402,19 @@
|
|||
"payment_app_disabled": "Bir yönetici, ödeme uygulamasını devre dışı bıraktı",
|
||||
"edit_event_type": "Etkinlik türünü düzenle",
|
||||
"collective_scheduling": "Toplu Planlama",
|
||||
"make_it_easy_to_book": "Herkes müsait olduğunda ekibinizin rezervasyon yapmasını kolaylaştırın.",
|
||||
"find_the_best_person": "Mevcut en iyi kişiyi bulun ve ekip üyeleriniz arasında rotasyon yapın.",
|
||||
"fixed_round_robin": "Sabit döngü",
|
||||
"add_one_fixed_attendee": "Tek bir sabit katılımcı ve birden fazla katılımcının olduğu bir döngü ekleyin.",
|
||||
"calcom_is_better_with_team": "Cal.com ekiplerle daha iyidir",
|
||||
"add_your_team_members": "Ekip üyelerinizi etkinlik türlerinize ekleyin. Herkesi eklemek için toplu planlamayı kullanın veya döngüsel planlama ile en uygun kişiyi bulun.",
|
||||
"booking_limit_reached": "Bu etkinlik türü için Rezervasyon Sınırına ulaşıldı",
|
||||
"admin_has_disabled": "Bir yönetici {{appName}} uygulamasını devre dışı bıraktı",
|
||||
"disabled_app_affects_event_type": "Bir yönetici {{eventType}} etkinlik türünüzü etkileyen {{appName}} uygulamasını devre dışı bıraktı",
|
||||
"disable_payment_app": "Yönetici, {{title}} etkinlik türünüzü etkileyebilecek {{appName}} uygulamasını devre dışı bıraktı. Katılımcılar yine de bu tür bir etkinlik için rezervasyon yaptırabilirler ancak herhangi bir ödeme yapmalarına gerek yoktur. Bu durumu önlemek için yöneticiniz ödeme yönteminizi yeniden etkinleştirene kadar etkinlik türünü gizleyebilirsiniz.",
|
||||
"payment_disabled_still_able_to_book": "Katılımcılar yine de bu tür bir etkinlik için rezervasyon yaptırabilirler ancak herhangi bir ödeme yapmalarına gerek yoktur. Bu durumu önlemek için yöneticiniz ödeme yönteminizi yeniden etkinleştirene kadar etkinlik türünü gizleyebilirsiniz.",
|
||||
"app_disabled_with_event_type": "Yönetici, etkinlik {{title}} türünüzü etkileyen {{appName}} uygulamasını devre dışı bıraktı.",
|
||||
"app_disabled_video": "Yönetici, etkinlik türlerinizi etkileyebilecek {{appName}} uygulamasını devre dışı bıraktı. Konum olarak {{appName}} olan etkinlik türleriniz varsa varsayılan uygulama Cal Video olacaktır.",
|
||||
"app_disabled_subject": "{{appName}} devre dışı bırakıldı",
|
||||
"navigate_installed_apps": "Yüklü uygulamalara git",
|
||||
"disabled_calendar": "Yüklü başka bir takviminiz varsa yeni randevular bu takvime eklenecektir. Aksi takdirde yeni bir takvim bağlamazsanız yeni rezervasyonları kaçırabilirsiniz.",
|
||||
|
@ -1415,13 +1424,18 @@
|
|||
"app_is_disabled": "{{appName}} devre dışı bırakıldı",
|
||||
"keys_have_been_saved": "Anahtarlar kaydedildi",
|
||||
"disable_app": "Uygulamaları Devre Dışı Bırak",
|
||||
"disable_app_description": "Bu uygulamanın devre dışı bırakılması kullanıcılarınızın Cal ile etkileşimde bulunmaları konusunda sorunlara neden olabilir",
|
||||
"edit_keys": "Anahtarları Düzenle",
|
||||
"admin_apps_description": "Cal örneğiniz için uygulamayı etkinleştirin",
|
||||
"no_available_apps": "Kullanılabilir uygulama yok",
|
||||
"no_available_apps_description": "'Paketler/uygulama mağazası' altındaki dağıtım klasörünüzde uygulamaların olduğundan emin olun",
|
||||
"no_apps": "Bu Cal örneğindeki hiçbir uygulama etkin değil",
|
||||
"apps_settings": "Uygulama ayarları",
|
||||
"fill_this_field": "Lütfen bu alanı doldurun",
|
||||
"options": "Seçenekler",
|
||||
"enter_option": "{{index}} Seçeneğini Girin",
|
||||
"add_an_option": "Bir seçenek ekle",
|
||||
"radio": "Radio",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-kopyala",
|
||||
"set_as_default": "Varsayılan olarak ayarla"
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"still_waiting_for_approval": "Захід досі чекає на схвалення",
|
||||
"event_is_still_waiting": "Запит на захід досі в очікуванні: {{attendeeName}} – {{date}} – {{eventType}}",
|
||||
"no_more_results": "Інших результатів немає",
|
||||
"no_results": "Немає результатів",
|
||||
"load_more_results": "Завантажити інші результати",
|
||||
"integration_meeting_id": "Ідентифікатор наради «{{integrationName}}»: {{meetingId}}",
|
||||
"confirmed_event_type_subject": "Підтверджено: {{eventType}} «{{name}}», {{date}}",
|
||||
|
@ -248,6 +249,7 @@
|
|||
"add_to_calendar": "Додати в календар",
|
||||
"add_another_calendar": "Додати інший календар",
|
||||
"other": "Інше",
|
||||
"email_sign_in_subject": "Ваше посилання для входу в {{appName}}",
|
||||
"emailed_you_and_attendees": "Ми надіслали вам та іншим учасникам запрошення до календаря з усіма деталями.",
|
||||
"emailed_you_and_attendees_recurring": "Ми надіслали вам та іншим учасникам запрошення з календаря на перший із цих періодичних заходів.",
|
||||
"emailed_you_and_any_other_attendees": "Цю інформацію надіслано вам і всім іншим учасникам.",
|
||||
|
@ -383,7 +385,6 @@
|
|||
"go_to_billing_portal": "Перейти на портал виставлення рахунків",
|
||||
"need_anything_else": "Потрібно щось інше?",
|
||||
"further_billing_help": "Якщо вам потрібна додаткова допомога з виставленням рахунків, наша служба підтримки готова її надати.",
|
||||
"contact_our_support_team": "Зв’язатися зі службою підтримки",
|
||||
"uh_oh": "Отакої!",
|
||||
"no_event_types_have_been_setup": "Цей користувач ще не налаштував жодних типів заходів.",
|
||||
"edit_logo": "Редагувати логотип",
|
||||
|
@ -420,6 +421,7 @@
|
|||
"current_incorrect_password": "Поточний пароль неправильний",
|
||||
"password_hint_caplow": "Комбінація великих і малих літер",
|
||||
"password_hint_min": "Щонайменше 8 символів завдовжки",
|
||||
"password_hint_admin_min": "Щонайменше 15 символів",
|
||||
"password_hint_num": "Містить принаймні 1 цифру",
|
||||
"invalid_password_hint": "Пароль має містити не менше ніж 7 символів: принаймні одну цифру та комбінацію великих і малих літер",
|
||||
"incorrect_password": "Пароль неправильний.",
|
||||
|
@ -463,11 +465,14 @@
|
|||
"booking_confirmation": "Підтвердьте свій захід ({{eventTypeTitle}}) із користувачем {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Перенесіть свій захід ({{eventTypeTitle}}) із користувачем {{profileName}}",
|
||||
"in_person_meeting": "Посилання або особиста зустріч",
|
||||
"attendeeInPerson": "Особисто (адреса відвідувача)",
|
||||
"inPerson": "Особисто (адреса організатора)",
|
||||
"link_meeting": "Нарада з посиланням",
|
||||
"phone_call": "Номер телефону учасника",
|
||||
"your_number": "Ваш номер телефону",
|
||||
"phone_number": "Номер телефону",
|
||||
"attendee_phone_number": "Номер телефону учасника",
|
||||
"organizer_phone_number": "Телефон організатора",
|
||||
"host_phone_number": "Ваш номер телефону",
|
||||
"enter_phone_number": "Введіть номер телефону",
|
||||
"reschedule": "Перенести",
|
||||
|
@ -554,6 +559,10 @@
|
|||
"collective": "Групова",
|
||||
"collective_description": "Плануйте наради, коли доступні всі вибрані учасники команди.",
|
||||
"duration": "Тривалість",
|
||||
"available_durations": "Доступні варіанти тривалості",
|
||||
"default_duration": "Тривалість за замовчуванням",
|
||||
"default_duration_no_options": "Спочатку виберіть доступні варіанти тривалості",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"minutes": "Хвилини",
|
||||
"round_robin": "Ротація",
|
||||
"round_robin_description": "Кілька учасників команди призначаються для нарад циклічно й по черзі.",
|
||||
|
@ -619,6 +628,7 @@
|
|||
"teams": "Команди",
|
||||
"team": "Команда",
|
||||
"team_billing": "Виставлення рахунків для команд",
|
||||
"team_billing_description": "Керуйте виставленням рахунків у своїй команді",
|
||||
"upgrade_to_flexible_pro_title": "Ми змінили умови оплати для команд",
|
||||
"upgrade_to_flexible_pro_message": "У вашій команді є учасники без придбаних місць. Перейдіть на план Pro, щоб отримати всі потрібні місця.",
|
||||
"changed_team_billing_info": "Станом на січень 2022 року оплата з учасників команди стягується за кількістю місць. Учасників вашої команди, які безкоштовно користувалися функціями Pro, тепер переведено на 14-денні пробні версії. Щойно пробний період завершиться, учасників вашої команди, для яких не придбано план Pro, буде приховано.",
|
||||
|
@ -694,6 +704,7 @@
|
|||
"hide_event_type": "Приховати тип заходу",
|
||||
"edit_location": "Змінити розташування",
|
||||
"into_the_future": "у майбутньому",
|
||||
"when_booked_with_less_than_notice": "Якщо бронювання створено менше ніж за <time></time> до заходу",
|
||||
"within_date_range": "У діапазоні дат",
|
||||
"indefinitely_into_future": "Колись у майбутньому",
|
||||
"add_new_custom_input_field": "Додати нове власне поле введення",
|
||||
|
@ -713,6 +724,7 @@
|
|||
"delete_account_confirmation_message": "Справді видалити обліковий запис {{appName}}? Усі, кому ви надавали посилання на свій обліковий запис, більше не зможуть бронювати ваш час за його допомогою. Усі збережені налаштування буде втрачено.",
|
||||
"integrations": "Інтеграції",
|
||||
"apps": "Додатки",
|
||||
"apps_listing": "Список додатків",
|
||||
"category_apps": "Додатки з категорії «{{category}}»",
|
||||
"app_store": "App Store",
|
||||
"app_store_description": "Спілкування та технології на робочому місці.",
|
||||
|
@ -736,6 +748,7 @@
|
|||
"toggle_calendars_conflict": "Увімкніть ті календарі, які потрібно перевірити на наявність конфліктів, щоб уникнути подвійних бронювань.",
|
||||
"select_destination_calendar": "Створюйте заходи в календарі",
|
||||
"connect_additional_calendar": "Підключити додатковий календар",
|
||||
"calendar_updated_successfully": "Календар оновлено",
|
||||
"conferencing": "Відеоконференції",
|
||||
"calendar": "Календар",
|
||||
"payments": "Платежі",
|
||||
|
@ -768,6 +781,7 @@
|
|||
"trending_apps": "Популярні додатки",
|
||||
"explore_apps": "Додатки з категорії «{{category}}»",
|
||||
"installed_apps": "Установлені додатки",
|
||||
"free_to_use_apps": "Безкоштовні",
|
||||
"no_category_apps": "{{category}} — немає додатків",
|
||||
"no_category_apps_description_calendar": "Додайте додаток для календаря, щоб перевіряти, чи немає конфліктів у розкладі, і уникати подвійних бронювань",
|
||||
"no_category_apps_description_conferencing": "Спробуйте додати додаток для конференцій, щоб інтегрувати можливість відеорозмов зі своїми клієнтами",
|
||||
|
@ -806,6 +820,8 @@
|
|||
"verify_wallet": "Пройдіть перевірку гаманця",
|
||||
"connect_metamask": "Підключіть Metamask",
|
||||
"create_events_on": "Створюйте заходи в календарі",
|
||||
"enterprise_license": "Це корпоративна функція",
|
||||
"enterprise_license_description": "Щоб увімкнути цю функцію, отримайте ключ розгортання в консолі {{consoleUrl}} і додайте його у свій файл .env як CALCOM_LICENSE_KEY. Якщо у вашої команди вже є ліцензія, зверніться по допомогу за адресою {{supportMail}}.",
|
||||
"missing_license": "Відсутня ліцензія",
|
||||
"signup_requires": "Потрібна комерційна ліцензія",
|
||||
"signup_requires_description": "{{companyName}} зараз не надає безкоштовну версію сторінки реєстрації з відкритим кодом. Щоб отримати повний доступ до складових функціоналу реєстрації, потрібно придбати комерційну ліцензію. Для особистого використання та створення облікових записів радимо Prisma Data Platform або будь-який інший інтерфейс Postgres.",
|
||||
|
@ -897,6 +913,7 @@
|
|||
"user_impersonation_heading": "Виконання ролі користувача",
|
||||
"user_impersonation_description": "Ви можете дозволити нашій команді підтримки тимчасово входити в систему під вашим іменем, щоб швидко вирішувати проблеми, про які ви повідомляєте.",
|
||||
"team_impersonation_description": "Дозвольте адміністраторам своєї команди тимчасово входити під вашим іменем.",
|
||||
"allow_booker_to_select_duration": "Дозволити автору бронювання вибирати тривалість",
|
||||
"impersonate_user_tip": "Усі випадки використання цієї функції підпадають під аудит.",
|
||||
"impersonating_user_warning": "Виконується роль користувача {{user}}.",
|
||||
"impersonating_stop_instructions": "<0>Натисніть тут, щоб зупинити</0>.",
|
||||
|
@ -1014,6 +1031,9 @@
|
|||
"error_removing_app": "Не вдалося вилучити додаток",
|
||||
"web_conference": "Вебконференція",
|
||||
"requires_confirmation": "Потрібне підтвердження",
|
||||
"always_requires_confirmation": "Завжди",
|
||||
"requires_confirmation_threshold": "Вимагати підтвердження, якщо бронювання створено менше ніж за {{time}} $t({{unit}}_timeUnit) до заходу",
|
||||
"may_require_confirmation": "Може вимагати підтвердження",
|
||||
"nr_event_type_one": "{{count}} тип заходу",
|
||||
"nr_event_type_other": "Типів заходів: {{count}}",
|
||||
"add_action": "Додати дію",
|
||||
|
@ -1101,6 +1121,9 @@
|
|||
"event_limit_tab_description": "Як часто ваш час можуть бронювати",
|
||||
"event_advanced_tab_description": "Налаштування календаря та інше…",
|
||||
"event_advanced_tab_title": "Додатково",
|
||||
"event_setup_multiple_duration_error": "Налаштування заходу: якщо мають бути доступні різні варіанти, потрібно вказати принаймні один.",
|
||||
"event_setup_multiple_duration_default_error": "Налаштування заходу: виберіть припустиму тривалість за замовчуванням.",
|
||||
"event_setup_booking_limits_error": "Ліміти на бронювання мають бути впорядковані за зростанням. [день,тиждень,місяць,рік]",
|
||||
"select_which_cal": "Виберіть календар, у який додаватимуться бронювання",
|
||||
"custom_event_name": "Користувацька назва події",
|
||||
"custom_event_name_description": "Вибирайте для заходів власні назви, що показуватимуться в календарі",
|
||||
|
@ -1154,8 +1177,11 @@
|
|||
"invoices": "Рахунки-фактури",
|
||||
"embeds": "Вставки",
|
||||
"impersonation": "Вхід під іншим іменем",
|
||||
"impersonation_description": "Налаштування входу під іншим іменем",
|
||||
"users": "Користувачі",
|
||||
"profile_description": "Керуйте налаштуваннями свого профілю {{appName}}",
|
||||
"users_description": "Тут наведено список усіх користувачів",
|
||||
"users_listing": "Список користувачів",
|
||||
"general_description": "Налаштуйте параметри мови й часового поясу",
|
||||
"calendars_description": "Налаштуйте, як типи заходів мають взаємодіяти з вашими календарями",
|
||||
"appearance_description": "Налаштуйте варіанти оформлення свого бронювання",
|
||||
|
@ -1360,6 +1386,7 @@
|
|||
"number_sms_notifications": "Номер телефону (SMS-сповіщення)",
|
||||
"attendee_email_workflow": "Адреса ел. пошти учасника",
|
||||
"attendee_email_info": "Адреса ел. пошти особи, яка бронює",
|
||||
"kbar_search_placeholder": "Введіть команду або пошуковий запис…",
|
||||
"invalid_credential": "Отакої! Схоже, дозвіл більше не дійсний або його відкликано. Перевстановіть додаток знову.",
|
||||
"choose_common_schedule_team_event": "Виберіть спільний розклад",
|
||||
"choose_common_schedule_team_event_description": "Увімкніть цей параметр, щоб використовувати спільний для двох ведучих розклад. Якщо цей параметр вимкнено, бронювання для кожного з ведучих відбуватиметься за їхніми власними графіками.",
|
||||
|
@ -1370,5 +1397,43 @@
|
|||
"test_preview": "Перевірити попередній перегляд",
|
||||
"route_to": "Кінцева точка",
|
||||
"test_preview_description": "Перевірте свою форму переспрямування без надсилання даних",
|
||||
"test_routing": "Перевірка переспрямування"
|
||||
"test_routing": "Перевірка переспрямування",
|
||||
"payment_app_disabled": "Додаток для оплати вимкнув адміністратор",
|
||||
"edit_event_type": "Редагувати тип заходу",
|
||||
"collective_scheduling": "Колективне планування",
|
||||
"make_it_easy_to_book": "Просте бронювання у випадках, коли всі члени вашої команди доступні.",
|
||||
"find_the_best_person": "Пошук найкращого доступного члена команди та циклічна ротація між ними.",
|
||||
"fixed_round_robin": "Фіксована циклічна ротація",
|
||||
"add_one_fixed_attendee": "Додавання одного фіксованого учасника та циклічна ротація між кількома учасниками.",
|
||||
"calcom_is_better_with_team": "З Cal.com краще працювати в командах",
|
||||
"add_your_team_members": "Додавайте членів своєї команди в типи заходів. Колективне планування дає змогу включати всіх або знаходити найвідповіднішого члена завдяки циклічній ротації.",
|
||||
"booking_limit_reached": "Для цього типу заходу досягнуто ліміт бронювання",
|
||||
"admin_has_disabled": "Адміністратор вимкнув {{appName}}",
|
||||
"disabled_app_affects_event_type": "Адміністратор вимкнув {{appName}}, що впливає на ваш тип заходу «{{eventType}}»",
|
||||
"disable_payment_app": "Адміністратор вимкнув {{appName}}, що впливає на ваш тип заходу «{{title}}». Учасники все одно можуть бронювати події такого типу, але від них не вимагатиметься оплата. Ви можете приховати цей тип заходу, щоб цього не ставалося, та дочекатися на активацію способу оплати з боку адміністратора.",
|
||||
"payment_disabled_still_able_to_book": "Учасники все одно можуть бронювати події такого типу, але від них не вимагатиметься оплата. Ви можете приховати цей тип заходу, щоб цього не ставалося, та дочекатися на активацію способу оплати з боку адміністратора.",
|
||||
"app_disabled_with_event_type": "Адміністратор вимкнув {{appName}}, що впливає на ваш тип заходу «{{title}}».",
|
||||
"app_disabled_video": "Адміністратор вимкнув {{appName}}, що впливає на ваші типи заходів. Якщо у вас є типи заходів, де {{appName}} визначає розташування, за замовчуванням використовуватиметься Cal Video.",
|
||||
"app_disabled_subject": "{{appName}} вимкнено",
|
||||
"navigate_installed_apps": "Перейти до встановлених додатків",
|
||||
"disabled_calendar": "Якщо ви встановите інший календар, у нього буде додано нові бронювання. Якщо цього не станеться, під’єднайте новий календар, щоб не пропускати нові бронювання.",
|
||||
"enable_apps": "Увімкнення додатків",
|
||||
"enable_apps_description": "Активуйте додатки, які користувачі зможуть інтегрувати з Cal.com",
|
||||
"app_is_enabled": "{{appName}} увімкнено",
|
||||
"app_is_disabled": "{{appName}} вимкнено",
|
||||
"keys_have_been_saved": "Ключі збережено",
|
||||
"disable_app": "Вимкнути додаток",
|
||||
"disable_app_description": "Якщо вимкнути цей додаток, у користувачів можуть виникнути проблеми із роботою з Cal",
|
||||
"edit_keys": "Редагувати ключі",
|
||||
"no_available_apps": "Немає доступних додатків",
|
||||
"no_available_apps_description": "Перевірте, чи в «packages/app-store» є додатки для розгортання",
|
||||
"no_apps": "У цьому екземплярі Cal немає ввімкнених додатків",
|
||||
"apps_settings": "Налаштування додатків",
|
||||
"fill_this_field": "Заповніть це поле",
|
||||
"options": "Варіанти",
|
||||
"enter_option": "Введіть варіант {{index}}",
|
||||
"add_an_option": "Додайте варіант",
|
||||
"radio": "Радіо",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-копія",
|
||||
"set_as_default": "Встановити за замовчуванням"
|
||||
}
|
||||
|
|
|
@ -385,7 +385,6 @@
|
|||
"go_to_billing_portal": "Đi tới cổng thanh toán",
|
||||
"need_anything_else": "Cần gì nữa không?",
|
||||
"further_billing_help": "Nếu bạn cần thêm bất kỳ trợ giúp nào về thanh toán, nhóm hỗ trợ của chúng tôi luôn sẵn sàng trợ giúp.",
|
||||
"contact_our_support_team": "Liên hệ với nhóm hỗ trợ của chúng tôi",
|
||||
"uh_oh": "\bÔi không!",
|
||||
"no_event_types_have_been_setup": "Người dùng này chưa thiết lập bất kỳ loại sự kiện nào.",
|
||||
"edit_logo": "Chỉnh sửa logo",
|
||||
|
@ -422,6 +421,7 @@
|
|||
"current_incorrect_password": "Mật khẩu hiện tại không đúng",
|
||||
"password_hint_caplow": "Kết hợp các chữ cái in hoa & in thường",
|
||||
"password_hint_min": "Dài tối thiểu 8 ký tự",
|
||||
"password_hint_admin_min": "Dài tối thiểu 15 ký tự",
|
||||
"password_hint_num": "Chứa ít nhất 1 con số",
|
||||
"invalid_password_hint": "Mật khẩu phải có tối thiểu 7 ký tự chứa ít nhất một con số và phải kết hợp chữ cái in hoa lẫn in thường",
|
||||
"incorrect_password": "Mật khẩu không đúng.",
|
||||
|
@ -465,11 +465,14 @@
|
|||
"booking_confirmation": "Xác nhận {{eventTypeTitle}} của bạn với {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Lên lịch lại {{eventTypeTitle}} của bạn với {{profileName}}",
|
||||
"in_person_meeting": "Gặp mặt trực tiếp",
|
||||
"attendeeInPerson": "Đích thân (địa chỉ người tham gia)",
|
||||
"inPerson": "Đích thân (địa chỉ người tổ chức)",
|
||||
"link_meeting": "Liên kết cuộc họp",
|
||||
"phone_call": "Số điện thoại của người tham gia",
|
||||
"your_number": "Số điện thoại của bạn",
|
||||
"phone_number": "Số điện thoại",
|
||||
"attendee_phone_number": "Số điện thoại của người tham gia",
|
||||
"organizer_phone_number": "Số điện thoại người tổ chức",
|
||||
"host_phone_number": "Số điện thoại của bạn",
|
||||
"enter_phone_number": "Nhập số điện thoại",
|
||||
"reschedule": "Thay đổi lịch hẹn",
|
||||
|
@ -556,6 +559,10 @@
|
|||
"collective": "Tập thể",
|
||||
"collective_description": "Lên lịch họp khi tất cả các thành viên trong nhóm đã chọn đều có mặt.",
|
||||
"duration": "Khoảng thời gian",
|
||||
"available_durations": "Khoảng thời gian khả dụng",
|
||||
"default_duration": "Khoảng thời gian mặc định",
|
||||
"default_duration_no_options": "Vui lòng chọn trước tiên những khoảng thời gian khả dụng",
|
||||
"multiple_duration_mins": "{{count}}$t(minute_timeUnit)",
|
||||
"minutes": "Phút",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Luân phiên những cuộc họp giữa các thành viên trong nhóm.",
|
||||
|
@ -621,6 +628,7 @@
|
|||
"teams": "Các nhóm",
|
||||
"team": "Nhóm",
|
||||
"team_billing": "Thanh toán nhóm",
|
||||
"team_billing_description": "Quản lí thanh toán cho đội ngũ của bạn",
|
||||
"upgrade_to_flexible_pro_title": "Chúng tôi đã thay đổi thanh toán cho các nhóm",
|
||||
"upgrade_to_flexible_pro_message": "Có những thành viên trong nhóm của bạn không có gói. Nâng cấp gói PRO của bạn để trang trải những gói bị thiếu.",
|
||||
"changed_team_billing_info": "Kể từ tháng 1 năm 2022, chúng tôi tính phí trên cơ sở từng người cho các thành viên trong nhóm. Các thành viên trong nhóm của bạn mà từng có PRO miễn phí nay sẽ chuyển sang dùng thử 14 ngày. Khi thời gian dùng thử hết hạn, các thành viên này sẽ bị ẩn khỏi nhóm của bạn trừ khi bạn nâng cấp.",
|
||||
|
@ -696,6 +704,7 @@
|
|||
"hide_event_type": "Ẩn loại sự kiện",
|
||||
"edit_location": "Chỉnh sửa vị trí",
|
||||
"into_the_future": "trong tương lai",
|
||||
"when_booked_with_less_than_notice": "Khi đặt hẹn với khoảng thời gian thông báo ít hơn <time></time>",
|
||||
"within_date_range": "Trong phạm vi ngày",
|
||||
"indefinitely_into_future": "Vô thời hạn trong tương lai",
|
||||
"add_new_custom_input_field": "Thêm trường tùy chỉnh mới",
|
||||
|
@ -715,6 +724,7 @@
|
|||
"delete_account_confirmation_message": "Bạn có chắc chắn muốn xóa tài khoản {{appName}} của mình không? Bất kỳ ai mà bạn đã chia sẻ liên kết tài khoản của mình sẽ không thể đặt trước bằng liên kết đó nữa và mọi tùy chọn bạn đã lưu sẽ bị mất.",
|
||||
"integrations": "Tích hợp",
|
||||
"apps": "Ứng dụng",
|
||||
"apps_listing": "Danh sách ứng dụng",
|
||||
"category_apps": "Ứng dụng {{category}}",
|
||||
"app_store": "Cửa hàng ứng dụng",
|
||||
"app_store_description": "Kết nối con người, công nghệ và nơi làm việc.",
|
||||
|
@ -738,6 +748,7 @@
|
|||
"toggle_calendars_conflict": "Bật lịch mà bạn muốn kiểm tra trùng ngày để tránh đặt lịch hẹn trùng.",
|
||||
"select_destination_calendar": "Tạo sự kiện trên",
|
||||
"connect_additional_calendar": "Kết nối lịch bổ sung",
|
||||
"calendar_updated_successfully": "Đã cập nhật lịch thành công",
|
||||
"conferencing": "Hội nghị",
|
||||
"calendar": "Lịch",
|
||||
"payments": "Thanh toán",
|
||||
|
@ -770,6 +781,7 @@
|
|||
"trending_apps": "Ứng dụng thịnh hành",
|
||||
"explore_apps": "{{category}} ứng dụng",
|
||||
"installed_apps": "Ứng dụng đã cài đặt",
|
||||
"free_to_use_apps": "Miễn phí",
|
||||
"no_category_apps": "Không có ứng dụng {{category}}",
|
||||
"no_category_apps_description_calendar": "Thêm một ứng dụng lịch để kiểm tra xung đột nhằm tránh đặt lịch hẹn trùng",
|
||||
"no_category_apps_description_conferencing": "Thử thêm vào một ứng dụng hội nghị để hợp nhất cuộc gọi video với khách hàng của bạn",
|
||||
|
@ -808,6 +820,8 @@
|
|||
"verify_wallet": "Xác minh Ví",
|
||||
"connect_metamask": "Kết nối Metamask",
|
||||
"create_events_on": "Tạo sự kiện trên",
|
||||
"enterprise_license": "Đây là tính năng doanh nghiệp",
|
||||
"enterprise_license_description": "Để bật tính năng này, nhận khoá triển khai tại console {{consoleUrl}} và thêm nó vào .env của bạn ở dạng CALCOM_LICENSE_KEY. Nếu nhóm của bạn đã có giấy phép, vui lòng liên hệ {{supportMail}} để được trợ giúp.",
|
||||
"missing_license": "Giấy phép bị thiếu",
|
||||
"signup_requires": "Yêu cầu giấy phép thương mại",
|
||||
"signup_requires_description": "{{companyName}} hiện không cung cấp phiên bản nguồn mở miễn phí của trang đăng ký. Để nhận toàn quyền truy cập vào các thành phần đăng ký, bạn cần có giấy phép thương mại. Đối với mục đích sử dụng cá nhân, chúng tôi khuyên bạn nên sử dụng Nền tảng dữ liệu Prisma hoặc bất kỳ giao diện Postgres nào khác để tạo tài khoản.",
|
||||
|
@ -899,6 +913,7 @@
|
|||
"user_impersonation_heading": "Mạo danh người dùng",
|
||||
"user_impersonation_description": "Cho phép đội ngũ hỗ trợ tạm thời đăng nhập với tư cách là bạn nhằm giúp chúng tôi nhanh chóng giải quyết mọi vấn đề mà bạn báo cáo cho chúng tôi.",
|
||||
"team_impersonation_description": "Cho phép các quản trị viên của nhóm bạn tạm thời đăng nhập bằng danh tính của bạn.",
|
||||
"allow_booker_to_select_duration": "Cho phép người đặt hẹn chọn khoảng thời gian",
|
||||
"impersonate_user_tip": "Tất cả những lần dùng tính năng này đều bị kiểm toán.",
|
||||
"impersonating_user_warning": "Đang mạo danh tên người dùng \"{{user}}\".",
|
||||
"impersonating_stop_instructions": "<0>Nhấp vào đây để ngừng lại</0>.",
|
||||
|
@ -1016,6 +1031,9 @@
|
|||
"error_removing_app": "Lỗi khi gỡ bỏ ứng dụng",
|
||||
"web_conference": "Hội nghị web",
|
||||
"requires_confirmation": "Yêu cầu xác nhận",
|
||||
"always_requires_confirmation": "Luôn luôn",
|
||||
"requires_confirmation_threshold": "Cần xác nhận nếu đặt hẹn với thông báo trước chưa đầy {{time}}$t({{unit}}_timeUnit)",
|
||||
"may_require_confirmation": "Có thể yêu cầu xác nhận",
|
||||
"nr_event_type_one": "{{count}} loại sự kiện",
|
||||
"nr_event_type_other": "{{count}} loại sự kiện",
|
||||
"add_action": "Thêm hoạt động",
|
||||
|
@ -1103,6 +1121,9 @@
|
|||
"event_limit_tab_description": "Bạn có thể được đặt lịch bao lần",
|
||||
"event_advanced_tab_description": "Cài đặt cho lịch & nhiều tính năng khác...",
|
||||
"event_advanced_tab_title": "Nâng cao",
|
||||
"event_setup_multiple_duration_error": "Thiết lập sự kiện: Nhiều khoảng thời gian cần ít nhất 1 tuỳ chọn.",
|
||||
"event_setup_multiple_duration_default_error": "Thiết lập sự kiện: Vui lòng chọn một khoảng thời gian mặc định hợp lệ.",
|
||||
"event_setup_booking_limits_error": "Giới hạn lịch hẹn phải theo thứ tự tăng dần. [day,week,month,year]",
|
||||
"select_which_cal": "Chọn lịch nào cần thêm lịch hẹn vào",
|
||||
"custom_event_name": "Tên sự kiện tuỳ chỉnh",
|
||||
"custom_event_name_description": "Tạo những tên sự kiện tuỳ chỉnh để hiển thị ở phần sự kiện lịch",
|
||||
|
@ -1156,8 +1177,11 @@
|
|||
"invoices": "Hoá đơn",
|
||||
"embeds": "Nhúng",
|
||||
"impersonation": "Mạo danh",
|
||||
"impersonation_description": "Cài đặt để quản lý mạo danh người dùng",
|
||||
"users": "Người dùng",
|
||||
"profile_description": "Quản lí cài đặt cho hồ sơ {{appName}} của bạn",
|
||||
"users_description": "Tại đây bạn có thể tìm thấy danh sách tất cả người dùng",
|
||||
"users_listing": "Danh sách người dùng",
|
||||
"general_description": "Quản lí cài đặt cho ngôn ngữ và múi giờ của bạn",
|
||||
"calendars_description": "Cấu hình cách các loại sự kiện của bạn tương tác với lịch của bạn",
|
||||
"appearance_description": "Quản lí cài đặt cho giao diện lịch hẹn của bạn",
|
||||
|
@ -1362,6 +1386,7 @@
|
|||
"number_sms_notifications": "Số điện thoại (thông báo SMS)",
|
||||
"attendee_email_workflow": "Email người tham dự",
|
||||
"attendee_email_info": "Email người tham gia lịch hẹn",
|
||||
"kbar_search_placeholder": "Nhập một lệnh hoặc tìm kiếm...",
|
||||
"invalid_credential": "Ôi không! Có vẻ như quyền đã hết hạn hoặc đã bị thu hồi. Vui lòng cài đặt lại.",
|
||||
"choose_common_schedule_team_event": "Chọn một lịch chung",
|
||||
"choose_common_schedule_team_event_description": "Bật mục này nếu bạn muốn dùng lịch thông thường giữa các chủ sự kiện. Khi tắt đi, mỗi chủ sự kiện sẽ được đặt lịch theo lịch mặc định của họ.",
|
||||
|
@ -1372,5 +1397,9 @@
|
|||
"test_preview": "Kiểm tra Xem trước",
|
||||
"route_to": "Định hướng đến",
|
||||
"test_preview_description": "Kiểm tra biểu mẫu định hướng mà không cần gửi bất kỳ dữ liệu nào",
|
||||
"test_routing": "Kiểm tra định hướng"
|
||||
"test_routing": "Kiểm tra định hướng",
|
||||
"payment_app_disabled": "Một quản trị viên đã vô hiệu hoá một ứng dụng thanh toán",
|
||||
"edit_event_type": "Sửa loại sự kiện",
|
||||
"collective_scheduling": "Lên lịch tập thể",
|
||||
"admin_apps_description": "Bật các ứng dụng cho thực thể Cal của bạn"
|
||||
}
|
||||
|
|
|
@ -249,6 +249,7 @@
|
|||
"add_to_calendar": "添加到日历",
|
||||
"add_another_calendar": "添加另一个日历",
|
||||
"other": "其他",
|
||||
"email_sign_in_subject": "您的 {{appName}} 登录链接",
|
||||
"emailed_you_and_attendees": "我们通过电子邮件给您和其他参与者发送了一份包含所有详细信息的日历邀请。",
|
||||
"emailed_you_and_attendees_recurring": "我们通过电子邮件给您和其他参与者发送了第一次定期活动的日历邀请。",
|
||||
"emailed_you_and_any_other_attendees": "已向您和其他参与者发送了关于此信息的电子邮件",
|
||||
|
@ -384,7 +385,6 @@
|
|||
"go_to_billing_portal": "转到账单门户网站",
|
||||
"need_anything_else": "还有其他需要?",
|
||||
"further_billing_help": "如果您在支付帐单时需要任何进一步的帮助,我们的支持团队会给您提供帮助。",
|
||||
"contact_our_support_team": "联系我们的支持团队",
|
||||
"uh_oh": "糟糕!",
|
||||
"no_event_types_have_been_setup": "此用户尚未设置任何活动类型。",
|
||||
"edit_logo": "编辑标志",
|
||||
|
@ -421,6 +421,7 @@
|
|||
"current_incorrect_password": "当前密码不正确",
|
||||
"password_hint_caplow": "大写和小写字母混合",
|
||||
"password_hint_min": "至少 8 个字符长",
|
||||
"password_hint_admin_min": "至少 15 个字符长",
|
||||
"password_hint_num": "包含至少 1 个数字",
|
||||
"invalid_password_hint": "密码必须是至少 7 个字符长,包含至少一个数字,并且是大写和小写字母混合",
|
||||
"incorrect_password": "密码不正确。",
|
||||
|
@ -464,11 +465,14 @@
|
|||
"booking_confirmation": "确认您和 {{profileName}} 的 {{eventTypeTitle}}",
|
||||
"booking_reschedule_confirmation": "重新安排您和 {{profileName}} 的 {{eventTypeTitle}}",
|
||||
"in_person_meeting": "线上或线下会议",
|
||||
"attendeeInPerson": "本人(参与者地址)",
|
||||
"inPerson": "本人(组织者地址)",
|
||||
"link_meeting": "线上会议",
|
||||
"phone_call": "参与者电话号码",
|
||||
"your_number": "您的电话号码",
|
||||
"phone_number": "电话号码",
|
||||
"attendee_phone_number": "参与者电话号码",
|
||||
"organizer_phone_number": "组织者电话号码",
|
||||
"host_phone_number": "您的电话号码",
|
||||
"enter_phone_number": "输入电话号码",
|
||||
"reschedule": "重新安排",
|
||||
|
@ -555,6 +559,10 @@
|
|||
"collective": "集体模式",
|
||||
"collective_description": "当所有选定的团队成员都可预约时安排会议。",
|
||||
"duration": "时长",
|
||||
"available_durations": "可用时长",
|
||||
"default_duration": "默认时长",
|
||||
"default_duration_no_options": "请先选择可用时长",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"minutes": "分钟",
|
||||
"round_robin": "轮流模式",
|
||||
"round_robin_description": "和多个团队成员之间轮流举行会议。",
|
||||
|
@ -620,6 +628,7 @@
|
|||
"teams": "团队",
|
||||
"team": "团队",
|
||||
"team_billing": "团队计费",
|
||||
"team_billing_description": "管理您团队的计费",
|
||||
"upgrade_to_flexible_pro_title": "我们更改了团队的计费方式",
|
||||
"upgrade_to_flexible_pro_message": "您的团队中有没有位置的成员。升级到专业版来获得更多位置。",
|
||||
"changed_team_billing_info": "自2022年1月起,我们将按每个位置向团队成员收取费用。 您的团队中免费获得专业版的成员现在正在进行为期14天的试用。 试用期结束后,这些成员将在您的团队中隐藏,除非您现在升级到专业版。",
|
||||
|
@ -714,6 +723,8 @@
|
|||
"delete_account_confirmation_message": "您确定要删除您的 {{appName}} 账户? 任何曾通过您的账户链接与您预约过的人都将无法再使用它和您预约,您保存的任何偏好设置都将丢失。",
|
||||
"integrations": "集成",
|
||||
"apps": "应用",
|
||||
"apps_description": "为您的 Cal 实例启用应用",
|
||||
"apps_listing": "应用列表",
|
||||
"category_apps": "{{category}}应用",
|
||||
"app_store": "应用商店",
|
||||
"app_store_description": "连接人员、技术和工作场所。",
|
||||
|
@ -737,6 +748,7 @@
|
|||
"toggle_calendars_conflict": "切换要检查冲突的日历,以防止重复预约。",
|
||||
"select_destination_calendar": "创建活动于",
|
||||
"connect_additional_calendar": "连接其他日历",
|
||||
"calendar_updated_successfully": "日历已成功更新",
|
||||
"conferencing": "会议",
|
||||
"calendar": "日历",
|
||||
"payments": "付款",
|
||||
|
@ -769,6 +781,7 @@
|
|||
"trending_apps": "热门应用",
|
||||
"explore_apps": "{{category}}应用",
|
||||
"installed_apps": "已安装的应用",
|
||||
"free_to_use_apps": "免费",
|
||||
"no_category_apps": "无{{category}}应用",
|
||||
"no_category_apps_description_calendar": "添加日历应用以检查冲突,防止重复预约",
|
||||
"no_category_apps_description_conferencing": "尝试添加会议应用以整合与客户的视频通话",
|
||||
|
@ -807,6 +820,8 @@
|
|||
"verify_wallet": "验证钱包",
|
||||
"connect_metamask": "连接元掩码",
|
||||
"create_events_on": "活动创建于:",
|
||||
"enterprise_license": "这是企业版功能",
|
||||
"enterprise_license_description": "要启用此功能,请在 {{consoleUrl}} 控制台获取一个部署密钥,并将其添加到您的 .env 中作为 CALCOM_LICENSE_KEY。如果您的团队已经有许可证,请联系 {{supportMail}} 获取帮助。",
|
||||
"missing_license": "缺少许可证",
|
||||
"signup_requires": "需要商业许可证",
|
||||
"signup_requires_description": "{{companyName}} 免费开源版目前不提供的注册页面。 要获得对注册页面组件的完全访问权,您需要获得商业许可证。 对于个人使用用途,我们推荐使用Prisma 数据平台或任何其他Postgres接口创建账户。",
|
||||
|
@ -898,6 +913,7 @@
|
|||
"user_impersonation_heading": "用户模拟",
|
||||
"user_impersonation_description": "允许我们的支持团队临时以您的身份登录,帮助我们快速解决您报告给我们的问题。",
|
||||
"team_impersonation_description": "允许您的团队成员以您的身份临时登录。",
|
||||
"allow_booker_to_select_duration": "允许预约者选择时长",
|
||||
"impersonate_user_tip": "此功能的所有使用都已审核。",
|
||||
"impersonating_user_warning": "正在模拟用户名“{{user}}”。",
|
||||
"impersonating_stop_instructions": "<0>点击此处停止</0>。",
|
||||
|
@ -1015,6 +1031,8 @@
|
|||
"error_removing_app": "删除应用时出错",
|
||||
"web_conference": "网络会议",
|
||||
"requires_confirmation": "需要确认",
|
||||
"always_requires_confirmation": "始终",
|
||||
"may_require_confirmation": "可能需要确认",
|
||||
"nr_event_type_one": "{{count}} 种活动类型",
|
||||
"nr_event_type_other": "{{count}} 种活动类型",
|
||||
"add_action": "添加操作",
|
||||
|
@ -1102,6 +1120,9 @@
|
|||
"event_limit_tab_description": "您多久可被预约一次",
|
||||
"event_advanced_tab_description": "日历设置及更多...",
|
||||
"event_advanced_tab_title": "高级",
|
||||
"event_setup_multiple_duration_error": "活动设置: 多个时长需要至少 1 个选项。",
|
||||
"event_setup_multiple_duration_default_error": "活动设置: 请选择有效的默认时长。",
|
||||
"event_setup_booking_limits_error": "预约限制必须按升序排列。[日,周,月,年]",
|
||||
"select_which_cal": "选择要添加预约的日历",
|
||||
"custom_event_name": "自定义活动名称",
|
||||
"custom_event_name_description": "创建要在日历活动上显示的自定义活动名称",
|
||||
|
@ -1155,8 +1176,11 @@
|
|||
"invoices": "发票",
|
||||
"embeds": "嵌入项",
|
||||
"impersonation": "模拟",
|
||||
"impersonation_description": "用于管理用户模拟的设置",
|
||||
"users": "用户",
|
||||
"profile_description": "管理您的 {{appName}} 个人资料的设置",
|
||||
"users_description": "在这里可以找到所有用户的列表",
|
||||
"users_listing": "用户列表",
|
||||
"general_description": "管理语言和时区的设置",
|
||||
"calendars_description": "配置您的活动类型与日历的交互方式",
|
||||
"appearance_description": "管理预约页面外观的设置",
|
||||
|
@ -1361,6 +1385,7 @@
|
|||
"number_sms_notifications": "电话号码(短信通知)",
|
||||
"attendee_email_workflow": "参与者电子邮件",
|
||||
"attendee_email_info": "预约人的电子邮件",
|
||||
"kbar_search_placeholder": "输入命令或搜索...",
|
||||
"invalid_credential": "哦不!看起来权限已过期或被撤销。请重新安装。",
|
||||
"choose_common_schedule_team_event": "选择通用时间表",
|
||||
"choose_common_schedule_team_event_description": "如果您希望在主持人之间使用通用时间表,请启用此选项。如果禁用,将根据主持人的默认时间表预约每个主持人。",
|
||||
|
@ -1371,5 +1396,36 @@
|
|||
"test_preview": "测试预览",
|
||||
"route_to": "根据途径找到",
|
||||
"test_preview_description": "测试途径表格而不提交任何数据",
|
||||
"test_routing": "测试途径"
|
||||
"test_routing": "测试途径",
|
||||
"payment_app_disabled": "管理员已禁用支付应用",
|
||||
"edit_event_type": "编辑活动类型",
|
||||
"collective_scheduling": "集体日程安排",
|
||||
"fixed_round_robin": "固定轮流模式",
|
||||
"calcom_is_better_with_team": "Cal.com 更适合团队",
|
||||
"admin_has_disabled": "管理员已禁用 {{appName}}",
|
||||
"disabled_app_affects_event_type": "管理员已禁用会影响活动类型 {{eventType}} 的 {{appName}}",
|
||||
"disable_payment_app": "管理员已禁用会影响活动类型 {{title}} 的 {{appName}}。参与者仍然能预约此类型的活动,但不会被提示付款。您可以隐藏该活动类型来防止出现这种情况,直到管理员重新启用您的支付方式。",
|
||||
"payment_disabled_still_able_to_book": "参与者仍然能预约此类型的活动,但不会被提示付款。您可以隐藏该活动类型来防止出现这种情况,直到管理员重新启用您的支付方式。",
|
||||
"app_disabled_with_event_type": "管理员已禁用会影响活动类型 {{title}} 的 {{appName}}。",
|
||||
"app_disabled_video": "管理员已禁用可能会影响活动类型的 {{appName}}。如果您有将 {{appName}} 作为位置的活动类型,则默认为 Cal Video。",
|
||||
"app_disabled_subject": "{{appName}} 已被禁用",
|
||||
"navigate_installed_apps": "转到已安装的应用",
|
||||
"enable_apps": "启用应用",
|
||||
"enable_apps_description": "启用用户可以与 Cal.com 集成的应用",
|
||||
"app_is_enabled": "{{appName}} 已启用",
|
||||
"app_is_disabled": "{{appName}} 已禁用",
|
||||
"keys_have_been_saved": "密钥已保存",
|
||||
"disable_app": "禁用应用",
|
||||
"disable_app_description": "禁用此应用可能会导致您的用户与 Cal 的交互方式出现问题",
|
||||
"edit_keys": "编辑密钥",
|
||||
"admin_apps_description": "为您的 Cal 实例启用应用",
|
||||
"no_available_apps": "没有可用的应用",
|
||||
"no_available_apps_description": "请确保您在“packages/app-store”下的部署中有应用",
|
||||
"no_apps": "此 Cal 实例中未启用任何应用",
|
||||
"apps_settings": "应用设置",
|
||||
"fill_this_field": "请填写此字段",
|
||||
"options": "选项",
|
||||
"enter_option": "输入选项 {{index}}",
|
||||
"add_an_option": "添加选项",
|
||||
"set_as_default": "设置为默认"
|
||||
}
|
||||
|
|
|
@ -383,7 +383,6 @@
|
|||
"go_to_billing_portal": "前往付費入口",
|
||||
"need_anything_else": "還要什麼嗎?",
|
||||
"further_billing_help": "如果有關於付費的進一步需求,我們的支援團隊隨時上前幫忙。",
|
||||
"contact_our_support_team": "聯繫支援團隊",
|
||||
"uh_oh": "哎呦喂呀!",
|
||||
"no_event_types_have_been_setup": "使用者尚未設定任何活動類型。",
|
||||
"edit_logo": "編輯標誌",
|
||||
|
|
After Width: | Height: | Size: 697 KiB |
|
@ -432,6 +432,11 @@ hr {
|
|||
border: none !important;
|
||||
}
|
||||
|
||||
.react-date-picker__inputGroup__input {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* animations */
|
||||
.slideInBottom {
|
||||
animation-duration: 0.3s;
|
||||
|
|
|
@ -10,6 +10,7 @@ const HAWAII_AND_NEWYORK_TEAM = [
|
|||
timeZone: "America/Detroit", // GMT -4 per 22th of Aug, 2022
|
||||
workingHours: [{ days: [1, 2, 3, 4, 5], startTime: 780, endTime: 1260 }],
|
||||
busy: [],
|
||||
dateOverrides: [],
|
||||
},
|
||||
{
|
||||
timeZone: "Pacific/Honolulu", // GMT -10 per 22th of Aug, 2022
|
||||
|
@ -20,6 +21,7 @@ const HAWAII_AND_NEWYORK_TEAM = [
|
|||
{ days: [5], startTime: 780, endTime: 1439 },
|
||||
],
|
||||
busy: [],
|
||||
dateOverrides: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 30cbf3990bb5baae7fd4fd46c93f6e5bc402f84c
|
||||
Subproject commit 794dda81932f6c0572941fb70cc38b599510d31f
|
|
@ -39,6 +39,7 @@ import { metadata as stripepayment_meta } from "./stripepayment/_metadata";
|
|||
import { metadata as tandemvideo_meta } from "./tandemvideo/_metadata";
|
||||
import { metadata as telegram_meta } from "./telegram/_metadata";
|
||||
import { metadata as typeform_meta } from "./typeform/_metadata";
|
||||
import { metadata as vimcal_meta } from "./vimcal/_metadata";
|
||||
import { metadata as vital_meta } from "./vital/_metadata";
|
||||
import { metadata as weather_in_your_calendar_meta } from "./weather_in_your_calendar/_metadata";
|
||||
import { metadata as whatsapp_meta } from "./whatsapp/_metadata";
|
||||
|
@ -86,6 +87,7 @@ export const appStoreMetadata = {
|
|||
tandemvideo: tandemvideo_meta,
|
||||
telegram: telegram_meta,
|
||||
typeform: typeform_meta,
|
||||
vimcal: vimcal_meta,
|
||||
vital: vital_meta,
|
||||
weather_in_your_calendar: weather_in_your_calendar_meta,
|
||||
whatsapp: whatsapp_meta,
|
||||
|
|
|
@ -38,6 +38,7 @@ export const apiHandlers = {
|
|||
tandemvideo: import("./tandemvideo/api"),
|
||||
telegram: import("./telegram/api"),
|
||||
typeform: import("./typeform/api"),
|
||||
vimcal: import("./vimcal/api"),
|
||||
vital: import("./vital/api"),
|
||||
weather_in_your_calendar: import("./weather_in_your_calendar/api"),
|
||||
whatsapp: import("./whatsapp/api"),
|
||||
|
|
|
@ -40,8 +40,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
await prisma.credential.create({
|
||||
data,
|
||||
});
|
||||
} catch (reason) {
|
||||
logger.error("Could not add this caldav account", reason);
|
||||
} catch (e) {
|
||||
logger.error("Could not add this caldav account", e);
|
||||
if (e instanceof Error) {
|
||||
let message = e.message;
|
||||
if (e.message.indexOf("Invalid credentials") > -1 && url.indexOf("dav.php") > -1) {
|
||||
const parsedUrl = new URL(url);
|
||||
const adminUrl =
|
||||
parsedUrl.protocol +
|
||||
"//" +
|
||||
parsedUrl.hostname +
|
||||
(parsedUrl.port ? ":" + parsedUrl.port : "") +
|
||||
"/admin/?/settings/standard/";
|
||||
message = `Couldn\'t connect to caldav account, please verify WebDAV authentication type is set to "Basic"`;
|
||||
return res.status(500).json({ message, actionUrl: adminUrl });
|
||||
}
|
||||
}
|
||||
return res.status(500).json({ message: "Could not add this caldav account" });
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ export default function CalDavCalendarSetup() {
|
|||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [errorActionUrl, setErrorActionUrl] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
|
@ -49,6 +50,9 @@ export default function CalDavCalendarSetup() {
|
|||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrorMessage(json?.message || t("something_went_wrong"));
|
||||
if (json.actionUrl) {
|
||||
setErrorActionUrl(json.actionUrl);
|
||||
}
|
||||
} else {
|
||||
router.push(json.url);
|
||||
}
|
||||
|
@ -78,7 +82,24 @@ export default function CalDavCalendarSetup() {
|
|||
/>
|
||||
</fieldset>
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
{errorMessage && (
|
||||
<Alert
|
||||
severity="error"
|
||||
title={errorMessage}
|
||||
actions={
|
||||
errorActionUrl !== "" ? (
|
||||
<Button
|
||||
href={errorActionUrl}
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
className="ml-5 w-32 !p-5">
|
||||
Go to Admin
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
className="my-4"
|
||||
/>
|
||||
)}
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
|
|
|
@ -135,7 +135,7 @@ export const FormActionsDropdown = ({ form, children }: { form: RoutingForm; chi
|
|||
return (
|
||||
<dropdownCtx.Provider value={{ dropdown: true }}>
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger data-testid="form-dropdown" asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
|
|
|
@ -120,6 +120,7 @@ const Actions = ({
|
|||
{typeformApp?.isInstalled ? (
|
||||
<FormActionsDropdown form={form}>
|
||||
<FormAction
|
||||
data-testid="copy-redirect-url"
|
||||
routingForm={form}
|
||||
action="copyRedirectUrl"
|
||||
color="minimal"
|
||||
|
@ -172,6 +173,7 @@ const Actions = ({
|
|||
</FormAction>
|
||||
{typeformApp ? (
|
||||
<FormAction
|
||||
data-testid="copy-redirect-url"
|
||||
routingForm={form}
|
||||
action="copyRedirectUrl"
|
||||
color="minimal"
|
||||
|
|
|
@ -131,6 +131,7 @@ export default function RoutingForms({
|
|||
</FormAction>
|
||||
{typeformApp?.isInstalled ? (
|
||||
<FormAction
|
||||
data-testid="copy-redirect-url"
|
||||
routingForm={form}
|
||||
action="copyRedirectUrl"
|
||||
color="minimal"
|
||||
|
|
|
@ -8,7 +8,7 @@ async function gotoRoutingLink(page: Page, formId: string) {
|
|||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
async function addForm(page: Page) {
|
||||
export async function addForm(page: Page) {
|
||||
await page.click('[data-testid="new-routing-form"]');
|
||||
await page.fill("input[name]", "Test Form Name");
|
||||
await page.click('[data-testid="add-form"]');
|
||||
|
@ -41,7 +41,7 @@ async function verifySelectOptions(
|
|||
};
|
||||
}
|
||||
|
||||
async function fillForm(
|
||||
export async function fillForm(
|
||||
page: Page,
|
||||
form: { description: string; field?: { typeIndex: number; label: string } }
|
||||
) {
|
||||
|
|
|
@ -157,13 +157,32 @@ export default class Office365CalendarService implements Calendar {
|
|||
}
|
||||
|
||||
async listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
const response = await this.fetcher(`/me/calendars`);
|
||||
let responseBody = await handleErrorsJson<{ value: OfficeCalendar[] }>(response);
|
||||
const officeCalendars: OfficeCalendar[] = [];
|
||||
// List calendars from MS are paginated
|
||||
let finishedParsingCalendars = false;
|
||||
// Store @odata.nextLink if in response
|
||||
let requestLink = "/me/calendars";
|
||||
|
||||
while (!finishedParsingCalendars) {
|
||||
const response = await this.fetcher(requestLink);
|
||||
let responseBody = await handleErrorsJson<{ value: OfficeCalendar[]; "@odata.nextLink"?: string }>(
|
||||
response
|
||||
);
|
||||
// If responseBody is valid then parse the JSON text
|
||||
if (typeof responseBody === "string") {
|
||||
responseBody = JSON.parse(responseBody) as { value: OfficeCalendar[] };
|
||||
}
|
||||
return responseBody?.value.map((cal: OfficeCalendar) => {
|
||||
|
||||
officeCalendars.push(...responseBody.value);
|
||||
|
||||
if (responseBody["@odata.nextLink"]) {
|
||||
requestLink = responseBody["@odata.nextLink"].replace(this.apiGraphUrl, "");
|
||||
} else {
|
||||
finishedParsingCalendars = true;
|
||||
}
|
||||
}
|
||||
|
||||
return officeCalendars.map((cal: OfficeCalendar) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No Id",
|
||||
integration: this.integrationName,
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import { Page, expect } from "@playwright/test";
|
||||
|
||||
import {
|
||||
addForm as addRoutingForm,
|
||||
fillForm as fillRoutingForm,
|
||||
} from "@calcom/app-store/ee/routing-forms/playwright/tests/basic.e2e";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { Fixtures, test } from "@calcom/web/playwright/lib/fixtures";
|
||||
|
||||
const installApps = async (page: Page, users: Fixtures["users"]) => {
|
||||
const user = await users.create({ username: "routing-forms" });
|
||||
await user.login();
|
||||
await page.goto(`/apps/routing-forms`);
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
await page.waitForNavigation({
|
||||
url: (url) => url.pathname === `/apps/routing-forms/forms`,
|
||||
});
|
||||
await page.goto(`/apps/typeform`);
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
await page.waitForNavigation({
|
||||
url: (url) => url.pathname === `/apps/typeform/how-to-use`,
|
||||
});
|
||||
};
|
||||
|
||||
test.describe("Typeform App", () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
// This also delete forms on cascade
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Typeform Redirect Link", () => {
|
||||
test("should copy link in editing area", async ({ page, context, users }) => {
|
||||
await installApps(page, users);
|
||||
context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
|
||||
await page.goto(`/apps/routing-forms/forms`);
|
||||
const formId = await addRoutingForm(page);
|
||||
await fillRoutingForm(page, {
|
||||
description: "",
|
||||
field: { label: "test", typeIndex: 1 },
|
||||
});
|
||||
|
||||
await page.click('[data-testid="form-dropdown"]');
|
||||
await page.click('[data-testid="copy-redirect-url"]');
|
||||
const text = await page.evaluate(async () => {
|
||||
return navigator.clipboard.readText();
|
||||
});
|
||||
expect(text).toBe(`${CAL_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
|
||||
});
|
||||
|
||||
test("should copy link in RoutingForms list", async ({ page, context, users }) => {
|
||||
await installApps(page, users);
|
||||
context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
|
||||
await page.goto("/apps/routing-forms/forms");
|
||||
const formId = await addRoutingForm(page);
|
||||
await fillRoutingForm(page, {
|
||||
description: "",
|
||||
field: { label: "test", typeIndex: 1 },
|
||||
});
|
||||
|
||||
await page.goto("/apps/routing-forms/forms");
|
||||
await page.click('[data-testid="form-dropdown"]');
|
||||
await page.click('[data-testid="copy-redirect-url"]');
|
||||
const text = await page.evaluate(async () => {
|
||||
return navigator.clipboard.readText();
|
||||
});
|
||||
expect(text).toBe(`${CAL_URL}/router?form=${formId}&test={Recalled_Response_For_This_Field}`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
description: The world's fastest calendar, beautifully designed for a remote world
|
||||
items:
|
||||
- /api/app-store/vimcal/1.gif
|
||||
- /api/app-store/vimcal/2.gif
|
||||
- /api/app-store/vimcal/3.gif
|
||||
- /api/app-store/vimcal/4.gif
|
||||
---
|
||||
|
||||
{description}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import config from "./config.json";
|
||||
|
||||
export const metadata = {
|
||||
category: "other",
|
||||
...config,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
|
@ -0,0 +1,21 @@
|
|||
import { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import { createDefaultInstallation } from "../../_utils/installation";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
// Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
redirect: {
|
||||
newTab: true,
|
||||
url: "https://cal.com/blog/cal-plus-vimcal",
|
||||
},
|
||||
createCredential: ({ appType, user, slug }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -0,0 +1 @@
|
|||
export { default as add } from "./add";
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Vimcal",
|
||||
"slug": "vimcal",
|
||||
"type": "vimcal_other",
|
||||
"imageSrc": "/api/app-store/vimcal/icon.svg",
|
||||
"logo": "/api/app-store/vimcal/icon.svg",
|
||||
"url": "https://cal.com/apps/vimcal",
|
||||
"variant": "other",
|
||||
"categories": ["calendar"],
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "support@cal.com",
|
||||
"description": "The world's fastest calendar, beautifully designed for a remote world\r",
|
||||
"extendsFeature": "User",
|
||||
"__createdUsingCli": true
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * as api from "./api";
|
||||
export { metadata } from "./_metadata";
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/vimcal",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"description": "The world's fastest calendar, beautifully designed for a remote world\r",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.8 MiB |
After Width: | Height: | Size: 844 KiB |
After Width: | Height: | Size: 2.1 MiB |
After Width: | Height: | Size: 1.6 MiB |
|
@ -0,0 +1,28 @@
|
|||
<svg width="62" height="58" viewBox="0 0 62 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.90676 11.7682C0.331514 15.5535 -0.904363 20.8542 2.31152 24.925C4.55796 27.7688 8.12817 28.9722 11.4731 28.3502C25.0725 15.2446 38.1409 10.9884 48.1289 10.1525C49.6733 10.0233 51.1425 9.97591 52.5271 9.98991C55.2643 7.11068 58.346 4.54111 61.7196 2.35078L61.7443 2.28935C61.7443 2.28935 47.1718 -1.08376 32.9863 0.929534C20.5999 2.68751 8.38054 7.03215 3.90676 11.7682Z" fill="url(#paint0_linear_1303_1047)"/>
|
||||
<path d="M13.9571 39.6669C11.2816 36.28 11.3264 31.6336 13.7764 28.3277C26.6775 16.3673 38.9317 12.4894 48.26 11.7088L50.7032 12.0128C46.1398 17.3558 42.6942 23.6199 40.6579 30.4189C37.9724 31.1454 35.321 32.4756 32.801 34.2211C29.4853 36.5177 26.3539 39.561 23.607 42.988C20.1257 43.8278 16.3134 42.6495 13.9571 39.6669Z" fill="url(#paint1_linear_1303_1047)"/>
|
||||
<path d="M24.7519 44.0672C24.5613 44.4084 24.3899 44.7653 24.2397 45.1369C22.2998 49.9396 24.6453 55.3958 29.4789 57.3235C31.0785 57.9616 32.7496 58.1333 34.3407 57.9032C35.9331 57.6814 37.4896 57.0516 38.8456 55.9941C39.1282 55.7738 39.3946 55.5405 39.6449 55.296C39.4201 54.2267 39.2282 53.1435 39.0705 52.047C38.0873 45.208 38.525 38.4931 40.1611 32.1969C37.9955 32.9019 35.8236 34.0315 33.7003 35.5022C30.4914 37.7248 27.4375 40.6986 24.7519 44.0672Z" fill="url(#paint2_linear_1303_1047)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1303_1047" x1="57.8884" y1="7.12205" x2="-11.0432" y2="37.0341" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB6BE"/>
|
||||
<stop offset="0.113339" stop-color="#FA6753"/>
|
||||
<stop offset="0.432815" stop-color="#F50058"/>
|
||||
<stop offset="0.61187" stop-color="#F50081"/>
|
||||
<stop offset="0.77952" stop-color="#C71FD6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1303_1047" x1="57.8876" y1="7.12205" x2="-11.0441" y2="37.0341" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB6BE"/>
|
||||
<stop offset="0.113339" stop-color="#FA6753"/>
|
||||
<stop offset="0.432815" stop-color="#F50058"/>
|
||||
<stop offset="0.61187" stop-color="#F50081"/>
|
||||
<stop offset="0.77952" stop-color="#C71FD6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1303_1047" x1="57.8879" y1="7.12206" x2="-11.0437" y2="37.0341" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB6BE"/>
|
||||
<stop offset="0.113339" stop-color="#FA6753"/>
|
||||
<stop offset="0.432815" stop-color="#F50058"/>
|
||||
<stop offset="0.61187" stop-color="#F50081"/>
|
||||
<stop offset="0.77952" stop-color="#C71FD6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -47,6 +47,7 @@ const getEventType = async (id: number) => {
|
|||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
date: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -225,18 +226,32 @@ export async function getUserAvailability(
|
|||
const startGetWorkingHours = performance.now();
|
||||
|
||||
const timeZone = schedule.timeZone || eventType?.timeZone || currentUser.timeZone;
|
||||
const workingHours = getWorkingHours(
|
||||
{ timeZone },
|
||||
|
||||
const availability =
|
||||
schedule.availability ||
|
||||
(eventType?.availability.length ? eventType.availability : currentUser.availability)
|
||||
);
|
||||
(eventType?.availability.length ? eventType.availability : currentUser.availability);
|
||||
|
||||
const workingHours = getWorkingHours({ timeZone }, availability);
|
||||
|
||||
const endGetWorkingHours = performance.now();
|
||||
logger.debug(`getWorkingHours took ${endGetWorkingHours - startGetWorkingHours}ms for userId ${userId}`);
|
||||
|
||||
const dateOverrides = availability
|
||||
.filter((availability) => !!availability.date)
|
||||
.map((override) => {
|
||||
const startTime = dayjs.utc(override.startTime);
|
||||
const endTime = dayjs.utc(override.endTime);
|
||||
return {
|
||||
start: dayjs.utc(override.date).hour(startTime.hour()).minute(startTime.minute()).toDate(),
|
||||
end: dayjs.utc(override.date).hour(endTime.hour()).minute(endTime.minute()).toDate(),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
busy: bufferedBusyTimes,
|
||||
timeZone,
|
||||
workingHours,
|
||||
dateOverrides,
|
||||
currentSeats,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat";
|
|||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isToday from "dayjs/plugin/isToday";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import minmax from "dayjs/plugin/minMax";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import timeZone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
|
@ -44,6 +45,7 @@ dayjs.extend(relativeTime);
|
|||
dayjs.extend(timeZone);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(minmax);
|
||||
|
||||
export type Dayjs = dayjs.Dayjs;
|
||||
|
||||
|
|
|
@ -1,30 +1,33 @@
|
|||
import { LinkIcon } from "./LinkIcon";
|
||||
|
||||
export const CallToAction = (props: { label: string; href: string }) => (
|
||||
export const CallToAction = (props: { label: string; href: string; secondary?: boolean }) => (
|
||||
<p
|
||||
style={{
|
||||
display: "inline-block",
|
||||
background: "#292929",
|
||||
background: props.secondary ? "#FFFFFF" : "#292929",
|
||||
border: props.secondary ? "1px solid #d1d5db" : "",
|
||||
color: "#ffffff",
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: "16px",
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
lineHeight: "120%",
|
||||
lineHeight: "20px",
|
||||
margin: 0,
|
||||
textDecoration: "none",
|
||||
textTransform: "none",
|
||||
padding: "10px 25px",
|
||||
padding: "10px 16px",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
msoPaddingAlt: "0px",
|
||||
borderRadius: "3px",
|
||||
borderRadius: "6px",
|
||||
boxSizing: "border-box",
|
||||
}}>
|
||||
<a
|
||||
style={{ color: "#FFFFFF", textDecoration: "none" }}
|
||||
style={{ color: props.secondary ? "#292929" : "#FFFFFF", textDecoration: "none" }}
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
{props.label} <LinkIcon />
|
||||
{props.label}
|
||||
<LinkIcon secondary />
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,6 @@ export const CallToActionTable = (props: { children: React.ReactNode }) => (
|
|||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
bgcolor="#292929"
|
||||
role="presentation"
|
||||
style={{
|
||||
border: "none",
|
||||
|
@ -13,7 +12,6 @@ export const CallToActionTable = (props: { children: React.ReactNode }) => (
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
msoPaddingAlt: "10px 25px",
|
||||
background: "#292929",
|
||||
}}
|
||||
valign="middle">
|
||||
{props.children}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
export const LinkIcon = () => (
|
||||
export const LinkIcon = ({ secondary }: { secondary?: boolean }) => (
|
||||
<img
|
||||
src={IS_PRODUCTION ? BASE_URL + "/emails/linkIcon.png" : "https://app.cal.com/emails/linkIcon.png"}
|
||||
width="12px"
|
||||
srcSet={IS_PRODUCTION ? BASE_URL + "/emails/linkIcon.svg" : "https://app.cal.com/emails/linkIcon.svg"}
|
||||
width="16px"
|
||||
style={{ marginBottom: "-3px", marginLeft: "8px", ...(secondary && { filter: "brightness(80%)" }) }}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const Separator = () => (
|
||||
<p style={{ width: "16px", height: "16px", display: "inline-block" }}> </p>
|
||||
);
|
|
@ -0,0 +1,191 @@
|
|||
/* eslint-disable @next/next/no-head-element */
|
||||
import BaseTable from "./BaseTable";
|
||||
import EmailBodyLogo from "./EmailBodyLogo";
|
||||
import EmailHead from "./EmailHead";
|
||||
import EmailScheduledBodyHeaderContent from "./EmailScheduledBodyHeaderContent";
|
||||
import EmailSchedulingBodyDivider from "./EmailSchedulingBodyDivider";
|
||||
import EmailSchedulingBodyHeader, { BodyHeadType } from "./EmailSchedulingBodyHeader";
|
||||
import RawHtml from "./RawHtml";
|
||||
import Row from "./Row";
|
||||
|
||||
const Html = (props: { children: React.ReactNode }) => (
|
||||
<>
|
||||
<RawHtml html="<!doctype html>" />
|
||||
<html>{props.children}</html>
|
||||
</>
|
||||
);
|
||||
|
||||
export const V2BaseEmailHtml = (props: {
|
||||
children: React.ReactNode;
|
||||
callToAction?: React.ReactNode;
|
||||
subject: string;
|
||||
title?: string;
|
||||
subtitle?: React.ReactNode;
|
||||
headerType?: BodyHeadType;
|
||||
}) => {
|
||||
return (
|
||||
<Html>
|
||||
<EmailHead title={props.subject} />
|
||||
<body style={{ wordSpacing: "normal", backgroundColor: "#F5F5F5" }}>
|
||||
<div style={{ backgroundColor: "#F5F5F5" }}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
<div style={{ margin: "0px auto", maxWidth: 600 }}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: "0px",
|
||||
padding: "0px",
|
||||
paddingTop: "40px",
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->`}
|
||||
/>
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
{props.headerType && <EmailSchedulingBodyHeader headerType={props.headerType} />}
|
||||
{props.title && <EmailScheduledBodyHeaderContent title={props.title} subtitle={props.subtitle} />}
|
||||
{(props.headerType || props.title || props.subtitle) && <EmailSchedulingBodyDivider />}
|
||||
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
}}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: 0,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
border: "1px solid #E1E1E1",
|
||||
borderRadius: "6px",
|
||||
}}>
|
||||
<Row
|
||||
border="0"
|
||||
style={{
|
||||
verticalAlign: "top",
|
||||
borderRadius: "6px",
|
||||
background: "#FFFFFF",
|
||||
backgroundColor: "#FFFFFF",
|
||||
}}
|
||||
width="100%">
|
||||
<td
|
||||
align="left"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
padding: "40px",
|
||||
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
textAlign: "left",
|
||||
color: "#3E3E3E",
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
{props.callToAction && <EmailSchedulingBodyDivider />}
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
}}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: 0,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
|
||||
/>
|
||||
{props.callToAction && (
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
}}>
|
||||
<BaseTable border="0" style={{ verticalAlign: "top" }} width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
vertical-align="middle"
|
||||
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
{props.callToAction}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 13,
|
||||
lineHeight: 1,
|
||||
textAlign: "left",
|
||||
color: "#000000",
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</BaseTable>
|
||||
</div>
|
||||
)}
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
<EmailBodyLogo />
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</div>
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
};
|
|
@ -3,21 +3,27 @@ import { RRule } from "rrule";
|
|||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
function getRecurringWhen({ calEvent }: { calEvent: CalendarEvent }) {
|
||||
if (calEvent.recurringEvent) {
|
||||
const t = calEvent.attendees[0].language.translate;
|
||||
const rruleOptions = new RRule(calEvent.recurringEvent).options;
|
||||
const recurringEvent: RecurringEvent = {
|
||||
export function getRecurringWhen({
|
||||
recurringEvent,
|
||||
attendee,
|
||||
}: {
|
||||
recurringEvent?: RecurringEvent | null;
|
||||
attendee: Pick<Person, "language">;
|
||||
}) {
|
||||
if (recurringEvent) {
|
||||
const t = attendee.language.translate;
|
||||
const rruleOptions = new RRule(recurringEvent).options;
|
||||
const recurringEventConfig: RecurringEvent = {
|
||||
freq: rruleOptions.freq,
|
||||
count: rruleOptions.count || 1,
|
||||
interval: rruleOptions.interval,
|
||||
};
|
||||
return ` - ${getEveryFreqFor({ t, recurringEvent })}`;
|
||||
return `${getEveryFreqFor({ t, recurringEvent: recurringEventConfig })}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
@ -33,10 +39,15 @@ export function WhenInfo(props: { calEvent: CalendarEvent; timeZone: string; t:
|
|||
return dayjs(props.calEvent.endTime).tz(timeZone).format(format);
|
||||
}
|
||||
|
||||
const recurringInfo = getRecurringWhen({
|
||||
recurringEvent: props.calEvent.recurringEvent,
|
||||
attendee: props.calEvent.attendees[0],
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Info
|
||||
label={`${t("when")} ${getRecurringWhen(props)}`}
|
||||
label={`${t("when")} ${recurringInfo !== "" ? ` - ${recurringInfo}` : ""}`}
|
||||
lineThrough={
|
||||
!!props.calEvent.cancellationReason && !props.calEvent.cancellationReason.includes("$RCH$")
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { BaseEmailHtml } from "./BaseEmailHtml";
|
||||
export { V2BaseEmailHtml } from "./V2BaseEmailHtml";
|
||||
export { CallToAction } from "./CallToAction";
|
||||
export { CallToActionTable } from "./CallToActionTable";
|
||||
export { CustomInputs } from "./CustomInputs";
|
||||
|
@ -10,3 +11,4 @@ export { default as RawHtml } from "./RawHtml";
|
|||
export { WhenInfo } from "./WhenInfo";
|
||||
export { WhoInfo } from "./WhoInfo";
|
||||
export { AppsStatus } from "./AppsStatus";
|
||||
export { Separator } from "./Separator";
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import { CallToAction, CallToActionTable } from "../components";
|
||||
import { createHmac } from "crypto";
|
||||
|
||||
import { CallToAction, CallToActionTable, Separator } from "../components";
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
|
||||
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
|
||||
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => {
|
||||
const signedData = `${props.attendee.email}/${props.calEvent.uid}`;
|
||||
const signature = createHmac("sha1", CALENDSO_ENCRYPTION_KEY).update(signedData).digest("base64");
|
||||
return (
|
||||
<OrganizerScheduledEmail
|
||||
title={
|
||||
props.title || props.calEvent.recurringEvent?.count
|
||||
|
@ -14,14 +21,22 @@ export const OrganizerRequestEmail = (props: React.ComponentProps<typeof Organiz
|
|||
callToAction={
|
||||
<CallToActionTable>
|
||||
<CallToAction
|
||||
label={props.calEvent.organizer.language.translate("confirm_or_reject_request")}
|
||||
href={
|
||||
process.env.NEXT_PUBLIC_WEBAPP_URL +
|
||||
(props.calEvent.recurringEvent?.count ? "/bookings/recurring" : "/bookings/upcoming")
|
||||
}
|
||||
label={props.calEvent.organizer.language.translate("accept")}
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/booking/direct/accept/${encodeURIComponent(
|
||||
props.attendee.email
|
||||
)}/${encodeURIComponent(props.calEvent.uid as string)}/${encodeURIComponent(signature)}`}
|
||||
/>
|
||||
<Separator />
|
||||
<CallToAction
|
||||
label={props.calEvent.organizer.language.translate("reject")}
|
||||
secondary
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/booking/direct/reject/${encodeURIComponent(
|
||||
props.attendee.email
|
||||
)}/${encodeURIComponent(props.calEvent.uid as string)}/${encodeURIComponent(signature)}`}
|
||||
/>
|
||||
</CallToActionTable>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { APP_NAME, BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
import { V2BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
type TeamInvite = {
|
||||
language: TFunction;
|
||||
|
@ -12,15 +12,36 @@ type TeamInvite = {
|
|||
joinLink: string;
|
||||
};
|
||||
|
||||
export const TeamInviteEmail = (props: TeamInvite & Partial<React.ComponentProps<typeof BaseEmailHtml>>) => {
|
||||
export const TeamInviteEmail = (
|
||||
props: TeamInvite & Partial<React.ComponentProps<typeof V2BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
<V2BaseEmailHtml
|
||||
subject={props.language("user_invited_you", {
|
||||
user: props.from,
|
||||
team: props.teamName,
|
||||
appName: APP_NAME,
|
||||
})}>
|
||||
<p>
|
||||
<img
|
||||
height="64"
|
||||
src={
|
||||
IS_PRODUCTION
|
||||
? BASE_URL + "/emails/teamCircle@2x.png"
|
||||
: "http://localhost:3000/emails/teamCircle@2x.png"
|
||||
}
|
||||
style={{
|
||||
border: "0",
|
||||
display: "block",
|
||||
outline: "none",
|
||||
textDecoration: "none",
|
||||
height: "64px",
|
||||
fontSize: "13px",
|
||||
marginBottom: "24px",
|
||||
}}
|
||||
width="64"
|
||||
alt=""
|
||||
/>
|
||||
<p style={{ fontSize: "24px", marginBottom: "16px" }}>
|
||||
<>
|
||||
{props.language("user_invited_you", {
|
||||
user: props.from,
|
||||
|
@ -30,21 +51,22 @@ export const TeamInviteEmail = (props: TeamInvite & Partial<React.ComponentProps
|
|||
!
|
||||
</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "32px" }}>
|
||||
<>{props.language("calcom_explained", { appName: APP_NAME })}</>
|
||||
</p>
|
||||
<CallToAction label={props.language("accept_invitation")} href={props.joinLink} />
|
||||
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<div style={{ borderTop: "1px solid #E1E1E1", marginTop: "32px", paddingTop: "32px" }}>
|
||||
<p style={{ fontWeight: 400, margin: 0 }}>
|
||||
<>
|
||||
{props.language("have_any_questions")}{" "}
|
||||
<a href="mailto:support@cal.com" style={{ color: "#3E3E3E" }} target="_blank" rel="noreferrer">
|
||||
<>{props.language("contact_our_support_team")}</>
|
||||
</a>
|
||||
<>{props.language("contact")}</>
|
||||
</a>{" "}
|
||||
{props.language("our_support_team")}
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# Availability related code will live here
|
||||
|
||||
- [ ] Maybe migrate `getBusyTimes` here
|
||||
- [ ] Maybe migrate `getUserAvailability` here (or into `users` feature)
|
|
@ -195,6 +195,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
},
|
||||
availability: {
|
||||
select: {
|
||||
date: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
|
@ -834,6 +835,8 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
|||
evt.appsStatus = Object.values(calcAppsStatus);
|
||||
}
|
||||
|
||||
let videoCallUrl;
|
||||
|
||||
if (originalRescheduledBooking?.uid) {
|
||||
// Use EventManager to conditionally use all needed integrations.
|
||||
const updateManager = await eventManager.reschedule(
|
||||
|
@ -868,9 +871,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
|||
metadata.conferenceData = updatedEvent.conferenceData;
|
||||
metadata.entryPoints = updatedEvent.entryPoints;
|
||||
handleAppsStatus(results, booking);
|
||||
videoCallUrl = metadata.hangoutLink || videoCallUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (noEmail !== true) {
|
||||
await sendRescheduledEmails({
|
||||
...evt,
|
||||
|
@ -892,6 +895,9 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
|||
|
||||
results = createManager.results;
|
||||
referencesToCreate = createManager.referencesToCreate;
|
||||
|
||||
videoCallUrl = evt.videoCallData && evt.videoCallData.url ? evt.videoCallData.url : null;
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
const error = {
|
||||
errorCode: "BookingCreatingMeetingFailed",
|
||||
|
@ -908,6 +914,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
|||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||
handleAppsStatus(results, booking);
|
||||
videoCallUrl = metadata.hangoutLink || videoCallUrl;
|
||||
}
|
||||
if (noEmail !== true) {
|
||||
await sendScheduledEmails({
|
||||
|
@ -944,7 +951,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
|||
}
|
||||
|
||||
log.debug(`Booking ${organizerUser.username} completed`);
|
||||
|
||||
const metadata = videoCallUrl ? { videoCallUrl } : undefined;
|
||||
if (isConfirmedByDefault) {
|
||||
const eventTrigger: WebhookTriggerEvents = rescheduleUid
|
||||
? WebhookTriggerEvents.BOOKING_RESCHEDULED
|
||||
|
@ -1000,7 +1007,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
|||
...eventTypeInfo,
|
||||
bookingId,
|
||||
rescheduleUid,
|
||||
metadata: reqBody.metadata,
|
||||
metadata: { ...metadata, ...reqBody.metadata },
|
||||
eventTypeId,
|
||||
status: "ACCEPTED",
|
||||
smsReminderNumber: booking?.smsReminderNumber || undefined,
|
||||
|
@ -1042,6 +1049,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
|||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
metadata,
|
||||
references: {
|
||||
createMany: {
|
||||
data: referencesToCreate,
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import dayjs from "@calcom/dayjs";
|
||||
import { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { CalendarEvent } from "./types/events";
|
||||
|
||||
const startDate = dayjs().set("hour", 11).set("minute", 0);
|
||||
|
||||
export const events: CalendarEvent[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Event 1",
|
||||
start: startDate.add(10, "minutes").toDate(),
|
||||
end: startDate.add(45, "minutes").toDate(),
|
||||
allDay: false,
|
||||
source: "Booking",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Event 2",
|
||||
start: startDate.add(1, "day").toDate(),
|
||||
end: startDate.add(1, "day").add(30, "minutes").toDate(),
|
||||
allDay: false,
|
||||
source: "Booking",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Event 3",
|
||||
start: startDate.add(2, "day").toDate(),
|
||||
end: startDate.add(2, "day").add(60, "minutes").toDate(),
|
||||
allDay: false,
|
||||
source: "Booking",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Event 4",
|
||||
start: startDate.add(3, "day").toDate(),
|
||||
end: startDate.add(3, "day").add(2, "hour").add(30, "minutes").toDate(),
|
||||
allDay: false,
|
||||
source: "Booking",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Event 4 Overlap",
|
||||
start: startDate.add(3, "day").add(30, "minutes").toDate(),
|
||||
end: startDate.add(3, "day").add(2, "hour").add(45, "minutes").toDate(),
|
||||
allDay: false,
|
||||
source: "Booking",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Event 1 Overlap",
|
||||
start: startDate.toDate(),
|
||||
end: startDate.add(30, "minutes").toDate(),
|
||||
allDay: false,
|
||||
source: "Booking",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Event 1 Overlap Two",
|
||||
start: startDate.toDate(),
|
||||
end: startDate.add(30, "minutes").toDate(),
|
||||
allDay: false,
|
||||
source: "Booking",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
];
|
||||
|
||||
export const blockingDates: TimeRange[] = [
|
||||
{
|
||||
start: startDate.add(1, "day").hour(10).toDate(),
|
||||
end: startDate.add(1, "day").hour(13).toDate(),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { useCalendarStore } from "../state/store";
|
||||
import "../styles/styles.css";
|
||||
import { CalendarComponentProps } from "../types/state";
|
||||
import { getDaysBetweenDates, getHoursToDisplay } from "../utils";
|
||||
import { DateValues } from "./DateValues";
|
||||
import { BlockedList } from "./blocking/BlockedList";
|
||||
import { EmptyCell } from "./event/Empty";
|
||||
import { EventList } from "./event/EventList";
|
||||
import { SchedulerColumns } from "./grid";
|
||||
import { SchedulerHeading } from "./heading/SchedulerHeading";
|
||||
import { HorizontalLines } from "./horizontalLines";
|
||||
import { VeritcalLines } from "./verticalLines";
|
||||
|
||||
export function Calendar(props: CalendarComponentProps) {
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
const containerNav = useRef<HTMLDivElement | null>(null);
|
||||
const containerOffset = useRef<HTMLDivElement | null>(null);
|
||||
const initalState = useCalendarStore((state) => state.initState);
|
||||
|
||||
const startDate = useCalendarStore((state) => state.startDate);
|
||||
const endDate = useCalendarStore((state) => state.endDate);
|
||||
const startHour = useCalendarStore((state) => state.startHour || 0);
|
||||
const endHour = useCalendarStore((state) => state.endHour || 23);
|
||||
const usersCellsStopsPerHour = useCalendarStore((state) => state.gridCellsPerHour || 4);
|
||||
|
||||
const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]);
|
||||
|
||||
const hours = useMemo(() => getHoursToDisplay(startHour || 0, endHour || 23), [startHour, endHour]);
|
||||
|
||||
const numberOfGridStopsPerDay = hours.length * usersCellsStopsPerHour;
|
||||
|
||||
// Initalise State on inital mount
|
||||
useEffect(() => {
|
||||
initalState(props);
|
||||
}, [props, initalState]);
|
||||
|
||||
return (
|
||||
<MobileNotSupported>
|
||||
<div
|
||||
className="scheduler-wrapper flex h-full w-full flex-col overflow-y-scroll"
|
||||
style={
|
||||
{ "--one-minute-height": `calc(1.75rem/(60/${usersCellsStopsPerHour}))` } as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
|
||||
}>
|
||||
<SchedulerHeading />
|
||||
<div ref={container} className="relative isolate flex flex-auto flex-col bg-white">
|
||||
<div
|
||||
style={{ width: "165%" }}
|
||||
className="flex max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
|
||||
<DateValues containerNavRef={containerNav} days={days} />
|
||||
{/* TODO: Implement this at a later date. */}
|
||||
{/* <CurrentTime
|
||||
containerNavRef={containerNav}
|
||||
containerOffsetRef={containerOffset}
|
||||
containerRef={container}
|
||||
/> */}
|
||||
<div className="flex flex-auto">
|
||||
<div className="sticky left-0 z-10 w-14 flex-none bg-white ring-1 ring-gray-100" />
|
||||
<div className="grid flex-auto grid-cols-1 grid-rows-1 ">
|
||||
<HorizontalLines
|
||||
hours={hours}
|
||||
numberOfGridStopsPerCell={usersCellsStopsPerHour}
|
||||
containerOffsetRef={containerOffset}
|
||||
/>
|
||||
<VeritcalLines days={days} />
|
||||
|
||||
{/* Empty Cells */}
|
||||
<SchedulerColumns
|
||||
zIndex={50}
|
||||
offsetHeight={containerOffset.current?.offsetHeight}
|
||||
gridStopsPerDay={numberOfGridStopsPerDay}>
|
||||
<>
|
||||
{[...Array(days.length)].map((_, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
gridRow: `2 / span ${numberOfGridStopsPerDay}`,
|
||||
position: "relative",
|
||||
}}>
|
||||
{/* While startDate < endDate: */}
|
||||
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
|
||||
const key = `${i}-${j}`;
|
||||
return (
|
||||
<EmptyCell
|
||||
key={key}
|
||||
day={days[i].toDate()}
|
||||
gridCellIdx={j}
|
||||
totalGridCells={numberOfGridStopsPerDay}
|
||||
selectionLength={endHour - startHour}
|
||||
startHour={startHour}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
</SchedulerColumns>
|
||||
|
||||
<SchedulerColumns
|
||||
offsetHeight={containerOffset.current?.offsetHeight}
|
||||
gridStopsPerDay={numberOfGridStopsPerDay}>
|
||||
{/*Loop over events per day */}
|
||||
{days.map((day, i) => {
|
||||
return (
|
||||
<li key={day.toISOString()} className="relative" style={{ gridColumnStart: i + 1 }}>
|
||||
<EventList day={day} />
|
||||
<BlockedList day={day} containerRef={container} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</SchedulerColumns>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MobileNotSupported>
|
||||
);
|
||||
}
|
||||
|
||||
/** @todo Will be removed once we have mobile support */
|
||||
const MobileNotSupported = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full flex-col items-center justify-center sm:hidden">
|
||||
<h1 className="text-2xl font-bold">Mobile not supported yet </h1>
|
||||
<p className="text-gray-500">Please use a desktop browser to view this page</p>
|
||||
</div>
|
||||
<div className="hidden sm:block">{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
import React from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
type Props = {
|
||||
days: dayjs.Dayjs[];
|
||||
containerNavRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function DateValues({ days, containerNavRef }: Props) {
|
||||
return (
|
||||
<div
|
||||
ref={containerNavRef}
|
||||
className="sticky top-0 z-30 flex-none border-b border-b-gray-300 bg-white sm:pr-8">
|
||||
<div className="flex text-sm leading-6 text-gray-500 sm:hidden" data-dayslength={days.length}>
|
||||
{days.map((day) => {
|
||||
const isToday = dayjs().isSame(day, "day");
|
||||
return (
|
||||
<button
|
||||
key={day.toString()}
|
||||
type="button"
|
||||
className="flex flex-1 flex-col items-center pt-2 pb-3">
|
||||
{day.format("dd")}{" "}
|
||||
<span
|
||||
className={classNames(
|
||||
"mt-1 flex h-8 w-8 items-center justify-center font-semibold text-gray-900",
|
||||
isToday && "rounded-full bg-gray-900 text-white"
|
||||
)}>
|
||||
{day.format("D")}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="-mr-px hidden auto-cols-fr text-sm leading-6 text-gray-500 sm:flex ">
|
||||
<div className="col-end-1 w-14" />
|
||||
{days.map((day) => {
|
||||
const isToday = dayjs().isSame(day, "day");
|
||||
return (
|
||||
<div
|
||||
key={day.toString()}
|
||||
className={classNames("flex flex-1 items-center justify-center py-3", isToday && "font-bold")}>
|
||||
<span>
|
||||
{day.format("ddd")}{" "}
|
||||
<span
|
||||
className={classNames(
|
||||
"items-center justify-center p-1",
|
||||
isToday && "rounded-full bg-gray-900 text-white"
|
||||
)}>
|
||||
{day.format("DD")}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
import { useMemo } from "react";
|
||||
import shallow from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
import { BlockedTimeCell } from "./BlockedTimeCell";
|
||||
|
||||
type Props = {
|
||||
day: dayjs.Dayjs;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
function roundX(x: number, roundBy: number) {
|
||||
return Math.round(x / roundBy) * roundBy;
|
||||
}
|
||||
|
||||
type BlockedDayProps = {
|
||||
startHour: number;
|
||||
endHour: number;
|
||||
day: dayjs.Dayjs;
|
||||
};
|
||||
|
||||
function BlockedBeforeToday({ day, startHour, endHour }: BlockedDayProps) {
|
||||
return (
|
||||
<>
|
||||
{day.isBefore(dayjs(), "day") && (
|
||||
<div
|
||||
key={day.format("YYYY-MM-DD")}
|
||||
className="absolute z-40 w-full"
|
||||
style={{
|
||||
top: `var(--one-minute-height)`,
|
||||
zIndex: 60,
|
||||
height: `calc(${(endHour + 1 - startHour) * 60} * var(--one-minute-height))`, // Add 1 to endHour to include the last hour that we add to display the last vertical line
|
||||
}}>
|
||||
<BlockedTimeCell />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
function BlockedToday({
|
||||
day,
|
||||
startHour,
|
||||
gridCellsPerHour,
|
||||
endHour,
|
||||
}: BlockedDayProps & { gridCellsPerHour: number }) {
|
||||
const dayStart = useMemo(() => day.startOf("day").hour(startHour), [day, startHour]);
|
||||
const dayEnd = useMemo(() => day.startOf("day").hour(endHour), [day, endHour]);
|
||||
const dayEndInMinutes = useMemo(() => dayEnd.diff(dayStart, "minutes"), [dayEnd, dayStart]);
|
||||
let nowComparedToDayStart = useMemo(() => dayjs().diff(dayStart, "minutes"), [dayStart]);
|
||||
|
||||
if (nowComparedToDayStart > dayEndInMinutes) nowComparedToDayStart = dayEndInMinutes;
|
||||
|
||||
return (
|
||||
<>
|
||||
{day.isToday() && (
|
||||
<div
|
||||
key={day.format("YYYY-MM-DD")}
|
||||
className="absolute z-40 w-full"
|
||||
style={{
|
||||
top: `var(--one-minute-height)`, // Still need this as this var takes into consideration the offset of the "AllDayEvents" bar
|
||||
zIndex: 60,
|
||||
height: `calc(${roundX(
|
||||
nowComparedToDayStart,
|
||||
60 / gridCellsPerHour
|
||||
)} * var(--one-minute-height) - 2px)`, // We minus the border width to make it 🧹
|
||||
}}>
|
||||
<BlockedTimeCell />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function BlockedList({ day }: Props) {
|
||||
const { startHour, blockingDates, endHour, gridCellsPerHour } = useCalendarStore(
|
||||
(state) => ({
|
||||
startHour: state.startHour || 0,
|
||||
endHour: state.endHour || 23,
|
||||
blockingDates: state.blockingDates,
|
||||
gridCellsPerHour: state.gridCellsPerHour || 4,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockedBeforeToday day={day} startHour={startHour} endHour={endHour} />
|
||||
<BlockedToday gridCellsPerHour={gridCellsPerHour} day={day} startHour={startHour} endHour={endHour} />
|
||||
{blockingDates &&
|
||||
blockingDates.map((event, i) => {
|
||||
const dayStart = day.startOf("day").hour(startHour);
|
||||
const blockingStart = dayjs(event.start);
|
||||
const eventEnd = dayjs(event.end);
|
||||
|
||||
const eventStart = dayStart.isAfter(blockingStart) ? dayStart : blockingStart;
|
||||
|
||||
if (!eventStart.isSame(day, "day")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (eventStart.isBefore(dayjs())) {
|
||||
if (eventEnd.isBefore(dayjs())) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const eventDuration = eventEnd.diff(eventStart, "minutes");
|
||||
|
||||
const eventStartHour = eventStart.hour();
|
||||
const eventStartDiff = (eventStartHour - (startHour || 0)) * 60;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${eventStart.toISOString()}-${i}`}
|
||||
className="absolute w-full"
|
||||
style={{
|
||||
zIndex: 60,
|
||||
top: `calc(${eventStartDiff}*var(--one-minute-height))`,
|
||||
height: `calc(${eventDuration}*var(--one-minute-height))`,
|
||||
}}>
|
||||
<BlockedTimeCell />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { classNames } from "@calcom/lib";
|
||||
|
||||
export function BlockedTimeCell() {
|
||||
return (
|
||||
<div
|
||||
className={classNames("group absolute inset-0 flex h-full flex-col hover:cursor-not-allowed")}
|
||||
style={{
|
||||
backgroundColor: "#D1D5DB",
|
||||
opacity: 0.2,
|
||||
background:
|
||||
"repeating-linear-gradient( -45deg, #E5E7EB, #E5E7EB 4.5px, #D1D5DB 4.5px, #D1D5DB 22.5px )",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
Examples,
|
||||
Example,
|
||||
Note,
|
||||
Title,
|
||||
CustomArgsTable,
|
||||
VariantsTable,
|
||||
VariantRow,
|
||||
} from "@calcom/storybook/components";
|
||||
|
||||
import { events, blockingDates } from "../_storybookData";
|
||||
import "../styles/styles.css";
|
||||
import { CalendarEvent } from "../types/events";
|
||||
import { Calendar } from "./Calendar";
|
||||
|
||||
<Meta title="UI/Calendar" component={Calendar} />
|
||||
|
||||
<Title title="Calendar" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
|
||||
|
||||
## Props
|
||||
|
||||
The Args Table below shows you a breakdown of what props can be passed into the Calendar component. All props should have a desciption to make it self explanitory to see what is going on.
|
||||
|
||||
<CustomArgsTable of={Calendar} />
|
||||
|
||||
## Example
|
||||
|
||||
There will be a few examples of how to use the Calendar component to show different usecases.
|
||||
|
||||
export const Template = (args) => <Calendar {...args} />;
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Customising Start Hour and EndHour"
|
||||
argTypes={{
|
||||
startHour: {
|
||||
control: { type: "number", min: 0, max: 23, step: 1 },
|
||||
},
|
||||
endHour: {
|
||||
control: { type: "number", min: 0, max: 23, step: 1 },
|
||||
},
|
||||
}}
|
||||
args={{
|
||||
sortEvents: true,
|
||||
startHour: 8,
|
||||
endHour: 20,
|
||||
events: events,
|
||||
hoverEventDuration: 0,
|
||||
blockingDates: blockingDates,
|
||||
}}>
|
||||
{Template.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
<Canvas>
|
||||
<Story name="Onclick Handlers">
|
||||
<Calendar
|
||||
startHour={8}
|
||||
endHour={17}
|
||||
events={events}
|
||||
onEventClick={(e) => alert(e.title)}
|
||||
onEmptyCellClick={(date) => alert(date.toString())}
|
||||
sortEvents
|
||||
hoverEventDuration={30}
|
||||
/>
|
||||
</Story>
|
||||
</Canvas>
|
|
@ -0,0 +1,43 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
|
||||
type Props = {
|
||||
containerNavRef: React.RefObject<HTMLDivElement>;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
containerOffsetRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function CurrentTime({ containerOffsetRef }: Props) {
|
||||
const [currentTimePos, setCurrentTimePos] = useState<number>(0);
|
||||
const { startHour, endHour } = useCalendarStore((state) => ({
|
||||
startHour: state.startHour || 0,
|
||||
endHour: state.endHour || 23,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// Set the container scroll position based on the current time.
|
||||
let currentMinute = new Date().getHours() * 60;
|
||||
currentMinute = currentMinute + new Date().getMinutes();
|
||||
|
||||
if (containerOffsetRef.current) {
|
||||
const totalHours = endHour - startHour;
|
||||
const currentTimePos = currentMinute;
|
||||
setCurrentTimePos(currentTimePos);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [startHour, endHour]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute z-40 ml-3 flex h-1 items-center justify-center border-gray-800"
|
||||
aria-hidden="true"
|
||||
style={{ top: `calc(${currentTimePos}*var(--one-minute-height))`, zIndex: 100 }}>
|
||||
{dayjs().format("HH:mm")}
|
||||
<div className="ml-1 h-3 w-1 bg-gray-800" />
|
||||
<div className="h-1 w-full bg-gray-800" />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import shallow from "zustand/shallow";
|
||||
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
import { gridCellToDateTime, GridCellToDateProps } from "../../utils";
|
||||
|
||||
export function EmptyCell(props: GridCellToDateProps) {
|
||||
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
|
||||
(state) => ({
|
||||
onEmptyCellClick: state.onEmptyCellClick,
|
||||
hoverEventDuration: state.hoverEventDuration,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const cellToDate = gridCellToDateTime({
|
||||
day: props.day,
|
||||
gridCellIdx: props.gridCellIdx,
|
||||
totalGridCells: props.totalGridCells,
|
||||
selectionLength: props.selectionLength,
|
||||
startHour: props.startHour,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group w-full"
|
||||
style={{ height: "1.75rem", overflow: "visible" }}
|
||||
onClick={() => onEmptyCellClick && onEmptyCellClick(cellToDate.toDate())}>
|
||||
{hoverEventDuration !== 0 && (
|
||||
<div
|
||||
className="opacity-4 absolute inset-x-1 hidden rounded-[4px] border-[1px] border-gray-900 bg-gray-100
|
||||
py-1
|
||||
px-[6px] text-xs font-semibold leading-5 text-gray-900 hover:bg-gray-200 group-hover:block group-hover:cursor-pointer"
|
||||
style={{
|
||||
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
|
||||
zIndex: 49,
|
||||
width: "90%",
|
||||
}}>
|
||||
<div className="overflow-ellipsis leading-4">{cellToDate.format("HH:mm")}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|