Merge branch 'main' into test-datepicker
This commit is contained in:
commit
3b0e896be6
|
@ -40,7 +40,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||||
// If user is not ADMIN, return only his data.
|
// If user is not ADMIN, return only his data.
|
||||||
if (!isAdmin) where.id = userId;
|
if (!isAdmin) where.id = userId;
|
||||||
const [total, data] = await prisma.$transaction([
|
const [total, data] = await prisma.$transaction([
|
||||||
prisma.user.count(),
|
prisma.user.count({ where }),
|
||||||
prisma.user.findMany({ where, take, skip }),
|
prisma.user.findMany({ where, take, skip }),
|
||||||
]);
|
]);
|
||||||
const users = schemaUsersReadPublic.parse(data);
|
const users = schemaUsersReadPublic.parse(data);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useRouter } from "next/navigation";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmptyScreen } from "@calcom/ui";
|
import { EmptyScreen } from "@calcom/ui";
|
||||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
@ -12,7 +12,7 @@ type AppsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
actions?: (className?: string) => JSX.Element;
|
actions?: (className?: string) => JSX.Element;
|
||||||
emptyStore?: boolean;
|
emptyStore?: boolean;
|
||||||
} & Omit<ComponentProps<typeof Shell>, "actions">;
|
} & Omit<ComponentProps<typeof ShellMain>, "actions">;
|
||||||
|
|
||||||
export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
|
export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -22,7 +22,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||||
|
|
||||||
if (session.status === "loading") return <></>;
|
if (session.status === "loading") return <></>;
|
||||||
return (
|
return (
|
||||||
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
<ShellMain {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
||||||
<div className="flex flex-col xl:flex-row">
|
<div className="flex flex-col xl:flex-row">
|
||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
{emptyStore ? (
|
{emptyStore ? (
|
||||||
|
@ -38,7 +38,6 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</Shell>
|
</ShellMain>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const getLayout = (page: React.ReactElement) => <AppsLayout>{page}</AppsLayout>;
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { ascendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
|
||||||
import type { PeriodType } from "@calcom/prisma/enums";
|
import type { PeriodType } from "@calcom/prisma/enums";
|
||||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
|
@ -462,7 +463,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||||
type="number"
|
type="number"
|
||||||
{...offsetStartLockedProps}
|
{...offsetStartLockedProps}
|
||||||
label={t("offset_start")}
|
label={t("offset_start")}
|
||||||
defaultValue={eventType.offsetStart}
|
|
||||||
{...formMethods.register("offsetStart")}
|
{...formMethods.register("offsetStart")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
hint={t("offset_start_description", {
|
hint={t("offset_start_description", {
|
||||||
|
@ -477,11 +477,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||||
|
|
||||||
type IntervalLimitsKey = keyof IntervalLimit;
|
type IntervalLimitsKey = keyof IntervalLimit;
|
||||||
|
|
||||||
const intervalOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"] as const;
|
const INTERVAL_LIMIT_OPTIONS = ascendingLimitKeys.map((key) => ({
|
||||||
|
|
||||||
const INTERVAL_LIMIT_OPTIONS = intervalOrderKeys.map((key) => ({
|
|
||||||
value: key as keyof IntervalLimit,
|
value: key as keyof IntervalLimit,
|
||||||
label: `Per ${key.split("_")[1].toLocaleLowerCase()}`,
|
label: `Per ${intervalLimitKeyToUnit(key)}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type IntervalLimitItemProps = {
|
type IntervalLimitItemProps = {
|
||||||
|
@ -590,8 +588,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||||
Object.entries(currentIntervalLimits)
|
Object.entries(currentIntervalLimits)
|
||||||
.sort(([limitKeyA], [limitKeyB]) => {
|
.sort(([limitKeyA], [limitKeyB]) => {
|
||||||
return (
|
return (
|
||||||
intervalOrderKeys.indexOf(limitKeyA as IntervalLimitsKey) -
|
ascendingLimitKeys.indexOf(limitKeyA as IntervalLimitsKey) -
|
||||||
intervalOrderKeys.indexOf(limitKeyB as IntervalLimitsKey)
|
ascendingLimitKeys.indexOf(limitKeyB as IntervalLimitsKey)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
@ -99,6 +101,7 @@ const CheckedHostField = ({
|
||||||
isFixed,
|
isFixed,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
helperText,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
labelText: string;
|
labelText: string;
|
||||||
|
@ -107,6 +110,7 @@ const CheckedHostField = ({
|
||||||
value: { isFixed: boolean; userId: number }[];
|
value: { isFixed: boolean; userId: number }[];
|
||||||
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
|
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
|
||||||
options?: Options<CheckedSelectOption>;
|
options?: Options<CheckedSelectOption>;
|
||||||
|
helperText?: React.ReactNode | string;
|
||||||
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-muted flex flex-col space-y-5 p-4">
|
<div className="bg-muted flex flex-col space-y-5 p-4">
|
||||||
|
@ -136,11 +140,24 @@ const CheckedHostField = ({
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
{helperText && <p className="text-subtle text-sm">{helperText}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FixedHostHelper = (
|
||||||
|
<Trans i18nKey="fixed_host_helper">
|
||||||
|
Add anyone who needs to attend the event.
|
||||||
|
<Link
|
||||||
|
className="underline underline-offset-2"
|
||||||
|
target="_blank"
|
||||||
|
href="https://cal.com/docs/enterprise-features/teams/round-robin-scheduling#fixed-hosts">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Trans>
|
||||||
|
);
|
||||||
|
|
||||||
const RoundRobinHosts = ({
|
const RoundRobinHosts = ({
|
||||||
teamMembers,
|
teamMembers,
|
||||||
value,
|
value,
|
||||||
|
@ -167,6 +184,7 @@ const RoundRobinHosts = ({
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={t("add_fixed_hosts")}
|
placeholder={t("add_fixed_hosts")}
|
||||||
labelText={t("fixed_hosts")}
|
labelText={t("fixed_hosts")}
|
||||||
|
helperText={FixedHostHelper}
|
||||||
/>
|
/>
|
||||||
<CheckedHostField
|
<CheckedHostField
|
||||||
options={teamMembers.sort(sortByLabel)}
|
options={teamMembers.sort(sortByLabel)}
|
||||||
|
@ -175,6 +193,7 @@ const RoundRobinHosts = ({
|
||||||
isFixed={false}
|
isFixed={false}
|
||||||
placeholder={t("add_attendees")}
|
placeholder={t("add_attendees")}
|
||||||
labelText={t("round_robin_hosts")}
|
labelText={t("round_robin_hosts")}
|
||||||
|
helperText={t("round_robin_helper")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { usePathname, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
export default function useIsBookingPage() {
|
export default function useIsBookingPage() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isBookingPage = ["/booking", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
|
const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const userParam = searchParams.get("user");
|
const userParam = searchParams.get("user");
|
||||||
|
|
|
@ -127,7 +127,10 @@ const matcherConfigUserTypeEmbedRoute = {
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
i18n,
|
i18n: {
|
||||||
|
...i18n,
|
||||||
|
localeDetection: false,
|
||||||
|
},
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: true,
|
||||||
/* We already do type check on GH actions */
|
/* We already do type check on GH actions */
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, showToast } from "@calcom/ui";
|
import { Button, showToast } from "@calcom/ui";
|
||||||
import { Copy } from "@calcom/ui/components/icon";
|
import { Copy } from "@calcom/ui/components/icon";
|
||||||
|
@ -21,14 +21,11 @@ export default function Error500() {
|
||||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||||
<h1 className="font-cal text-emphasis text-6xl">500</h1>
|
<h1 className="font-cal text-emphasis text-6xl">500</h1>
|
||||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">It's not you, it's us.</h2>
|
<h2 className="text-emphasis mt-6 text-2xl font-medium">It's not you, it's us.</h2>
|
||||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">{t("something_went_wrong_on_our_end")}</p>
|
||||||
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
|
||||||
away for you.
|
|
||||||
</p>
|
|
||||||
{searchParams?.get("error") && (
|
{searchParams?.get("error") && (
|
||||||
<div className="mb-8 flex flex-col">
|
<div className="mb-8 flex flex-col">
|
||||||
<p className="text-default mb-4 max-w-2xl text-sm">
|
<p className="text-default mb-4 max-w-2xl text-sm">
|
||||||
Please provide the following text when contacting support to better help you:
|
{t("please_provide_following_text_to_suppport")}:
|
||||||
</p>
|
</p>
|
||||||
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
|
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
|
||||||
{searchParams?.get("error")}
|
{searchParams?.get("error")}
|
||||||
|
@ -46,9 +43,9 @@ export default function Error500() {
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
<Button href="mailto:support@cal.com">{t("contact_support")}</Button>
|
||||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||||
Go back
|
{t("go_back")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import type { AppProps } from "@lib/app-providers";
|
import type { AppProps } from "@lib/app-providers";
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { ChangeEventHandler } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||||
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||||
|
@ -94,6 +95,7 @@ export default function Apps({
|
||||||
}
|
}
|
||||||
|
|
||||||
Apps.PageWrapper = PageWrapper;
|
Apps.PageWrapper = PageWrapper;
|
||||||
|
Apps.getLayout = getLayout;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req, res } = context;
|
const { req, res } = context;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
@ -130,15 +131,17 @@ export default function AvailabilityPage() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Shell
|
<ShellMain
|
||||||
heading={t("availability")}
|
heading={t("availability")}
|
||||||
hideHeadingOnMobile
|
hideHeadingOnMobile
|
||||||
subtitle={t("configure_availability")}
|
subtitle={t("configure_availability")}
|
||||||
CTA={<NewScheduleButton />}>
|
CTA={<NewScheduleButton />}>
|
||||||
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
|
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
|
||||||
</Shell>
|
</ShellMain>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvailabilityPage.getLayout = getLayout;
|
||||||
|
|
||||||
AvailabilityPage.PageWrapper = PageWrapper;
|
AvailabilityPage.PageWrapper = PageWrapper;
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
import React from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||||
import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
|
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
|
||||||
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
|
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||||
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||||
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { HorizontalTabs } from "@calcom/ui";
|
||||||
|
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
|
||||||
import { Alert, Button, EmptyScreen } from "@calcom/ui";
|
import { Alert, Button, EmptyScreen } from "@calcom/ui";
|
||||||
import { Calendar } from "@calcom/ui/components/icon";
|
import { Calendar } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
@ -32,6 +37,28 @@ type RecurringInfo = {
|
||||||
bookings: { [key: string]: Date[] };
|
bookings: { [key: string]: Date[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
|
{
|
||||||
|
name: "upcoming",
|
||||||
|
href: "/bookings/upcoming",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unconfirmed",
|
||||||
|
href: "/bookings/unconfirmed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recurring",
|
||||||
|
href: "/bookings/recurring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "past",
|
||||||
|
href: "/bookings/past",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cancelled",
|
||||||
|
href: "/bookings/cancelled",
|
||||||
|
},
|
||||||
|
];
|
||||||
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
||||||
|
|
||||||
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
|
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
|
||||||
|
@ -112,90 +139,101 @@ export default function Bookings() {
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookingLayout heading={t("bookings")} subtitle={t("bookings_description")}>
|
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||||
<div className="flex w-full flex-col" ref={animationParentRef}>
|
<div className="flex flex-col">
|
||||||
{query.status === "error" && (
|
<div className="flex flex-col flex-wrap lg:flex-row">
|
||||||
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
|
<HorizontalTabs tabs={tabs} />
|
||||||
)}
|
<div className="max-w-full overflow-x-auto xl:ml-auto">
|
||||||
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
|
<FiltersContainer />
|
||||||
{query.status === "success" && !isEmpty && (
|
</div>
|
||||||
<>
|
</div>
|
||||||
{!!bookingsToday.length && status === "upcoming" && (
|
<main className="w-full">
|
||||||
<div className="mb-6 pt-2 xl:pt-0">
|
<div className="flex w-full flex-col" ref={animationParentRef}>
|
||||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
{query.status === "error" && (
|
||||||
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
|
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
|
||||||
<div className="border-subtle overflow-hidden rounded-md border">
|
)}
|
||||||
<table className="w-full max-w-full table-fixed">
|
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
|
||||||
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
|
{query.status === "success" && !isEmpty && (
|
||||||
<Fragment>
|
<>
|
||||||
{bookingsToday.map((booking: BookingOutput) => (
|
{!!bookingsToday.length && status === "upcoming" && (
|
||||||
<BookingListItem
|
<div className="mb-6 pt-2 xl:pt-0">
|
||||||
key={booking.id}
|
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||||
listingStatus={status}
|
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
|
||||||
recurringInfo={recurringInfoToday}
|
<div className="border-subtle overflow-hidden rounded-md border">
|
||||||
{...booking}
|
<table className="w-full max-w-full table-fixed">
|
||||||
/>
|
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
|
||||||
|
<Fragment>
|
||||||
|
{bookingsToday.map((booking: BookingOutput) => (
|
||||||
|
<BookingListItem
|
||||||
|
key={booking.id}
|
||||||
|
listingStatus={status}
|
||||||
|
recurringInfo={recurringInfoToday}
|
||||||
|
{...booking}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="pt-2 xl:pt-0">
|
||||||
|
<div className="border-subtle overflow-hidden rounded-md border">
|
||||||
|
<table className="w-full max-w-full table-fixed">
|
||||||
|
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
|
||||||
|
{query.data.pages.map((page, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
||||||
|
const recurringInfo = page.recurringInfo.find(
|
||||||
|
(info) => info.recurringEventId === booking.recurringEventId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<BookingListItem
|
||||||
|
key={booking.id}
|
||||||
|
listingStatus={status}
|
||||||
|
recurringInfo={recurringInfo}
|
||||||
|
{...booking}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</div>
|
||||||
|
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
loading={query.isFetchingNextPage}
|
||||||
|
disabled={!query.hasNextPage}
|
||||||
|
onClick={() => query.fetchNextPage()}>
|
||||||
|
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{query.status === "success" && isEmpty && (
|
||||||
|
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={Calendar}
|
||||||
|
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
||||||
|
description={t("no_status_bookings_yet_description", {
|
||||||
|
status: t(status).toLowerCase(),
|
||||||
|
description: t(descriptionByStatus[status]),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-2 xl:pt-0">
|
|
||||||
<div className="border-subtle overflow-hidden rounded-md border">
|
|
||||||
<table className="w-full max-w-full table-fixed">
|
|
||||||
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
|
|
||||||
{query.data.pages.map((page, index) => (
|
|
||||||
<Fragment key={index}>
|
|
||||||
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
|
||||||
const recurringInfo = page.recurringInfo.find(
|
|
||||||
(info) => info.recurringEventId === booking.recurringEventId
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<BookingListItem
|
|
||||||
key={booking.id}
|
|
||||||
listingStatus={status}
|
|
||||||
recurringInfo={recurringInfo}
|
|
||||||
{...booking}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
|
|
||||||
<Button
|
|
||||||
color="minimal"
|
|
||||||
loading={query.isFetchingNextPage}
|
|
||||||
disabled={!query.hasNextPage}
|
|
||||||
onClick={() => query.fetchNextPage()}>
|
|
||||||
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{query.status === "success" && isEmpty && (
|
|
||||||
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
|
||||||
<EmptyScreen
|
|
||||||
Icon={Calendar}
|
|
||||||
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
|
||||||
description={t("no_status_bookings_yet_description", {
|
|
||||||
status: t(status).toLowerCase(),
|
|
||||||
description: t(descriptionByStatus[status]),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</BookingLayout>
|
</ShellMain>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bookings.PageWrapper = PageWrapper;
|
Bookings.PageWrapper = PageWrapper;
|
||||||
|
Bookings.getLayout = getLayout;
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||||
const params = querySchema.safeParse(ctx.params);
|
const params = querySchema.safeParse(ctx.params);
|
||||||
|
|
|
@ -258,6 +258,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
startDate: periodDates.startDate,
|
startDate: periodDates.startDate,
|
||||||
endDate: periodDates.endDate,
|
endDate: periodDates.endDate,
|
||||||
},
|
},
|
||||||
|
offsetStart: eventType.offsetStart,
|
||||||
bookingFields: eventType.bookingFields,
|
bookingFields: eventType.bookingFields,
|
||||||
periodType: eventType.periodType,
|
periodType: eventType.periodType,
|
||||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type { FC } from "react";
|
||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||||
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
||||||
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
||||||
|
@ -15,7 +16,7 @@ import CreateEventTypeDialog from "@calcom/features/eventtypes/components/Create
|
||||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||||
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -932,25 +933,25 @@ const EventTypesPage = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ShellMain
|
||||||
|
withoutSeo
|
||||||
|
heading={t("event_types_page_title")}
|
||||||
|
hideHeadingOnMobile
|
||||||
|
subtitle={t("event_types_page_subtitle")}
|
||||||
|
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
|
||||||
|
beforeCTAactions={<Actions />}
|
||||||
|
CTA={<CTA data={data} />}>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title="Event Types"
|
title="Event Types"
|
||||||
description="Create events to share for people to book on your calendar."
|
description="Create events to share for people to book on your calendar."
|
||||||
/>
|
/>
|
||||||
<Shell
|
<Main data={data} status={status} error={error} filters={filters} />
|
||||||
withoutSeo
|
</ShellMain>
|
||||||
heading={t("event_types_page_title")}
|
|
||||||
hideHeadingOnMobile
|
|
||||||
subtitle={t("event_types_page_subtitle")}
|
|
||||||
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
|
|
||||||
beforeCTAactions={<Actions />}
|
|
||||||
CTA={<CTA data={data} />}>
|
|
||||||
<Main data={data} status={status} error={error} filters={filters} />
|
|
||||||
</Shell>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
EventTypesPage.getLayout = getLayout;
|
||||||
|
|
||||||
EventTypesPage.PageWrapper = PageWrapper;
|
EventTypesPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default EventTypesPage;
|
export default EventTypesPage;
|
||||||
|
|
|
@ -121,10 +121,10 @@ const OnboardingPage = () => {
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div className="mx-auto px-4 py-6 md:py-24">
|
<div className="mx-auto py-6 sm:px-4 md:py-24">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-[600px]">
|
<div className="sm:mx-auto sm:w-full sm:max-w-[600px]">
|
||||||
<div className="mx-auto sm:max-w-[520px]">
|
<div className="mx-auto px-4 sm:max-w-[520px]">
|
||||||
<header>
|
<header>
|
||||||
<p className="font-cal mb-3 text-[28px] font-medium leading-7">
|
<p className="font-cal mb-3 text-[28px] font-medium leading-7">
|
||||||
{headers[currentStepIndex]?.title || "Undefined title"}
|
{headers[currentStepIndex]?.title || "Undefined title"}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
import {
|
import {
|
||||||
AverageEventDurationChart,
|
AverageEventDurationChart,
|
||||||
|
@ -9,7 +10,7 @@ import {
|
||||||
} from "@calcom/features/insights/components";
|
} from "@calcom/features/insights/components";
|
||||||
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||||
import { Filters } from "@calcom/features/insights/filters";
|
import { Filters } from "@calcom/features/insights/filters";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { UpgradeTip } from "@calcom/features/tips";
|
import { UpgradeTip } from "@calcom/features/tips";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -56,7 +57,7 @@ export default function InsightsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Shell hideHeadingOnMobile>
|
<ShellMain hideHeadingOnMobile>
|
||||||
<UpgradeTip
|
<UpgradeTip
|
||||||
title={t("make_informed_decisions")}
|
title={t("make_informed_decisions")}
|
||||||
description={t("make_informed_decisions_description")}
|
description={t("make_informed_decisions_description")}
|
||||||
|
@ -111,12 +112,13 @@ export default function InsightsPage() {
|
||||||
</FiltersProvider>
|
</FiltersProvider>
|
||||||
)}
|
)}
|
||||||
</UpgradeTip>
|
</UpgradeTip>
|
||||||
</Shell>
|
</ShellMain>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InsightsPage.PageWrapper = PageWrapper;
|
InsightsPage.PageWrapper = PageWrapper;
|
||||||
|
InsightsPage.getLayout = getLayout;
|
||||||
|
|
||||||
// If feature flag is disabled, return not found on getServerSideProps
|
// If feature flag is disabled, return not found on getServerSideProps
|
||||||
export const getServerSideProps = async () => {
|
export const getServerSideProps = async () => {
|
||||||
|
|
|
@ -272,6 +272,9 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||||
username = available ? username : suggestion || username;
|
username = available ? username : suggestion || username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform all + to - in username
|
||||||
|
username = username.replace(/\+/g, "-");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...props,
|
...props,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
@ -17,7 +18,7 @@ function Teams() {
|
||||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<ShellMain
|
||||||
heading={t("teams")}
|
heading={t("teams")}
|
||||||
hideHeadingOnMobile
|
hideHeadingOnMobile
|
||||||
subtitle={t("create_manage_teams_collaborative")}
|
subtitle={t("create_manage_teams_collaborative")}
|
||||||
|
@ -33,7 +34,7 @@ function Teams() {
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
<TeamsListing />
|
<TeamsListing />
|
||||||
</Shell>
|
</ShellMain>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,5 +47,5 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
Teams.requiresLicense = false;
|
Teams.requiresLicense = false;
|
||||||
Teams.PageWrapper = PageWrapper;
|
Teams.PageWrapper = PageWrapper;
|
||||||
|
Teams.getLayout = getLayout;
|
||||||
export default Teams;
|
export default Teams;
|
||||||
|
|
|
@ -204,7 +204,7 @@
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"blog_description": "Lesen Sie unsere Neuigkeiten und Artikel",
|
"blog_description": "Lesen Sie unsere Neuigkeiten und Artikel",
|
||||||
"join_our_community": "Treten Sie unserer Community bei",
|
"join_our_community": "Treten Sie unserer Community bei",
|
||||||
"join_our_discord": "Treten Sie unserem Discord-Server bei",
|
"join_our_discord": "Treffe uns im Discord",
|
||||||
"404_claim_entity_user": "Benutzername registrieren und Termine vergeben",
|
"404_claim_entity_user": "Benutzername registrieren und Termine vergeben",
|
||||||
"popular_pages": "Beliebte Seiten",
|
"popular_pages": "Beliebte Seiten",
|
||||||
"register_now": "Jetzt registrieren",
|
"register_now": "Jetzt registrieren",
|
||||||
|
|
|
@ -256,6 +256,8 @@
|
||||||
"available_apps": "Available Apps",
|
"available_apps": "Available Apps",
|
||||||
"available_apps_lower_case": "Available apps",
|
"available_apps_lower_case": "Available apps",
|
||||||
"available_apps_desc": "You have no apps installed. View popular apps below and explore more in our <1>App Store</1>",
|
"available_apps_desc": "You have no apps installed. View popular apps below and explore more in our <1>App Store</1>",
|
||||||
|
"fixed_host_helper": "Add anyone who needs to attend the event. <1>Learn more</1>",
|
||||||
|
"round_robin_helper":"People in the group take turns and only one person will show up for the event.",
|
||||||
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
|
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"organization_general_description": "Manage settings for your team language and timezone",
|
"organization_general_description": "Manage settings for your team language and timezone",
|
||||||
|
@ -1850,6 +1852,8 @@
|
||||||
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
|
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
|
||||||
"acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.",
|
"acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.",
|
||||||
"card_details": "Card details",
|
"card_details": "Card details",
|
||||||
|
"something_went_wrong_on_our_end":"Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right away for you.",
|
||||||
|
"please_provide_following_text_to_suppport":"Please provide the following text when contacting support to better help you",
|
||||||
"seats_and_no_show_fee_error": "Currently cannot enable seats and charge a no-show fee",
|
"seats_and_no_show_fee_error": "Currently cannot enable seats and charge a no-show fee",
|
||||||
"complete_your_booking": "Complete your booking",
|
"complete_your_booking": "Complete your booking",
|
||||||
"complete_your_booking_subject": "Complete your booking: {{title}} on {{date}}",
|
"complete_your_booking_subject": "Complete your booking: {{title}} on {{date}}",
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
"team_upgrade_banner_description": "אנחנו מודים לך על כך שניסית את החבילה החדשה שלנו לצוותים. שמנו לב שהצוות שלך, \"{{teamName}}\", זקוק לשדרוג.",
|
"team_upgrade_banner_description": "אנחנו מודים לך על כך שניסית את החבילה החדשה שלנו לצוותים. שמנו לב שהצוות שלך, \"{{teamName}}\", זקוק לשדרוג.",
|
||||||
"upgrade_banner_action": "כאן משדרגים",
|
"upgrade_banner_action": "כאן משדרגים",
|
||||||
"team_upgraded_successfully": "הצוות שלך שודרג בהצלחה!",
|
"team_upgraded_successfully": "הצוות שלך שודרג בהצלחה!",
|
||||||
"org_upgrade_banner_description": "אנחנו מודים לך על כך שניסית את החבילה החדשה שלנו לארגונים. שמנו לב שהארגון שלך, \"{{teamName}}\", זקוק לשדרוג.",
|
"org_upgrade_banner_description": "אנחנו מודים לך על כך שניסית את החבילה החדשה שלנו לארגונים. שמנו לב שה-Organization שלך, \"{{teamName}}\", זקוק לשדרוג.",
|
||||||
"org_upgraded_successfully": "השדרוג של הארגון שלך בוצע בהצלחה!",
|
"org_upgraded_successfully": "השדרוג של הארגון שלך בוצע בהצלחה!",
|
||||||
"use_link_to_reset_password": "נא להשתמש בקישור הבא כדי לאפס את הסיסמה",
|
"use_link_to_reset_password": "נא להשתמש בקישור הבא כדי לאפס את הסיסמה",
|
||||||
"hey_there": "שלום,",
|
"hey_there": "שלום,",
|
||||||
|
@ -551,7 +551,7 @@
|
||||||
"team_description": "מספר משפטים אודות הצוות. המידע הזה יופיע בדף ה-URL של הצוות.",
|
"team_description": "מספר משפטים אודות הצוות. המידע הזה יופיע בדף ה-URL של הצוות.",
|
||||||
"org_description": "מספר משפטים אודות הארגון. הם יופיעו בדף עם כתובת ה-URL של הארגון.",
|
"org_description": "מספר משפטים אודות הארגון. הם יופיעו בדף עם כתובת ה-URL של הארגון.",
|
||||||
"members": "חברים",
|
"members": "חברים",
|
||||||
"organization_members": "חברי הארגון",
|
"organization_members": "חברי Organization",
|
||||||
"member": "חבר/ת",
|
"member": "חבר/ת",
|
||||||
"number_member_one": "חבר {{count}}",
|
"number_member_one": "חבר {{count}}",
|
||||||
"number_member_other": "{{count}} חברים",
|
"number_member_other": "{{count}} חברים",
|
||||||
|
@ -699,7 +699,7 @@
|
||||||
"create_team_to_get_started": "צור צוות כדי להתחיל",
|
"create_team_to_get_started": "צור צוות כדי להתחיל",
|
||||||
"teams": "צוותים",
|
"teams": "צוותים",
|
||||||
"team": "צוות",
|
"team": "צוות",
|
||||||
"organization": "ארגון",
|
"organization": "Organization",
|
||||||
"team_billing": "חיוב צוותים",
|
"team_billing": "חיוב צוותים",
|
||||||
"team_billing_description": "ניהול החיוב עבור הצוות",
|
"team_billing_description": "ניהול החיוב עבור הצוות",
|
||||||
"upgrade_to_flexible_pro_title": "שינינו את החיוב בעבור צוותים",
|
"upgrade_to_flexible_pro_title": "שינינו את החיוב בעבור צוותים",
|
||||||
|
@ -1151,7 +1151,7 @@
|
||||||
"custom": "התאמה אישית",
|
"custom": "התאמה אישית",
|
||||||
"reminder": "תזכורת",
|
"reminder": "תזכורת",
|
||||||
"rescheduled": "נקבע מועד חדש",
|
"rescheduled": "נקבע מועד חדש",
|
||||||
"completed": "הושלם",
|
"completed": "הושלמה",
|
||||||
"reminder_email": "תזכורת: {{eventType}} עם {{name}} בתאריך {{date}}",
|
"reminder_email": "תזכורת: {{eventType}} עם {{name}} בתאריך {{date}}",
|
||||||
"not_triggering_existing_bookings": "לא יופעל עבור הזמנות קיימות מאחר שהמשתמש יתבקש למסור מספר טלפון בעת הזמנת האירוע.",
|
"not_triggering_existing_bookings": "לא יופעל עבור הזמנות קיימות מאחר שהמשתמש יתבקש למסור מספר טלפון בעת הזמנת האירוע.",
|
||||||
"minute_one": "{{count}} דקה",
|
"minute_one": "{{count}} דקה",
|
||||||
|
@ -1938,7 +1938,7 @@
|
||||||
"insights_all_org_filter": "כל האפליקציות",
|
"insights_all_org_filter": "כל האפליקציות",
|
||||||
"insights_team_filter": "צוות: {{teamName}}",
|
"insights_team_filter": "צוות: {{teamName}}",
|
||||||
"insights_user_filter": "משתמש: {{userName}}",
|
"insights_user_filter": "משתמש: {{userName}}",
|
||||||
"insights_subtitle": "הצגת תובנות הזמנה מתוך האירועים שלך",
|
"insights_subtitle": "הצגת insights לגבי הזמנות מתוך האירועים שלך",
|
||||||
"custom_plan": "חבילה בהתאמה אישית",
|
"custom_plan": "חבילה בהתאמה אישית",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
"team_info": "Informazioni sul team",
|
"team_info": "Informazioni sul team",
|
||||||
"request_another_invitation_email": "Se preferisci non usare {{toEmail}} come email {{appName}} o già hai un account {{appName}}, per favore richiedi un altro invito a quella email.",
|
"request_another_invitation_email": "Se preferisci non usare {{toEmail}} come email {{appName}} o già hai un account {{appName}}, per favore richiedi un altro invito a quella email.",
|
||||||
"you_have_been_invited": "Sei stato invitato ad unirti al team {{teamName}}",
|
"you_have_been_invited": "Sei stato invitato ad unirti al team {{teamName}}",
|
||||||
"user_invited_you": "{{user}} ti ha invitato a unirti al {{team}} {{entity}} su {{appName}}",
|
"user_invited_you": "{{user}} ti ha invitato a unirti a {{entity}} {{team}} su {{appName}}",
|
||||||
"hidden_team_member_title": "Sei nascosto in questo team",
|
"hidden_team_member_title": "Sei nascosto in questo team",
|
||||||
"hidden_team_member_message": "Il tuo posto non è pagato. Passa a Pro oppure informa il proprietario del team che può pagare il tuo posto.",
|
"hidden_team_member_message": "Il tuo posto non è pagato. Passa a Pro oppure informa il proprietario del team che può pagare il tuo posto.",
|
||||||
"hidden_team_owner_message": "Per utilizzare i team ti occorre un account Pro. Sarai nascosto finché non cambierai piano.",
|
"hidden_team_owner_message": "Per utilizzare i team ti occorre un account Pro. Sarai nascosto finché non cambierai piano.",
|
||||||
|
@ -129,8 +129,8 @@
|
||||||
"team_upgrade_banner_description": "Grazie per aver provato il nostro piano team. Abbiamo notato che è necessario effettuare l'upgrade del tuo team \"{{teamName}}\".",
|
"team_upgrade_banner_description": "Grazie per aver provato il nostro piano team. Abbiamo notato che è necessario effettuare l'upgrade del tuo team \"{{teamName}}\".",
|
||||||
"upgrade_banner_action": "Effettua l'upgrade",
|
"upgrade_banner_action": "Effettua l'upgrade",
|
||||||
"team_upgraded_successfully": "Il tuo team è stato aggiornato!",
|
"team_upgraded_successfully": "Il tuo team è stato aggiornato!",
|
||||||
"org_upgrade_banner_description": "Grazie per aver provato il nostro piano Organization. Abbiamo notato che è necessario eseguire l'upgrade per la tua organizzazione \"{{teamName}}\".",
|
"org_upgrade_banner_description": "Grazie per aver provato il nostro piano Organization. Abbiamo notato che è necessario eseguire l'upgrade del piano Organization per \"{{teamName}}\".",
|
||||||
"org_upgraded_successfully": "È stato eseguito l'upgrade della tua organizzazione!",
|
"org_upgraded_successfully": "È stato eseguito l'upgrade del piano Organization!",
|
||||||
"use_link_to_reset_password": "Usa il link qui sotto per reimpostare la tua password",
|
"use_link_to_reset_password": "Usa il link qui sotto per reimpostare la tua password",
|
||||||
"hey_there": "Ciao,",
|
"hey_there": "Ciao,",
|
||||||
"forgot_your_password_calcom": "Hai dimenticato la password? - {{appName}}",
|
"forgot_your_password_calcom": "Hai dimenticato la password? - {{appName}}",
|
||||||
|
@ -555,7 +555,7 @@
|
||||||
"member": "Membri",
|
"member": "Membri",
|
||||||
"number_member_one": "{{count}} membro",
|
"number_member_one": "{{count}} membro",
|
||||||
"number_member_other": "{{count}} membri",
|
"number_member_other": "{{count}} membri",
|
||||||
"number_selected": "{{count}} selezionati",
|
"number_selected": "{{count}} selezionato/i",
|
||||||
"owner": "Proprietario",
|
"owner": "Proprietario",
|
||||||
"admin": "Amministratore",
|
"admin": "Amministratore",
|
||||||
"administrator_user": "Utente amministratore",
|
"administrator_user": "Utente amministratore",
|
||||||
|
@ -1912,7 +1912,7 @@
|
||||||
"organization_admin_invited_body": "Unisciti al tuo team su {{orgName}} per concentrarti sulle riunioni, non sulla loro pianificazione!",
|
"organization_admin_invited_body": "Unisciti al tuo team su {{orgName}} per concentrarti sulle riunioni, non sulla loro pianificazione!",
|
||||||
"duplicated_slugs_warning": "Non è stato possibile creare i seguenti team a causa di slug duplicati: {{slugs}}",
|
"duplicated_slugs_warning": "Non è stato possibile creare i seguenti team a causa di slug duplicati: {{slugs}}",
|
||||||
"team_names_empty": "I nomi di team non possono essere vuoti",
|
"team_names_empty": "I nomi di team non possono essere vuoti",
|
||||||
"team_names_repeated": "I nomi di team non possono essere ripetuti",
|
"team_names_repeated": "I nomi di team devono essere unici",
|
||||||
"user_belongs_organization": "L'utente appartiene a un'organizzazione",
|
"user_belongs_organization": "L'utente appartiene a un'organizzazione",
|
||||||
"org_no_teams_yet": "Questa organizzazione non ha ancora nessun team",
|
"org_no_teams_yet": "Questa organizzazione non ha ancora nessun team",
|
||||||
"org_no_teams_yet_description": "Se sei un amministratore, assicurati di creare dei team da mostrare qui.",
|
"org_no_teams_yet_description": "Se sei un amministratore, assicurati di creare dei team da mostrare qui.",
|
||||||
|
@ -1934,7 +1934,7 @@
|
||||||
"404_the_org": "L'organizzazione",
|
"404_the_org": "L'organizzazione",
|
||||||
"404_the_team": "Il team",
|
"404_the_team": "Il team",
|
||||||
"404_claim_entity_org": "Richiedi il sottodominio per la tua organizzazione",
|
"404_claim_entity_org": "Richiedi il sottodominio per la tua organizzazione",
|
||||||
"404_claim_entity_team": "Richiedi questo team e inizia a gestire le pianificazioni collettivamente",
|
"404_claim_entity_team": "Richiedi l'accesso a questo team per iniziare a gestire le pianificazioni collettivamente",
|
||||||
"insights_all_org_filter": "Tutte le app",
|
"insights_all_org_filter": "Tutte le app",
|
||||||
"insights_team_filter": "Team: {{teamName}}",
|
"insights_team_filter": "Team: {{teamName}}",
|
||||||
"insights_user_filter": "Utente: {{userName}}",
|
"insights_user_filter": "Utente: {{userName}}",
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
"reset_password_subject": "{{appName}}: パスワードのリセット手順",
|
"reset_password_subject": "{{appName}}: パスワードのリセット手順",
|
||||||
"verify_email_subject": "{{appName}}:アカウントを確認",
|
"verify_email_subject": "{{appName}}:アカウントを確認",
|
||||||
"check_your_email": "メールを確認してください",
|
"check_your_email": "メールを確認してください",
|
||||||
"verify_email_page_body": "{{email}} にメールを送信しました。{{appName}} からのメール配信とカレンダーが確実に連携するようにするには、メールアドレスの確認が重要です。",
|
"verify_email_page_body": "{{email}} にメールを送信しました。{{appName}} からのメール配信とカレンダー通知が確実に連携するようにするには、メールアドレスの確認が重要です。",
|
||||||
"verify_email_banner_body": "メール配信とカレンダーが確実に連携するようにするには、メールアドレスを確認してください。",
|
"verify_email_banner_body": "メール配信とカレンダー通知が確実に連携するようにするには、メールアドレスを確認してください。",
|
||||||
"verify_email_email_header": "メールアドレスの確認",
|
"verify_email_email_header": "メールアドレスの確認",
|
||||||
"verify_email_email_button": "メールを確認",
|
"verify_email_email_button": "メールを確認",
|
||||||
"verify_email_email_body": "以下のボタンをクリックして、メールアドレスを確認してください。",
|
"verify_email_email_body": "以下のボタンをクリックして、メールアドレスを確認してください。",
|
||||||
|
@ -1934,7 +1934,7 @@
|
||||||
"404_the_org": "組織",
|
"404_the_org": "組織",
|
||||||
"404_the_team": "チーム",
|
"404_the_team": "チーム",
|
||||||
"404_claim_entity_org": "組織のサブドメインを取得",
|
"404_claim_entity_org": "組織のサブドメインを取得",
|
||||||
"404_claim_entity_team": "このチームを取得して、これからはスケジュールをまとめて管理しましょう",
|
"404_claim_entity_team": "このチームの一員になって、これからはスケジュールをまとめて管理しましょう",
|
||||||
"insights_all_org_filter": "すべて",
|
"insights_all_org_filter": "すべて",
|
||||||
"insights_team_filter": "チーム:{{teamName}}",
|
"insights_team_filter": "チーム:{{teamName}}",
|
||||||
"insights_user_filter": "ユーザー:{{userName}}",
|
"insights_user_filter": "ユーザー:{{userName}}",
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
|
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
|
||||||
"verify_email_subject": "{{appName}}: Verificar sua conta",
|
"verify_email_subject": "{{appName}}: Verificar sua conta",
|
||||||
"check_your_email": "Verifique seu e-mail",
|
"check_your_email": "Verifique seu e-mail",
|
||||||
"verify_email_page_body": "Enviamos um e-mail para {{email}}. É importante verificar seu endereço de e-mail para garantir o fornecimento do melhor e-mail e calendário de {{appName}}.",
|
"verify_email_page_body": "Enviamos um e-mail para {{email}}. É importante verificar seu endereço de e-mail para garantir o melhor envio de e-mail e calendário do {{appName}}.",
|
||||||
"verify_email_banner_body": "Verifique seu endereço de e-mail para garantir o fornecimento do melhor e-mail e calendário",
|
"verify_email_banner_body": "Verifique seu endereço de e-mail para garantir o fornecimento do melhor e-mail e calendário",
|
||||||
"verify_email_email_header": "Verifique seu endereço de e-mail",
|
"verify_email_email_header": "Verifique seu endereço de e-mail",
|
||||||
"verify_email_email_button": "Verificar e-mail",
|
"verify_email_email_button": "Verificar e-mail",
|
||||||
|
|
|
@ -310,7 +310,7 @@
|
||||||
"bookerlayout_user_settings_title": "Disposição da reserva",
|
"bookerlayout_user_settings_title": "Disposição da reserva",
|
||||||
"bookerlayout_user_settings_description": "Pode selecionar várias e os clientes podem alternar entre vistas. Isto pode ser sobreposto para cada evento.",
|
"bookerlayout_user_settings_description": "Pode selecionar várias e os clientes podem alternar entre vistas. Isto pode ser sobreposto para cada evento.",
|
||||||
"bookerlayout_month_view": "Mês",
|
"bookerlayout_month_view": "Mês",
|
||||||
"bookerlayout_week_view": "Semanal",
|
"bookerlayout_week_view": "Semana",
|
||||||
"bookerlayout_column_view": "Coluna",
|
"bookerlayout_column_view": "Coluna",
|
||||||
"bookerlayout_error_min_one_enabled": "Tem de estar ativa pelo menos uma disposição.",
|
"bookerlayout_error_min_one_enabled": "Tem de estar ativa pelo menos uma disposição.",
|
||||||
"bookerlayout_error_default_not_enabled": "A disposição selecionada como a vista predefinida não faz parte das disposições ativas.",
|
"bookerlayout_error_default_not_enabled": "A disposição selecionada como a vista predefinida não faz parte das disposições ativas.",
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
"team_info": "Informații echipă",
|
"team_info": "Informații echipă",
|
||||||
"request_another_invitation_email": "Dacă preferați să nu utilizați {{toEmail}} ca e-mail {{appName}} sau aveți deja un cont {{appName}}, vă rugăm să solicitați o altă invitație la acel e-mail.",
|
"request_another_invitation_email": "Dacă preferați să nu utilizați {{toEmail}} ca e-mail {{appName}} sau aveți deja un cont {{appName}}, vă rugăm să solicitați o altă invitație la acel e-mail.",
|
||||||
"you_have_been_invited": "Ați fost invitat să vă alăturați echipei {{teamName}}",
|
"you_have_been_invited": "Ați fost invitat să vă alăturați echipei {{teamName}}",
|
||||||
"user_invited_you": "{{user}} v-a invitat să vă alăturați echipei {{entity}} {{team}} pe {{appName}}",
|
"user_invited_you": "{{user}} v-a invitat să vă alăturați la {{entity}} {{team}} de pe {{appName}}",
|
||||||
"hidden_team_member_title": "Sunteți ascuns în această echipă",
|
"hidden_team_member_title": "Sunteți ascuns în această echipă",
|
||||||
"hidden_team_member_message": "Licența dvs. nu este plătită. Fie faceți upgrade la Pro, fie anunțați proprietarul echipei că vă poate plăti licența.",
|
"hidden_team_member_message": "Licența dvs. nu este plătită. Fie faceți upgrade la Pro, fie anunțați proprietarul echipei că vă poate plăti licența.",
|
||||||
"hidden_team_owner_message": "Aveți nevoie de un cont Pro pentru a utiliza echipe. Sunteți ascuns până când faceți upgrade.",
|
"hidden_team_owner_message": "Aveți nevoie de un cont Pro pentru a utiliza echipe. Sunteți ascuns până când faceți upgrade.",
|
||||||
|
@ -129,8 +129,8 @@
|
||||||
"team_upgrade_banner_description": "Vă mulțumim că ați încercat noul nostru plan pentru echipe. Am observat că planul echipei dvs. „{{teamName}}” necesită un upgrade.",
|
"team_upgrade_banner_description": "Vă mulțumim că ați încercat noul nostru plan pentru echipe. Am observat că planul echipei dvs. „{{teamName}}” necesită un upgrade.",
|
||||||
"upgrade_banner_action": "Realizați upgrade aici",
|
"upgrade_banner_action": "Realizați upgrade aici",
|
||||||
"team_upgraded_successfully": "Echipa dvs. a fost actualizată cu succes!",
|
"team_upgraded_successfully": "Echipa dvs. a fost actualizată cu succes!",
|
||||||
"org_upgrade_banner_description": "Vă mulțumim că ați încercat planul nostru Organizație. Am observat că organizația dvs. „{{teamName}}” necesită un upgrade.",
|
"org_upgrade_banner_description": "Vă mulțumim că ați încercat planul nostru Organization. Am observat că organizația dvs. „{{teamName}}” necesită un upgrade.",
|
||||||
"org_upgraded_successfully": "Upgrade-ul la organizația dvs. a fost realizat cu succes!",
|
"org_upgraded_successfully": "Upgrade-ul pentru Organization a fost realizat cu succes!",
|
||||||
"use_link_to_reset_password": "Utilizați linkul de mai jos pentru a vă reseta parola",
|
"use_link_to_reset_password": "Utilizați linkul de mai jos pentru a vă reseta parola",
|
||||||
"hey_there": "Bună,",
|
"hey_there": "Bună,",
|
||||||
"forgot_your_password_calcom": "Ți-ai uitat parola? - {{appName}}",
|
"forgot_your_password_calcom": "Ți-ai uitat parola? - {{appName}}",
|
||||||
|
|
|
@ -131,7 +131,7 @@
|
||||||
"upgrade_banner_action": "Nadogradite ovde",
|
"upgrade_banner_action": "Nadogradite ovde",
|
||||||
"team_upgraded_successfully": "Vaš tim je uspešno pretplaćen!",
|
"team_upgraded_successfully": "Vaš tim je uspešno pretplaćen!",
|
||||||
"org_upgrade_banner_description": "Hvala što isprobavate plan naše organizacije. Primetili smo da tim vaše organizacije „{{teamName}}” treba da se nadogradi.",
|
"org_upgrade_banner_description": "Hvala što isprobavate plan naše organizacije. Primetili smo da tim vaše organizacije „{{teamName}}” treba da se nadogradi.",
|
||||||
"org_upgraded_successfully": "Vaša organizacija je uspešno nadograđena!",
|
"org_upgraded_successfully": "Vaš Organization je uspešno nadograđen!",
|
||||||
"use_link_to_reset_password": "Resetujte lozinku koristeći link ispod",
|
"use_link_to_reset_password": "Resetujte lozinku koristeći link ispod",
|
||||||
"hey_there": "Zdravo,",
|
"hey_there": "Zdravo,",
|
||||||
"forgot_your_password_calcom": "Zaboravili ste lozinku? - {{appName}}",
|
"forgot_your_password_calcom": "Zaboravili ste lozinku? - {{appName}}",
|
||||||
|
|
|
@ -129,8 +129,8 @@
|
||||||
"team_upgrade_banner_description": "Tack för att du testade vår nya teamplan. Vi märkte att ditt team \"{{teamName}}\" måste uppgraderas.",
|
"team_upgrade_banner_description": "Tack för att du testade vår nya teamplan. Vi märkte att ditt team \"{{teamName}}\" måste uppgraderas.",
|
||||||
"upgrade_banner_action": "Uppgradera här",
|
"upgrade_banner_action": "Uppgradera här",
|
||||||
"team_upgraded_successfully": "Ditt team har uppgraderats!",
|
"team_upgraded_successfully": "Ditt team har uppgraderats!",
|
||||||
"org_upgrade_banner_description": "Tack för att du testade vår nya organisationsplan. Vi märkte att din organisation \"{{teamName}}\" måste uppgraderas.",
|
"org_upgrade_banner_description": "Tack för att du testade vår nya Organization-plan. Vi märkte att din Organization \"{{teamName}}\" måste uppgraderas.",
|
||||||
"org_upgraded_successfully": "Din organisation har uppgraderats!",
|
"org_upgraded_successfully": "Din Organization har uppgraderats!",
|
||||||
"use_link_to_reset_password": "Använd länken nedan för att återställa ditt lösenord",
|
"use_link_to_reset_password": "Använd länken nedan för att återställa ditt lösenord",
|
||||||
"hey_there": "Hallå där,",
|
"hey_there": "Hallå där,",
|
||||||
"forgot_your_password_calcom": "Glömt ditt lösenord? - {{appName}}",
|
"forgot_your_password_calcom": "Glömt ditt lösenord? - {{appName}}",
|
||||||
|
@ -551,7 +551,7 @@
|
||||||
"team_description": "Några meningar om ditt team. Detta visas på ditt teams URL-sida.",
|
"team_description": "Några meningar om ditt team. Detta visas på ditt teams URL-sida.",
|
||||||
"org_description": "Några meningar om din organisation. Detta kommer att visas på din organisations webbsida.",
|
"org_description": "Några meningar om din organisation. Detta kommer att visas på din organisations webbsida.",
|
||||||
"members": "Medlemmar",
|
"members": "Medlemmar",
|
||||||
"organization_members": "Organisationens medlemmar",
|
"organization_members": "Organization-medlemmar",
|
||||||
"member": "Medlem",
|
"member": "Medlem",
|
||||||
"number_member_one": "{{count}} medlem",
|
"number_member_one": "{{count}} medlem",
|
||||||
"number_member_other": "{{count}} medlemmar",
|
"number_member_other": "{{count}} medlemmar",
|
||||||
|
@ -699,7 +699,7 @@
|
||||||
"create_team_to_get_started": "Skapa ett team för att komma igång",
|
"create_team_to_get_started": "Skapa ett team för att komma igång",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"organization": "Organisation",
|
"organization": "Organization",
|
||||||
"team_billing": "Team-fakturering",
|
"team_billing": "Team-fakturering",
|
||||||
"team_billing_description": "Hantera fakturering för ditt team",
|
"team_billing_description": "Hantera fakturering för ditt team",
|
||||||
"upgrade_to_flexible_pro_title": "Vi har ändrat fakturering för team",
|
"upgrade_to_flexible_pro_title": "Vi har ändrat fakturering för team",
|
||||||
|
@ -1938,7 +1938,7 @@
|
||||||
"insights_all_org_filter": "Alla",
|
"insights_all_org_filter": "Alla",
|
||||||
"insights_team_filter": "Team: {{teamName}}",
|
"insights_team_filter": "Team: {{teamName}}",
|
||||||
"insights_user_filter": "Användare: {{userName}}",
|
"insights_user_filter": "Användare: {{userName}}",
|
||||||
"insights_subtitle": "Visa bokningsinsikter för dina händelser",
|
"insights_subtitle": "Visa Insights-bokningar för dina händelser",
|
||||||
"custom_plan": "Anpassad plan",
|
"custom_plan": "Anpassad plan",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lägg till dina nya strängar här ovanför ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lägg till dina nya strängar här ovanför ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1938,7 +1938,7 @@
|
||||||
"insights_all_org_filter": "Tümü",
|
"insights_all_org_filter": "Tümü",
|
||||||
"insights_team_filter": "Ekip: {{teamName}}",
|
"insights_team_filter": "Ekip: {{teamName}}",
|
||||||
"insights_user_filter": "Kullanıcı: {{userName}}",
|
"insights_user_filter": "Kullanıcı: {{userName}}",
|
||||||
"insights_subtitle": "Etkinlikleriniz için rezervasyon analizlerini görüntüleyin",
|
"insights_subtitle": "Etkinlikleriniz için rezervasyon Insights'lerini görüntüleyin",
|
||||||
"custom_plan": "Özel Plan",
|
"custom_plan": "Özel Plan",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
"team_info": "Про команду",
|
"team_info": "Про команду",
|
||||||
"request_another_invitation_email": "Якщо ви не хочете використовувати {{toEmail}} в {{appName}} або вже маєте обліковий запис {{appName}}, надішліть запит на запрошення на потрібну електронну адресу.",
|
"request_another_invitation_email": "Якщо ви не хочете використовувати {{toEmail}} в {{appName}} або вже маєте обліковий запис {{appName}}, надішліть запит на запрошення на потрібну електронну адресу.",
|
||||||
"you_have_been_invited": "Вас запрошено приєднатися до команди «{{teamName}}»",
|
"you_have_been_invited": "Вас запрошено приєднатися до команди «{{teamName}}»",
|
||||||
"user_invited_you": "{{user}} запросив(-ла) вас приєднатися до команди «{{team}}» користувача {{entity}} в {{appName}}",
|
"user_invited_you": "{{user}} запросив(-ла) вас приєднатися сюди: {{entity}} «{{team}}» в {{appName}}",
|
||||||
"hidden_team_member_title": "У цій команді вас приховано",
|
"hidden_team_member_title": "У цій команді вас приховано",
|
||||||
"hidden_team_member_message": "Ваше місце не оплачено. Перейдіть на версію Pro або попросіть власника команди оплатити ваше місце.",
|
"hidden_team_member_message": "Ваше місце не оплачено. Перейдіть на версію Pro або попросіть власника команди оплатити ваше місце.",
|
||||||
"hidden_team_owner_message": "Щоб користуватися функціями команди, потрібен обліковий запис версії Pro. До переходу на цю версію вас буде приховано.",
|
"hidden_team_owner_message": "Щоб користуватися функціями команди, потрібен обліковий запис версії Pro. До переходу на цю версію вас буде приховано.",
|
||||||
|
|
|
@ -131,7 +131,7 @@
|
||||||
"upgrade_banner_action": "在此处升级",
|
"upgrade_banner_action": "在此处升级",
|
||||||
"team_upgraded_successfully": "您的团队升级成功!",
|
"team_upgraded_successfully": "您的团队升级成功!",
|
||||||
"org_upgrade_banner_description": "感谢您试用我们的 Organization 计划。我们注意到您的组织“{{teamName}}”需要升级。",
|
"org_upgrade_banner_description": "感谢您试用我们的 Organization 计划。我们注意到您的组织“{{teamName}}”需要升级。",
|
||||||
"org_upgraded_successfully": "您的组织已成功升级!",
|
"org_upgraded_successfully": "您的 Organization 已成功升级!",
|
||||||
"use_link_to_reset_password": "使用下面的链接重置您的密码",
|
"use_link_to_reset_password": "使用下面的链接重置您的密码",
|
||||||
"hey_there": "嘿,您好!",
|
"hey_there": "嘿,您好!",
|
||||||
"forgot_your_password_calcom": "忘记密码?- {{appName}}",
|
"forgot_your_password_calcom": "忘记密码?- {{appName}}",
|
||||||
|
@ -555,7 +555,7 @@
|
||||||
"team_description": "请写一段简单的团队介绍,该介绍将会显示在您的团队链接页面上。",
|
"team_description": "请写一段简单的团队介绍,该介绍将会显示在您的团队链接页面上。",
|
||||||
"org_description": "关于您的组织的几句话。这将显示在您组织的链接页面上。",
|
"org_description": "关于您的组织的几句话。这将显示在您组织的链接页面上。",
|
||||||
"members": "成员",
|
"members": "成员",
|
||||||
"organization_members": "组织成员",
|
"organization_members": "Organization 成员",
|
||||||
"member": "成员",
|
"member": "成员",
|
||||||
"number_member_one": "{{count}} 个成员",
|
"number_member_one": "{{count}} 个成员",
|
||||||
"number_member_other": "{{count}} 个成员",
|
"number_member_other": "{{count}} 个成员",
|
||||||
|
@ -1331,7 +1331,7 @@
|
||||||
"download_responses_description": "以 CSV 格式下载对表格的所有回复。",
|
"download_responses_description": "以 CSV 格式下载对表格的所有回复。",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"download_recording": "下载录制内容",
|
"download_recording": "下载录制内容",
|
||||||
"recording_from_your_recent_call": "您最近在 {{appName}} 上的通话录音可供下载",
|
"recording_from_your_recent_call": "您最近在 {{appName}} 上的通话录制内容可供下载",
|
||||||
"create_your_first_form": "创建您的第一个表格",
|
"create_your_first_form": "创建您的第一个表格",
|
||||||
"create_your_first_form_description": "利用途径表格,您可以提出符合条件的问题,并可根据途径找到正确的人或活动类型。",
|
"create_your_first_form_description": "利用途径表格,您可以提出符合条件的问题,并可根据途径找到正确的人或活动类型。",
|
||||||
"create_your_first_webhook": "创建您的第一个 Webhook",
|
"create_your_first_webhook": "创建您的第一个 Webhook",
|
||||||
|
@ -1691,7 +1691,7 @@
|
||||||
"attendee_no_longer_attending": "一名参与者不再参加您的活动",
|
"attendee_no_longer_attending": "一名参与者不再参加您的活动",
|
||||||
"attendee_no_longer_attending_subtitle": "{{name}} 已取消。这意味着该时段有一个位置空出来",
|
"attendee_no_longer_attending_subtitle": "{{name}} 已取消。这意味着该时段有一个位置空出来",
|
||||||
"create_event_on": "创建活动于",
|
"create_event_on": "创建活动于",
|
||||||
"create_routing_form_on": "创建途径表格于",
|
"create_routing_form_on": "创建途径表格的账户",
|
||||||
"default_app_link_title": "设置默认应用链接",
|
"default_app_link_title": "设置默认应用链接",
|
||||||
"default_app_link_description": "设置默认应用链接可以让所有新创建的活动类型使用您设置的应用链接。",
|
"default_app_link_description": "设置默认应用链接可以让所有新创建的活动类型使用您设置的应用链接。",
|
||||||
"organizer_default_conferencing_app": "组织者的默认应用",
|
"organizer_default_conferencing_app": "组织者的默认应用",
|
||||||
|
@ -1880,7 +1880,7 @@
|
||||||
"connect_google_workspace": "连接 Google Workspace",
|
"connect_google_workspace": "连接 Google Workspace",
|
||||||
"google_workspace_admin_tooltip": "您必须是 Workspace 管理员才能使用此功能",
|
"google_workspace_admin_tooltip": "您必须是 Workspace 管理员才能使用此功能",
|
||||||
"first_event_type_webhook_description": "为此活动类型创建第一个 Webhook",
|
"first_event_type_webhook_description": "为此活动类型创建第一个 Webhook",
|
||||||
"install_app_on": "安装应用在",
|
"install_app_on": "安装应用的账户",
|
||||||
"create_for": "创建",
|
"create_for": "创建",
|
||||||
"setup_organization": "设置组织",
|
"setup_organization": "设置组织",
|
||||||
"organization_banner_description": "创建一个环境,让团队可以在其中通过轮流和集体日程安排来创建共享的应用、工作流程和活动类型。",
|
"organization_banner_description": "创建一个环境,让团队可以在其中通过轮流和集体日程安排来创建共享的应用、工作流程和活动类型。",
|
||||||
|
@ -1942,7 +1942,7 @@
|
||||||
"insights_all_org_filter": "所有应用",
|
"insights_all_org_filter": "所有应用",
|
||||||
"insights_team_filter": "团队:{{teamName}}",
|
"insights_team_filter": "团队:{{teamName}}",
|
||||||
"insights_user_filter": "用户:{{userName}}",
|
"insights_user_filter": "用户:{{userName}}",
|
||||||
"insights_subtitle": "查看您的活动的预约洞察",
|
"insights_subtitle": "查看您的活动的预约 insights",
|
||||||
"custom_plan": "自定义计划",
|
"custom_plan": "自定义计划",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,21 +81,6 @@ describe("Check Booking Limits Tests", () => {
|
||||||
})
|
})
|
||||||
).rejects.toThrowError();
|
).rejects.toThrowError();
|
||||||
});
|
});
|
||||||
it("Should return busyTimes when set", async () => {
|
|
||||||
prismaMock.booking.count.mockResolvedValue(2);
|
|
||||||
expect(
|
|
||||||
checkBookingLimit({
|
|
||||||
key: "PER_DAY",
|
|
||||||
limitingNumber: 2,
|
|
||||||
eventStartDate: MOCK_DATA.startDate,
|
|
||||||
eventId: MOCK_DATA.id,
|
|
||||||
returnBusyTimes: true,
|
|
||||||
})
|
|
||||||
).resolves.toEqual({
|
|
||||||
start: dayjs(MOCK_DATA.startDate).startOf("day").toDate(),
|
|
||||||
end: dayjs(MOCK_DATA.startDate).endOf("day").toDate(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Booking limit validation", () => {
|
describe("Booking limit validation", () => {
|
||||||
|
|
|
@ -97,21 +97,6 @@ describe("Check Duration Limit Tests", () => {
|
||||||
})
|
})
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
it("Should return busyTimes when set and limit is reached", async () => {
|
|
||||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
|
||||||
await expect(
|
|
||||||
checkDurationLimit({
|
|
||||||
key: "PER_DAY",
|
|
||||||
limitingNumber: 60,
|
|
||||||
eventStartDate: MOCK_DATA.startDate,
|
|
||||||
eventId: MOCK_DATA.id,
|
|
||||||
returnBusyTimes: true,
|
|
||||||
})
|
|
||||||
).resolves.toEqual({
|
|
||||||
start: dayjs(MOCK_DATA.startDate).startOf("day").toDate(),
|
|
||||||
end: dayjs(MOCK_DATA.startDate).endOf("day").toDate(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Duration limit validation", () => {
|
describe("Duration limit validation", () => {
|
||||||
|
|
|
@ -207,4 +207,63 @@ export async function getBusyTimes(params: {
|
||||||
return busyTimes;
|
return busyTimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getBusyTimesForLimitChecks(params: {
|
||||||
|
userId: number;
|
||||||
|
eventTypeId: number;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
}) {
|
||||||
|
const { userId, eventTypeId, startDate, endDate } = params;
|
||||||
|
logger.silly(
|
||||||
|
`Fetch limit checks bookings in range ${startDate} to ${endDate} for input ${JSON.stringify({
|
||||||
|
userId,
|
||||||
|
eventTypeId,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
performance.mark("getBusyTimesForLimitChecksStart");
|
||||||
|
|
||||||
|
const bookings = await prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
eventTypeId,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
// FIXME: bookings that overlap on one side will never be counted
|
||||||
|
startTime: {
|
||||||
|
gte: startDate,
|
||||||
|
},
|
||||||
|
endTime: {
|
||||||
|
lte: endDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const busyTimes = bookings.map(({ id, startTime, endTime, eventType, title }) => ({
|
||||||
|
start: dayjs(startTime).toDate(),
|
||||||
|
end: dayjs(endTime).toDate(),
|
||||||
|
title,
|
||||||
|
source: `eventType-${eventType?.id}-booking-${id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
|
||||||
|
performance.mark("getBusyTimesForLimitChecksEnd");
|
||||||
|
performance.measure(
|
||||||
|
`prisma booking get for limits took $1'`,
|
||||||
|
"getBusyTimesForLimitChecksStart",
|
||||||
|
"getBusyTimesForLimitChecksEnd"
|
||||||
|
);
|
||||||
|
return busyTimes;
|
||||||
|
}
|
||||||
|
|
||||||
export default getBusyTimes;
|
export default getBusyTimes;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { parseBookingLimit, parseDurationLimit } from "@calcom/lib";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
import { buildDateRanges, subtract } from "@calcom/lib/date-ranges";
|
import { buildDateRanges, subtract } from "@calcom/lib/date-ranges";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { checkBookingLimit } from "@calcom/lib/server";
|
import { checkBookingLimit } from "@calcom/lib/server";
|
||||||
import { performance } from "@calcom/lib/server/perfObserver";
|
import { performance } from "@calcom/lib/server/perfObserver";
|
||||||
|
@ -14,9 +15,14 @@ import { getTotalBookingDuration } from "@calcom/lib/server/queries";
|
||||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||||
import type { EventBusyDetails, IntervalLimit } from "@calcom/types/Calendar";
|
import type {
|
||||||
|
EventBusyDate,
|
||||||
|
EventBusyDetails,
|
||||||
|
IntervalLimit,
|
||||||
|
IntervalLimitUnit,
|
||||||
|
} from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { getBusyTimes } from "./getBusyTimes";
|
import { getBusyTimes, getBusyTimesForLimitChecks } from "./getBusyTimes";
|
||||||
|
|
||||||
const availabilitySchema = z
|
const availabilitySchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -105,7 +111,7 @@ export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Da
|
||||||
export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
|
export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
|
||||||
|
|
||||||
/** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */
|
/** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */
|
||||||
export async function getUserAvailability(
|
export const getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse(
|
||||||
query: {
|
query: {
|
||||||
withSource?: boolean;
|
withSource?: boolean;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
@ -141,17 +147,36 @@ export async function getUserAvailability(
|
||||||
if (!eventType && eventTypeId) eventType = await getEventType(eventTypeId);
|
if (!eventType && eventTypeId) eventType = await getEventType(eventTypeId);
|
||||||
|
|
||||||
/* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
|
/* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
|
||||||
current bookings with a seats event type and display them on the calendar, even if they are full */
|
current bookings with a seats event type and display them on the calendar, even if they are full */
|
||||||
let currentSeats: CurrentSeats | null = initialData?.currentSeats || null;
|
let currentSeats: CurrentSeats | null = initialData?.currentSeats || null;
|
||||||
if (!currentSeats && eventType?.seatsPerTimeSlot) {
|
if (!currentSeats && eventType?.seatsPerTimeSlot) {
|
||||||
currentSeats = await getCurrentSeats(eventType.id, dateFrom, dateTo);
|
currentSeats = await getCurrentSeats(eventType.id, dateFrom, dateTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
|
||||||
|
const durationLimits = parseDurationLimit(eventType?.durationLimits);
|
||||||
|
|
||||||
|
const busyTimesFromLimits =
|
||||||
|
eventType && (bookingLimits || durationLimits)
|
||||||
|
? await getBusyTimesFromLimits(
|
||||||
|
bookingLimits,
|
||||||
|
durationLimits,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
duration,
|
||||||
|
eventType,
|
||||||
|
user.id
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// TODO: only query what we need after applying limits (shrink date range)
|
||||||
|
const getBusyTimesStart = dateFrom.toISOString();
|
||||||
|
const getBusyTimesEnd = dateTo.toISOString();
|
||||||
|
|
||||||
const busyTimes = await getBusyTimes({
|
const busyTimes = await getBusyTimes({
|
||||||
credentials: user.credentials,
|
credentials: user.credentials,
|
||||||
// needed to correctly apply limits (weeks can be part of two months)
|
startTime: getBusyTimesStart,
|
||||||
startTime: dateFrom.startOf("week").toISOString(),
|
endTime: getBusyTimesEnd,
|
||||||
endTime: dateTo.endOf("week").toISOString(),
|
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: `${user.username}`,
|
username: `${user.username}`,
|
||||||
|
@ -161,40 +186,16 @@ export async function getUserAvailability(
|
||||||
seatedEvent: !!eventType?.seatsPerTimeSlot,
|
seatedEvent: !!eventType?.seatsPerTimeSlot,
|
||||||
});
|
});
|
||||||
|
|
||||||
let bufferedBusyTimes: EventBusyDetails[] = busyTimes.map((a) => ({
|
const detailedBusyTimes: EventBusyDetails[] = [
|
||||||
...a,
|
...busyTimes.map((a) => ({
|
||||||
start: dayjs(a.start).toISOString(),
|
...a,
|
||||||
end: dayjs(a.end).toISOString(),
|
start: dayjs(a.start).toISOString(),
|
||||||
title: a.title,
|
end: dayjs(a.end).toISOString(),
|
||||||
source: query.withSource ? a.source : undefined,
|
title: a.title,
|
||||||
}));
|
source: query.withSource ? a.source : undefined,
|
||||||
|
})),
|
||||||
const bookings = busyTimes.filter((busyTime) => busyTime.source?.startsWith(`eventType-${eventType?.id}`));
|
...busyTimesFromLimits,
|
||||||
|
];
|
||||||
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
|
|
||||||
if (bookingLimits) {
|
|
||||||
const bookingBusyTimes = await getBusyTimesFromBookingLimits(
|
|
||||||
bookings,
|
|
||||||
bookingLimits,
|
|
||||||
dateFrom,
|
|
||||||
dateTo,
|
|
||||||
eventType
|
|
||||||
);
|
|
||||||
bufferedBusyTimes = bufferedBusyTimes.concat(bookingBusyTimes);
|
|
||||||
}
|
|
||||||
|
|
||||||
const durationLimits = parseDurationLimit(eventType?.durationLimits);
|
|
||||||
if (durationLimits) {
|
|
||||||
const durationBusyTimes = await getBusyTimesFromDurationLimits(
|
|
||||||
bookings,
|
|
||||||
durationLimits,
|
|
||||||
dateFrom,
|
|
||||||
dateTo,
|
|
||||||
duration,
|
|
||||||
eventType
|
|
||||||
);
|
|
||||||
bufferedBusyTimes = bufferedBusyTimes.concat(durationBusyTimes);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userSchedule = user.schedules.filter(
|
const userSchedule = user.schedules.filter(
|
||||||
(schedule) => !user?.defaultScheduleId || schedule.id === user?.defaultScheduleId
|
(schedule) => !user?.defaultScheduleId || schedule.id === user?.defaultScheduleId
|
||||||
|
@ -239,22 +240,22 @@ export async function getUserAvailability(
|
||||||
timeZone,
|
timeZone,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formattedBusyTimes = bufferedBusyTimes.map((busy) => ({
|
const formattedBusyTimes = detailedBusyTimes.map((busy) => ({
|
||||||
start: dayjs(busy.start),
|
start: dayjs(busy.start),
|
||||||
end: dayjs(busy.end),
|
end: dayjs(busy.end),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
busy: bufferedBusyTimes,
|
busy: detailedBusyTimes,
|
||||||
timeZone,
|
timeZone,
|
||||||
dateRanges: subtract(dateRanges, formattedBusyTimes),
|
dateRanges: subtract(dateRanges, formattedBusyTimes),
|
||||||
workingHours,
|
workingHours,
|
||||||
dateOverrides,
|
dateOverrides,
|
||||||
currentSeats,
|
currentSeats,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const getDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: "day" | "week" | "month" | "year") => {
|
const getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => {
|
||||||
const dates = [];
|
const dates = [];
|
||||||
let startDate = dayjs(dateFrom).startOf(period);
|
let startDate = dayjs(dateFrom).startOf(period);
|
||||||
const endDate = dayjs(dateTo).endOf(period);
|
const endDate = dayjs(dateTo).endOf(period);
|
||||||
|
@ -265,65 +266,191 @@ const getDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: "day" | "week"
|
||||||
return dates;
|
return dates;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BusyMapKey = `${IntervalLimitUnit}-${ReturnType<Dayjs["toISOString"]>}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helps create, check, and return busy times from limits (with parallel support)
|
||||||
|
*/
|
||||||
|
class LimitManager {
|
||||||
|
private busyMap: Map<BusyMapKey, EventBusyDate> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a busy map key
|
||||||
|
*/
|
||||||
|
private static createKey(start: Dayjs, unit: IntervalLimitUnit): BusyMapKey {
|
||||||
|
return `${unit}-${start.startOf(unit).toISOString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if already marked busy by ancestors or siblings
|
||||||
|
*/
|
||||||
|
isAlreadyBusy(start: Dayjs, unit: IntervalLimitUnit) {
|
||||||
|
if (this.busyMap.has(LimitManager.createKey(start, "year"))) return true;
|
||||||
|
|
||||||
|
if (unit === "month" && this.busyMap.has(LimitManager.createKey(start, "month"))) {
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
unit === "week" &&
|
||||||
|
// weeks can be part of two months
|
||||||
|
((this.busyMap.has(LimitManager.createKey(start, "month")) &&
|
||||||
|
this.busyMap.has(LimitManager.createKey(start.endOf("week"), "month"))) ||
|
||||||
|
this.busyMap.has(LimitManager.createKey(start, "week")))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
unit === "day" &&
|
||||||
|
(this.busyMap.has(LimitManager.createKey(start, "month")) ||
|
||||||
|
this.busyMap.has(LimitManager.createKey(start, "week")) ||
|
||||||
|
this.busyMap.has(LimitManager.createKey(start, "day")))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new busy time
|
||||||
|
*/
|
||||||
|
addBusyTime(start: Dayjs, unit: IntervalLimitUnit) {
|
||||||
|
this.busyMap.set(`${unit}-${start.toISOString()}`, {
|
||||||
|
start: start.toISOString(),
|
||||||
|
end: start.endOf(unit).toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all busy times
|
||||||
|
*/
|
||||||
|
getBusyTimes() {
|
||||||
|
return Array.from(this.busyMap.values());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBusyTimesFromLimits = async (
|
||||||
|
bookingLimits: IntervalLimit | null,
|
||||||
|
durationLimits: IntervalLimit | null,
|
||||||
|
dateFrom: Dayjs,
|
||||||
|
dateTo: Dayjs,
|
||||||
|
duration: number | undefined,
|
||||||
|
eventType: NonNullable<EventType>,
|
||||||
|
userId: number
|
||||||
|
) => {
|
||||||
|
performance.mark("limitsStart");
|
||||||
|
|
||||||
|
// shared amongst limiters to prevent processing known busy periods
|
||||||
|
const limitManager = new LimitManager();
|
||||||
|
|
||||||
|
let limitDateFrom = dayjs(dateFrom);
|
||||||
|
let limitDateTo = dayjs(dateTo);
|
||||||
|
|
||||||
|
// expand date ranges by absolute minimum required to apply limits
|
||||||
|
// (yearly limits are handled separately for performance)
|
||||||
|
for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"] as Exclude<keyof IntervalLimit, "PER_YEAR">[]) {
|
||||||
|
if (bookingLimits?.[key] || durationLimits?.[key]) {
|
||||||
|
const unit = intervalLimitKeyToUnit(key);
|
||||||
|
limitDateFrom = dayjs.min(limitDateFrom, dateFrom.startOf(unit));
|
||||||
|
limitDateTo = dayjs.max(limitDateTo, dateTo.endOf(unit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch only the data we need to check limits
|
||||||
|
const bookings = await getBusyTimesForLimitChecks({
|
||||||
|
userId,
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
startDate: limitDateFrom.toDate(),
|
||||||
|
endDate: limitDateTo.toDate(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// run this first, as counting bookings should always run faster..
|
||||||
|
if (bookingLimits) {
|
||||||
|
performance.mark("bookingLimitsStart");
|
||||||
|
await getBusyTimesFromBookingLimits(
|
||||||
|
bookings,
|
||||||
|
bookingLimits,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
eventType.id,
|
||||||
|
limitManager
|
||||||
|
);
|
||||||
|
performance.mark("bookingLimitsEnd");
|
||||||
|
performance.measure(`checking booking limits took $1'`, "bookingLimitsStart", "bookingLimitsEnd");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..than adding up durations (especially for the whole year)
|
||||||
|
if (durationLimits) {
|
||||||
|
performance.mark("durationLimitsStart");
|
||||||
|
await getBusyTimesFromDurationLimits(
|
||||||
|
bookings,
|
||||||
|
durationLimits,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
duration,
|
||||||
|
eventType,
|
||||||
|
limitManager
|
||||||
|
);
|
||||||
|
performance.mark("durationLimitsEnd");
|
||||||
|
performance.measure(`checking duration limits took $1'`, "durationLimitsStart", "durationLimitsEnd");
|
||||||
|
}
|
||||||
|
|
||||||
|
performance.mark("limitsEnd");
|
||||||
|
performance.measure(`checking all limits took $1'`, "limitsStart", "limitsEnd");
|
||||||
|
|
||||||
|
return limitManager.getBusyTimes();
|
||||||
|
};
|
||||||
|
|
||||||
const getBusyTimesFromBookingLimits = async (
|
const getBusyTimesFromBookingLimits = async (
|
||||||
bookings: EventBusyDetails[],
|
bookings: EventBusyDetails[],
|
||||||
bookingLimits: IntervalLimit,
|
bookingLimits: IntervalLimit,
|
||||||
dateFrom: Dayjs,
|
dateFrom: Dayjs,
|
||||||
dateTo: Dayjs,
|
dateTo: Dayjs,
|
||||||
eventType: EventType | undefined
|
eventTypeId: number,
|
||||||
|
limitManager: LimitManager
|
||||||
) => {
|
) => {
|
||||||
const busyTimes: EventBusyDetails[] = [];
|
for (const key of descendingLimitKeys) {
|
||||||
|
const limit = bookingLimits?.[key];
|
||||||
|
if (!limit) continue;
|
||||||
|
|
||||||
// Apply booking limit filter against our bookings
|
const unit = intervalLimitKeyToUnit(key);
|
||||||
for (const [key, limit] of Object.entries(bookingLimits)) {
|
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit);
|
||||||
const limitKey = key as keyof IntervalLimit;
|
|
||||||
|
|
||||||
if (limitKey === "PER_YEAR") {
|
for (const periodStart of periodStartDates) {
|
||||||
const yearlyBusyTime = await checkBookingLimit({
|
if (limitManager.isAlreadyBusy(periodStart, unit)) continue;
|
||||||
eventStartDate: dateFrom.toDate(),
|
|
||||||
limitingNumber: limit,
|
|
||||||
eventId: eventType?.id as number,
|
|
||||||
key: "PER_YEAR",
|
|
||||||
returnBusyTimes: true,
|
|
||||||
});
|
|
||||||
if (!yearlyBusyTime) break;
|
|
||||||
busyTimes.push({
|
|
||||||
start: yearlyBusyTime.start.toISOString(),
|
|
||||||
end: yearlyBusyTime.end.toISOString(),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take PER_DAY and turn it into day and PER_WEEK into week etc.
|
// special handling of yearly limits to improve performance
|
||||||
const filter = key.split("_")[1].toLowerCase() as "day" | "week" | "month" | "year";
|
if (unit === "year") {
|
||||||
const dates = getDatesBetween(dateFrom, dateTo, filter);
|
try {
|
||||||
|
await checkBookingLimit({
|
||||||
|
eventStartDate: periodStart.toDate(),
|
||||||
|
limitingNumber: limit,
|
||||||
|
eventId: eventTypeId,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
limitManager.addBusyTime(periodStart, unit);
|
||||||
|
if (periodStartDates.every((start) => limitManager.isAlreadyBusy(start, unit))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodEnd = periodStart.endOf(unit);
|
||||||
|
let totalBookings = 0;
|
||||||
|
|
||||||
// loop through all dates and check if we have reached the limit
|
|
||||||
for (const date of dates) {
|
|
||||||
let total = 0;
|
|
||||||
const startDate = date.startOf(filter);
|
|
||||||
// this is parsed above with parseBookingLimit so we know it's safe.
|
|
||||||
const endDate = date.endOf(filter);
|
|
||||||
for (const booking of bookings) {
|
for (const booking of bookings) {
|
||||||
const bookingEventTypeId = parseInt(booking.source?.split("-")[1] as string, 10);
|
// consider booking part of period independent of end date
|
||||||
if (
|
if (!dayjs(booking.start).isBetween(periodStart, periodEnd)) {
|
||||||
// Only check OUR booking that matches the current eventTypeId
|
|
||||||
// we don't care about another event type in this case as we dont need to know their booking limits
|
|
||||||
!(bookingEventTypeId == eventType?.id && dayjs(booking.start).isBetween(startDate, endDate))
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// increment total and check against the limit, adding a busy time if condition is met.
|
totalBookings++;
|
||||||
total++;
|
if (totalBookings >= limit) {
|
||||||
if (total >= limit) {
|
limitManager.addBusyTime(periodStart, unit);
|
||||||
busyTimes.push({ start: startDate.toISOString(), end: endDate.toISOString() });
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return busyTimes;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBusyTimesFromDurationLimits = async (
|
const getBusyTimesFromDurationLimits = async (
|
||||||
|
@ -332,60 +459,56 @@ const getBusyTimesFromDurationLimits = async (
|
||||||
dateFrom: Dayjs,
|
dateFrom: Dayjs,
|
||||||
dateTo: Dayjs,
|
dateTo: Dayjs,
|
||||||
duration: number | undefined,
|
duration: number | undefined,
|
||||||
eventType: EventType | undefined
|
eventType: NonNullable<EventType>,
|
||||||
|
limitManager: LimitManager
|
||||||
) => {
|
) => {
|
||||||
const busyTimes: EventBusyDetails[] = [];
|
for (const key of descendingLimitKeys) {
|
||||||
// Start check from larger time periods to smaller time periods, to skip unnecessary checks
|
const limit = durationLimits?.[key];
|
||||||
for (const [key, limit] of Object.entries(durationLimits).reverse()) {
|
if (!limit) continue;
|
||||||
// Use aggregate sql query if we are checking PER_YEAR
|
|
||||||
if (key === "PER_YEAR") {
|
|
||||||
const totalBookingDuration = await getTotalBookingDuration({
|
|
||||||
eventId: eventType?.id as number,
|
|
||||||
startDate: dateFrom.startOf("year").toDate(),
|
|
||||||
endDate: dateFrom.endOf("year").toDate(),
|
|
||||||
});
|
|
||||||
if (totalBookingDuration + (duration ?? 0) > limit) {
|
|
||||||
busyTimes.push({
|
|
||||||
start: dateFrom.startOf("year").toISOString(),
|
|
||||||
end: dateFrom.endOf("year").toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filter = key.split("_")[1].toLowerCase() as "day" | "week" | "month" | "year";
|
const unit = intervalLimitKeyToUnit(key);
|
||||||
const dates = getDatesBetween(dateFrom, dateTo, filter);
|
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit);
|
||||||
|
|
||||||
// loop through all dates and check if we have reached the limit
|
for (const periodStart of periodStartDates) {
|
||||||
for (const date of dates) {
|
if (limitManager.isAlreadyBusy(periodStart, unit)) continue;
|
||||||
let total = (duration || eventType?.length) ?? 0;
|
|
||||||
const startDate = date.startOf(filter);
|
|
||||||
const endDate = date.endOf(filter);
|
|
||||||
|
|
||||||
// add busy time if we have already reached the limit with just the selected duration
|
const selectedDuration = (duration || eventType.length) ?? 0;
|
||||||
if (total > limit) {
|
|
||||||
busyTimes.push({ start: startDate.toISOString(), end: endDate.toISOString() });
|
if (selectedDuration > limit) {
|
||||||
|
limitManager.addBusyTime(periodStart, unit);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// special handling of yearly limits to improve performance
|
||||||
|
if (unit === "year") {
|
||||||
|
const totalYearlyDuration = await getTotalBookingDuration({
|
||||||
|
eventId: eventType.id,
|
||||||
|
startDate: periodStart.toDate(),
|
||||||
|
endDate: periodStart.endOf(unit).toDate(),
|
||||||
|
});
|
||||||
|
if (totalYearlyDuration + selectedDuration > limit) {
|
||||||
|
limitManager.addBusyTime(periodStart, unit);
|
||||||
|
if (periodStartDates.every((start) => limitManager.isAlreadyBusy(start, unit))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodEnd = periodStart.endOf(unit);
|
||||||
|
let totalDuration = selectedDuration;
|
||||||
|
|
||||||
for (const booking of bookings) {
|
for (const booking of bookings) {
|
||||||
const bookingEventTypeId = parseInt(booking.source?.split("-")[1] as string, 10);
|
// consider booking part of period independent of end date
|
||||||
if (
|
if (!dayjs(booking.start).isBetween(periodStart, periodEnd)) {
|
||||||
// Only check OUR booking that matches the current eventTypeId
|
|
||||||
// we don't care about another event type in this case as we dont need to know their booking limits
|
|
||||||
!(bookingEventTypeId == eventType?.id && dayjs(booking.start).isBetween(startDate, endDate))
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Add current booking duration to total and check against the limit, adding a busy time if condition is met.
|
totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute");
|
||||||
total += dayjs(booking.end).diff(dayjs(booking.start), "minute");
|
if (totalDuration > limit) {
|
||||||
if (total > limit) {
|
limitManager.addBusyTime(periodStart, unit);
|
||||||
busyTimes.push({ start: startDate.toISOString(), end: endDate.toISOString() });
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return busyTimes;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates in document the number of times a component has been rendered. Helps in 2 ways. Using it doesn't cause any additional renders.
|
||||||
|
* - Did the component render when it shouldn't have?
|
||||||
|
* - Did the component reset its state when it shouldn't have?
|
||||||
|
*/
|
||||||
|
export const RenderCounter = ({ label }: { label: string }) => {
|
||||||
|
const counterRef = useRef(0);
|
||||||
|
counterRef.current++;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span>{label}:</span>
|
||||||
|
<span>{counterRef.current} </span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export { RenderCounter } from "./components/RenderCounter";
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "@calcom/debugging",
|
||||||
|
"description": "Debugging utilities",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "./index.ts"
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": "@calcom/tsconfig/react-library.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../../apps/web/next-env.d.ts",
|
||||||
|
"../types/*.d.ts",
|
||||||
|
"../types/next-auth.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
|
|
||||||
|
export default function MainLayout({
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
|
return (
|
||||||
|
<Shell withoutMain={true} {...rest}>
|
||||||
|
{children}
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLayout = (page: React.ReactElement) => <MainLayout>{page}</MainLayout>;
|
|
@ -298,7 +298,7 @@ const BookerComponent = ({
|
||||||
<m.span
|
<m.span
|
||||||
key="logo"
|
key="logo"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-6 mt-auto pt-6 [&_img]:h-[15px]",
|
"-z-10 mb-6 mt-auto pt-6 [&_img]:h-[15px]",
|
||||||
hasDarkBackground ? "dark" : "",
|
hasDarkBackground ? "dark" : "",
|
||||||
layout === BookerLayouts.MONTH_VIEW ? "block" : "hidden"
|
layout === BookerLayouts.MONTH_VIEW ? "block" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
|
||||||
import { HorizontalTabs } from "@calcom/ui";
|
|
||||||
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { FiltersContainer } from "../components/FiltersContainer";
|
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
|
||||||
{
|
|
||||||
name: "upcoming",
|
|
||||||
href: "/bookings/upcoming",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unconfirmed",
|
|
||||||
href: "/bookings/unconfirmed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "recurring",
|
|
||||||
href: "/bookings/recurring",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "past",
|
|
||||||
href: "/bookings/past",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cancelled",
|
|
||||||
href: "/bookings/cancelled",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function BookingLayout({
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
|
||||||
return (
|
|
||||||
<Shell {...rest} hideHeadingOnMobile>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex flex-col flex-wrap lg:flex-row">
|
|
||||||
<HorizontalTabs tabs={tabs} />
|
|
||||||
<div className="max-w-full overflow-x-auto xl:ml-auto">
|
|
||||||
<FiltersContainer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<main className="w-full">{children}</main>
|
|
||||||
</div>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export const getLayout = (page: React.ReactElement) => <BookingLayout>{page}</BookingLayout>;
|
|
|
@ -652,7 +652,6 @@ async function handler(
|
||||||
|
|
||||||
const fullName = getFullName(bookerName);
|
const fullName = getFullName(bookerName);
|
||||||
|
|
||||||
const tAttendees = await getTranslation(language ?? "en", "common");
|
|
||||||
const tGuests = await getTranslation("en", "common");
|
const tGuests = await getTranslation("en", "common");
|
||||||
log.debug(`Booking eventType ${eventTypeId} started`);
|
log.debug(`Booking eventType ${eventTypeId} started`);
|
||||||
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
|
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
|
||||||
|
@ -848,6 +847,46 @@ async function handler(
|
||||||
|
|
||||||
const allCredentials = await getAllCredentials(organizerUser, eventType);
|
const allCredentials = await getAllCredentials(organizerUser, eventType);
|
||||||
|
|
||||||
|
let rescheduleUid = reqBody.rescheduleUid;
|
||||||
|
let bookingSeat: Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null = null;
|
||||||
|
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||||
|
let originalRescheduledBooking: BookingType = null;
|
||||||
|
|
||||||
|
if (rescheduleUid) {
|
||||||
|
// rescheduleUid can be bookingUid and bookingSeatUid
|
||||||
|
bookingSeat = await prisma.bookingSeat.findUnique({
|
||||||
|
where: {
|
||||||
|
referenceUid: rescheduleUid,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
booking: true,
|
||||||
|
attendee: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (bookingSeat) {
|
||||||
|
rescheduleUid = bookingSeat.booking.uid;
|
||||||
|
}
|
||||||
|
originalRescheduledBooking = await getOriginalRescheduledBooking(
|
||||||
|
rescheduleUid,
|
||||||
|
!!eventType.seatsPerTimeSlot
|
||||||
|
);
|
||||||
|
if (!originalRescheduledBooking) {
|
||||||
|
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOrganizerRescheduling = organizerUser.id === userId;
|
||||||
|
|
||||||
|
const attendeeInfoOnReschedule =
|
||||||
|
isOrganizerRescheduling && originalRescheduledBooking
|
||||||
|
? originalRescheduledBooking.attendees.find((attendee) => attendee.email === bookerEmail)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const attendeeLanguage = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.locale : language;
|
||||||
|
const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone;
|
||||||
|
|
||||||
|
const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common");
|
||||||
|
|
||||||
// use host default
|
// use host default
|
||||||
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
|
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
|
||||||
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
||||||
|
@ -868,8 +907,8 @@ async function handler(
|
||||||
name: fullName,
|
name: fullName,
|
||||||
firstName: (typeof bookerName === "object" && bookerName.firstName) || "",
|
firstName: (typeof bookerName === "object" && bookerName.firstName) || "",
|
||||||
lastName: (typeof bookerName === "object" && bookerName.lastName) || "",
|
lastName: (typeof bookerName === "object" && bookerName.lastName) || "",
|
||||||
timeZone: reqBody.timeZone,
|
timeZone: attendeeTimezone,
|
||||||
language: { translate: tAttendees, locale: language ?? "en" },
|
language: { translate: tAttendees, locale: attendeeLanguage ?? "en" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -883,7 +922,7 @@ async function handler(
|
||||||
name: "",
|
name: "",
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
timeZone: reqBody.timeZone,
|
timeZone: attendeeTimezone,
|
||||||
language: { translate: tGuests, locale: "en" },
|
language: { translate: tGuests, locale: "en" },
|
||||||
});
|
});
|
||||||
return guestArray;
|
return guestArray;
|
||||||
|
@ -982,34 +1021,6 @@ async function handler(
|
||||||
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
||||||
};
|
};
|
||||||
|
|
||||||
let rescheduleUid = reqBody.rescheduleUid;
|
|
||||||
let bookingSeat: Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null = null;
|
|
||||||
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
|
||||||
let originalRescheduledBooking: BookingType = null;
|
|
||||||
|
|
||||||
if (rescheduleUid) {
|
|
||||||
// rescheduleUid can be bookingUid and bookingSeatUid
|
|
||||||
bookingSeat = await prisma.bookingSeat.findUnique({
|
|
||||||
where: {
|
|
||||||
referenceUid: rescheduleUid,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
booking: true,
|
|
||||||
attendee: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (bookingSeat) {
|
|
||||||
rescheduleUid = bookingSeat.booking.uid;
|
|
||||||
}
|
|
||||||
originalRescheduledBooking = await getOriginalRescheduledBooking(
|
|
||||||
rescheduleUid,
|
|
||||||
!!eventType.seatsPerTimeSlot
|
|
||||||
);
|
|
||||||
if (!originalRescheduledBooking) {
|
|
||||||
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Used for seats bookings to update evt object with video data */
|
/* Used for seats bookings to update evt object with video data */
|
||||||
const addVideoCallDataToEvt = (bookingReferences: BookingReference[]) => {
|
const addVideoCallDataToEvt = (bookingReferences: BookingReference[]) => {
|
||||||
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
||||||
|
@ -1397,7 +1408,7 @@ async function handler(
|
||||||
const updatedBookingAttendees = updatedNewBooking.attendees.map((attendee) => {
|
const updatedBookingAttendees = updatedNewBooking.attendees.map((attendee) => {
|
||||||
const evtAttendee = {
|
const evtAttendee = {
|
||||||
...attendee,
|
...attendee,
|
||||||
language: { translate: tAttendees, locale: language ?? "en" },
|
language: { translate: tAttendees, locale: attendeeLanguage ?? "en" },
|
||||||
};
|
};
|
||||||
return evtAttendee;
|
return evtAttendee;
|
||||||
});
|
});
|
||||||
|
@ -1529,7 +1540,7 @@ async function handler(
|
||||||
} else {
|
} else {
|
||||||
// Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language
|
// Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language
|
||||||
const bookingAttendees = booking.attendees.map((attendee) => {
|
const bookingAttendees = booking.attendees.map((attendee) => {
|
||||||
return { ...attendee, language: { translate: tAttendees, locale: language ?? "en" } };
|
return { ...attendee, language: { translate: tAttendees, locale: attendeeLanguage ?? "en" } };
|
||||||
});
|
});
|
||||||
|
|
||||||
evt = { ...evt, attendees: [...bookingAttendees, invitee[0]] };
|
evt = { ...evt, attendees: [...bookingAttendees, invitee[0]] };
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Select } from "@calcom/ui";
|
import { Select } from "@calcom/ui";
|
||||||
|
import { Check } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
onChange: (value: { externalId: string; integration: string }) => void;
|
||||||
|
@ -37,7 +38,10 @@ const OptionComponent = ({ ...props }: OptionProps<Option>) => {
|
||||||
const { label } = props.data;
|
const { label } = props.data;
|
||||||
return (
|
return (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<span>{label}</span>
|
<div className="flex">
|
||||||
|
<span className="mr-auto">{label}</span>
|
||||||
|
{props.isSelected && <Check className="ml-2 h-4 w-4" />}
|
||||||
|
</div>
|
||||||
</components.Option>
|
</components.Option>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,14 +34,28 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
where: { metadata: { path: ["paymentId"], equals: checkoutSession.id } },
|
where: { metadata: { path: ["paymentId"], equals: checkoutSession.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let metadata;
|
||||||
|
|
||||||
if (!team) {
|
if (!team) {
|
||||||
const prevTeam = await prisma.team.findFirstOrThrow({ where: { id } });
|
const prevTeam = await prisma.team.findFirstOrThrow({ where: { id } });
|
||||||
const metadata = teamMetadataSchema.parse(prevTeam.metadata);
|
|
||||||
|
metadata = teamMetadataSchema.safeParse(prevTeam.metadata);
|
||||||
|
if (!metadata.success) throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
|
||||||
|
|
||||||
|
if (!metadata.data?.requestedSlug) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Can't publish team/org without `requestedSlug`",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { requestedSlug, ...newMetadata } = metadata.data;
|
||||||
/** We save the metadata first to prevent duplicate payments */
|
/** We save the metadata first to prevent duplicate payments */
|
||||||
team = await prisma.team.update({
|
team = await prisma.team.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
|
...newMetadata,
|
||||||
paymentId: checkoutSession.id,
|
paymentId: checkoutSession.id,
|
||||||
subscriptionId: subscription.id || null,
|
subscriptionId: subscription.id || null,
|
||||||
subscriptionItemId: subscription.items.data[0].id || null,
|
subscriptionItemId: subscription.items.data[0].id || null,
|
||||||
|
@ -49,7 +63,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
/** Legacy teams already have a slug, this will allow them to upgrade as well */
|
/** Legacy teams already have a slug, this will allow them to upgrade as well */
|
||||||
const slug = prevTeam.slug || metadata?.requestedSlug;
|
const slug = prevTeam.slug || requestedSlug;
|
||||||
if (slug) {
|
if (slug) {
|
||||||
try {
|
try {
|
||||||
/** Then we try to upgrade the slug, which may fail if a conflict came up since team creation */
|
/** Then we try to upgrade the slug, which may fail if a conflict came up since team creation */
|
||||||
|
@ -64,12 +78,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
closeComUpdateTeam(prevTeam, team);
|
closeComUpdateTeam(prevTeam, team);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!metadata) {
|
||||||
|
metadata = teamMetadataSchema.safeParse(team.metadata);
|
||||||
|
if (!metadata.success) throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
|
||||||
|
}
|
||||||
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
|
|
||||||
if (!session) return { message: "Team upgraded successfully" };
|
if (!session) return { message: "Team upgraded successfully" };
|
||||||
|
|
||||||
|
const redirectUrl = metadata?.data?.isOrganization
|
||||||
|
? `${WEBAPP_URL}/settings/organizations/profile?upgraded=true`
|
||||||
|
: `${WEBAPP_URL}/settings/teams/${team.id}/profile?upgraded=true`;
|
||||||
|
|
||||||
// redirect to team screen
|
// redirect to team screen
|
||||||
res.redirect(302, `${WEBAPP_URL}/settings/teams/${team.id}/profile?upgraded=true`);
|
res.redirect(302, redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultHandler({
|
export default defaultHandler({
|
||||||
|
|
|
@ -29,7 +29,6 @@ import {
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { ExternalLink, MoreHorizontal, Edit2, Lock, UserX } from "@calcom/ui/components/icon";
|
import { ExternalLink, MoreHorizontal, Edit2, Lock, UserX } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { useOrgBranding } from "../../organizations/context/provider";
|
|
||||||
import MemberChangeRoleModal from "./MemberChangeRoleModal";
|
import MemberChangeRoleModal from "./MemberChangeRoleModal";
|
||||||
import TeamAvailabilityModal from "./TeamAvailabilityModal";
|
import TeamAvailabilityModal from "./TeamAvailabilityModal";
|
||||||
import TeamPill, { TeamRole } from "./TeamPill";
|
import TeamPill, { TeamRole } from "./TeamPill";
|
||||||
|
@ -54,7 +53,6 @@ const checkIsOrg = (team: Props["team"]) => {
|
||||||
|
|
||||||
export default function MemberListItem(props: Props) {
|
export default function MemberListItem(props: Props) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const orgBranding = useOrgBranding();
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false);
|
const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false);
|
||||||
|
@ -115,7 +113,7 @@ export default function MemberListItem(props: Props) {
|
||||||
return (
|
return (
|
||||||
<li className="divide-subtle divide-y px-5">
|
<li className="divide-subtle divide-y px-5">
|
||||||
<div className="my-4 flex justify-between">
|
<div className="my-4 flex justify-between">
|
||||||
<div className="flex w-full flex-col justify-between sm:flex-row">
|
<div className="flex w-full flex-col justify-between truncate sm:flex-row">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
@ -109,7 +109,7 @@ function UsersTableBare() {
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="rounded-md border"
|
className="border-subtle rounded-md border"
|
||||||
ref={tableContainerRef}
|
ref={tableContainerRef}
|
||||||
onScroll={() => fetchMoreOnBottomReached()}
|
onScroll={() => fetchMoreOnBottomReached()}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { useRouter } from "next/navigation";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -49,7 +50,7 @@ function WorkflowsPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<ShellMain
|
||||||
heading={t("workflows")}
|
heading={t("workflows")}
|
||||||
title={t("workflows")}
|
title={t("workflows")}
|
||||||
subtitle={t("workflows_to_automate_notifications")}
|
subtitle={t("workflows_to_automate_notifications")}
|
||||||
|
@ -92,7 +93,7 @@ function WorkflowsPage() {
|
||||||
</FilterResults>
|
</FilterResults>
|
||||||
</>
|
</>
|
||||||
</LicenseRequired>
|
</LicenseRequired>
|
||||||
</Shell>
|
</ShellMain>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,4 +222,6 @@ const Filter = (props: {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WorkflowsPage.getLayout = getLayout;
|
||||||
|
|
||||||
export default WorkflowsPage;
|
export default WorkflowsPage;
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const CheckedTeamSelect = ({
|
||||||
{/* This class name conditional looks a bit odd but it allows a seemless transition when using autoanimate
|
{/* This class name conditional looks a bit odd but it allows a seemless transition when using autoanimate
|
||||||
- Slides down from the top instead of just teleporting in from nowhere*/}
|
- Slides down from the top instead of just teleporting in from nowhere*/}
|
||||||
<ul
|
<ul
|
||||||
className={classNames("mt-3 rounded-md", value.length >= 1 && "border-subtle border")}
|
className={classNames("mb-1 mt-3 rounded-md", value.length >= 1 && "border-subtle border")}
|
||||||
ref={animationRef}>
|
ref={animationRef}>
|
||||||
{value.map((option, index) => (
|
{value.map((option, index) => (
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -69,7 +69,6 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-block min-w-[88px] text-sm capitalize">{weekday}</span>
|
<span className="inline-block min-w-[88px] text-sm capitalize">{weekday}</span>
|
||||||
{watchDayRange && !!watchDayRange.length && <div className="sm:hidden">{CopyButton}</div>}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +76,7 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
|
||||||
{watchDayRange ? (
|
{watchDayRange ? (
|
||||||
<div className="flex sm:ml-2">
|
<div className="flex sm:ml-2">
|
||||||
<DayRanges control={control} name={name} />
|
<DayRanges control={control} name={name} />
|
||||||
{!!watchDayRange.length && <div className="hidden sm:block">{CopyButton}</div>}
|
{!!watchDayRange.length && <div className="block">{CopyButton}</div>}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<SkeletonText className="ml-1 mt-2.5 h-6 w-48" />
|
<SkeletonText className="ml-1 mt-2.5 h-6 w-48" />
|
||||||
|
@ -244,7 +243,7 @@ const RemoveTimeButton = ({
|
||||||
const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => {
|
const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => {
|
||||||
// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
|
// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={classNames("flex flex-row gap-1",className)}>
|
||||||
<LazySelect
|
<LazySelect
|
||||||
className="inline-block w-[100px]"
|
className="inline-block w-[100px]"
|
||||||
value={value.start}
|
value={value.start}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge";
|
import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge";
|
||||||
import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner";
|
import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner";
|
||||||
import { OrgUpgradeBanner } from "@calcom/features/ee/organizations/components/OrgUpgradeBanner";
|
import { OrgUpgradeBanner } from "@calcom/features/ee/organizations/components/OrgUpgradeBanner";
|
||||||
|
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem";
|
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem";
|
||||||
import { TeamsUpgradeBanner } from "@calcom/features/ee/teams/components";
|
import { TeamsUpgradeBanner } from "@calcom/features/ee/teams/components";
|
||||||
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||||
|
@ -788,9 +789,6 @@ function SideBarContainer({ bannersHeight }: SideBarContainerProps) {
|
||||||
return <SideBar bannersHeight={bannersHeight} user={data?.user} />;
|
return <SideBar bannersHeight={bannersHeight} user={data?.user} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrganizationUrl = (slug: string) =>
|
|
||||||
`${slug}.${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace?.(/http(s*):\/\//, "")}`;
|
|
||||||
|
|
||||||
function SideBar({ bannersHeight, user }: SideBarProps) {
|
function SideBar({ bannersHeight, user }: SideBarProps) {
|
||||||
const { t, isLocaleReady } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
|
@ -798,7 +796,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
||||||
|
|
||||||
const publicPageUrl = useMemo(() => {
|
const publicPageUrl = useMemo(() => {
|
||||||
if (!user?.organizationId) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`;
|
if (!user?.organizationId) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`;
|
||||||
const publicPageUrl = orgBranding?.slug ? getOrganizationUrl(orgBranding?.slug) : "";
|
const publicPageUrl = orgBranding?.slug ? getOrgFullDomain(orgBranding.slug) : "";
|
||||||
return publicPageUrl;
|
return publicPageUrl;
|
||||||
}, [orgBranding?.slug, user?.organizationId, user?.username]);
|
}, [orgBranding?.slug, user?.organizationId, user?.username]);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
import type { Prisma } from ".prisma/client";
|
import type { Prisma } from ".prisma/client";
|
||||||
|
|
||||||
|
type EnabledApp = ReturnType<typeof getApps>[number] & { enabled: boolean };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param credentials - Can be user or team credentials
|
* @param credentials - Can be user or team credentials
|
||||||
|
@ -39,14 +41,13 @@ const getEnabledApps = async (credentials: CredentialDataWithTeamName[], filterO
|
||||||
select: { slug: true, enabled: true },
|
select: { slug: true, enabled: true },
|
||||||
});
|
});
|
||||||
const apps = getApps(credentials, filterOnCredentials);
|
const apps = getApps(credentials, filterOnCredentials);
|
||||||
|
const filteredApps = apps.reduce((reducedArray, app) => {
|
||||||
const filteredApps = enabledApps.reduce((reducedArray, app) => {
|
const appDbQuery = enabledApps.find((metadata) => metadata.slug === app.slug);
|
||||||
const appMetadata = apps.find((metadata) => metadata.slug === app.slug);
|
if (appDbQuery?.enabled || app.isGlobal) {
|
||||||
if (appMetadata) {
|
reducedArray.push({ ...app, enabled: true });
|
||||||
reducedArray.push({ ...appMetadata, enabled: app.enabled });
|
|
||||||
}
|
}
|
||||||
return reducedArray;
|
return reducedArray;
|
||||||
}, [] as (ReturnType<typeof getApps>[number] & { enabled: boolean })[]);
|
}, [] as EnabledApp[]);
|
||||||
|
|
||||||
return filteredApps;
|
return filteredApps;
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,6 +29,36 @@ describe("processWorkingHours", () => {
|
||||||
end: dayjs(`${dateTo.tz(timeZone).format("YYYY-MM-DD")}T21:00:00Z`).tz(timeZone),
|
end: dayjs(`${dateTo.tz(timeZone).format("YYYY-MM-DD")}T21:00:00Z`).tz(timeZone),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it("should return the correct working hours in the month were DST ends", () => {
|
||||||
|
const item = {
|
||||||
|
days: [0, 1, 2, 3, 4, 5, 6], // Monday to Sunday
|
||||||
|
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
|
||||||
|
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
|
||||||
|
};
|
||||||
|
|
||||||
|
// in America/New_York DST ends on first Sunday of November
|
||||||
|
const timeZone = "America/New_York";
|
||||||
|
|
||||||
|
let firstSundayOfNovember = dayjs().startOf("day").month(10).date(1);
|
||||||
|
while (firstSundayOfNovember.day() !== 0) {
|
||||||
|
firstSundayOfNovember = firstSundayOfNovember.add(1, "day");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateFrom = dayjs().month(10).date(1).startOf("day");
|
||||||
|
const dateTo = dayjs().month(10).endOf("month");
|
||||||
|
|
||||||
|
const results = processWorkingHours({ item, timeZone, dateFrom, dateTo });
|
||||||
|
|
||||||
|
const allDSTStartAt12 = results
|
||||||
|
.filter((res) => res.start.isBefore(firstSundayOfNovember))
|
||||||
|
.every((result) => result.start.utc().hour() === 12);
|
||||||
|
const allNotDSTStartAt13 = results
|
||||||
|
.filter((res) => res.start.isAfter(firstSundayOfNovember))
|
||||||
|
.every((result) => result.start.utc().hour() === 13);
|
||||||
|
|
||||||
|
expect(allDSTStartAt12).toBeTruthy();
|
||||||
|
expect(allNotDSTStartAt13).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("processDateOverrides", () => {
|
describe("processDateOverrides", () => {
|
||||||
|
|
|
@ -23,12 +23,21 @@ export function processWorkingHours({
|
||||||
}) {
|
}) {
|
||||||
const results = [];
|
const results = [];
|
||||||
for (let date = dateFrom.tz(timeZone).startOf("day"); dateTo.isAfter(date); date = date.add(1, "day")) {
|
for (let date = dateFrom.tz(timeZone).startOf("day"); dateTo.isAfter(date); date = date.add(1, "day")) {
|
||||||
if (!item.days.includes(date.day())) {
|
const dateInTz = date.tz(timeZone);
|
||||||
|
|
||||||
|
if (!item.days.includes(dateInTz.day())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = date.hour(item.startTime.getUTCHours()).minute(item.startTime.getUTCMinutes()).second(0);
|
let start = dateInTz.hour(item.startTime.getUTCHours()).minute(item.startTime.getUTCMinutes()).second(0);
|
||||||
const end = date.hour(item.endTime.getUTCHours()).minute(item.endTime.getUTCMinutes()).second(0);
|
|
||||||
|
let end = dateInTz.hour(item.endTime.getUTCHours()).minute(item.endTime.getUTCMinutes()).second(0);
|
||||||
|
|
||||||
|
const offsetBeginningOfDay = dayjs(start.format("YYYY-MM-DD hh:mm")).tz(timeZone).utcOffset();
|
||||||
|
const offsetDiff = start.utcOffset() - offsetBeginningOfDay; // there will be 60 min offset on the day day of DST change
|
||||||
|
|
||||||
|
start = start.add(offsetDiff, "minute");
|
||||||
|
end = end.add(offsetDiff, "minute");
|
||||||
|
|
||||||
const startResult = dayjs.max(start, dateFrom.tz(timeZone));
|
const startResult = dayjs.max(start, dateFrom.tz(timeZone));
|
||||||
const endResult = dayjs.min(end, dateTo.tz(timeZone));
|
const endResult = dayjs.min(end, dateTo.tz(timeZone));
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { IntervalLimit, IntervalLimitUnit } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
export const ascendingLimitKeys: (keyof IntervalLimit)[] = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"];
|
||||||
|
|
||||||
|
export const descendingLimitKeys = [...ascendingLimitKeys].reverse();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns `PER_DAY` into `day`, `PER_WEEK` into `week` etc.
|
||||||
|
*/
|
||||||
|
export function intervalLimitKeyToUnit(key: keyof IntervalLimit) {
|
||||||
|
return key.split("_")[1].toLowerCase() as IntervalLimitUnit;
|
||||||
|
}
|
|
@ -1,34 +1,31 @@
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
import { getErrorFromUnknown } from "../errors";
|
||||||
import { HttpError } from "../http-error";
|
import { HttpError } from "../http-error";
|
||||||
|
import { ascendingLimitKeys, intervalLimitKeyToUnit } from "../intervalLimit";
|
||||||
import { parseBookingLimit } from "../isBookingLimits";
|
import { parseBookingLimit } from "../isBookingLimits";
|
||||||
|
|
||||||
export async function checkBookingLimits(
|
export async function checkBookingLimits(
|
||||||
bookingLimits: IntervalLimit,
|
bookingLimits: IntervalLimit,
|
||||||
eventStartDate: Date,
|
eventStartDate: Date,
|
||||||
eventId: number,
|
eventId: number
|
||||||
returnBusyTimes?: boolean
|
|
||||||
) {
|
) {
|
||||||
const parsedBookingLimits = parseBookingLimit(bookingLimits);
|
const parsedBookingLimits = parseBookingLimit(bookingLimits);
|
||||||
if (parsedBookingLimits) {
|
if (!parsedBookingLimits) return false;
|
||||||
const limitCalculations = Object.entries(parsedBookingLimits).map(
|
|
||||||
async ([key, limitingNumber]) =>
|
// not iterating entries to preserve types
|
||||||
await checkBookingLimit({ key, limitingNumber, eventStartDate, eventId, returnBusyTimes })
|
const limitCalculations = ascendingLimitKeys.map((key) =>
|
||||||
);
|
checkBookingLimit({ key, limitingNumber: parsedBookingLimits[key], eventStartDate, eventId })
|
||||||
await Promise.all(limitCalculations)
|
);
|
||||||
.then((res) => {
|
|
||||||
if (returnBusyTimes) {
|
try {
|
||||||
return res;
|
return !!(await Promise.all(limitCalculations));
|
||||||
}
|
} catch (error) {
|
||||||
})
|
throw new HttpError({ message: getErrorFromUnknown(error).message, statusCode: 401 });
|
||||||
.catch((error) => {
|
|
||||||
throw new HttpError({ message: error.message, statusCode: 401 });
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkBookingLimit({
|
export async function checkBookingLimit({
|
||||||
|
@ -36,59 +33,39 @@ export async function checkBookingLimit({
|
||||||
eventId,
|
eventId,
|
||||||
key,
|
key,
|
||||||
limitingNumber,
|
limitingNumber,
|
||||||
returnBusyTimes = false,
|
|
||||||
}: {
|
}: {
|
||||||
eventStartDate: Date;
|
eventStartDate: Date;
|
||||||
eventId: number;
|
eventId: number;
|
||||||
key: string;
|
key: keyof IntervalLimit;
|
||||||
limitingNumber: number;
|
limitingNumber: number | undefined;
|
||||||
returnBusyTimes?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
{
|
{
|
||||||
const limitKey = key as keyof IntervalLimit;
|
if (!limitingNumber) return;
|
||||||
// Take PER_DAY and turn it into day and PER_WEEK into week etc.
|
|
||||||
const filter = limitKey.split("_")[1].toLocaleLowerCase() as "day" | "week" | "month" | "year"; // Have to cast here
|
|
||||||
const startDate = dayjs(eventStartDate).startOf(filter).toDate();
|
|
||||||
// this is parsed above with parseBookingLimit so we know it's safe.
|
|
||||||
|
|
||||||
const endDate = dayjs(startDate).endOf(filter).toDate();
|
const unit = intervalLimitKeyToUnit(key);
|
||||||
|
|
||||||
|
const startDate = dayjs(eventStartDate).startOf(unit).toDate();
|
||||||
|
const endDate = dayjs(eventStartDate).endOf(unit).toDate();
|
||||||
|
|
||||||
// This allows us to easily add it within dayjs
|
|
||||||
const bookingsInPeriod = await prisma.booking.count({
|
const bookingsInPeriod = await prisma.booking.count({
|
||||||
where: {
|
where: {
|
||||||
status: "ACCEPTED",
|
status: BookingStatus.ACCEPTED,
|
||||||
eventType: {
|
eventTypeId: eventId,
|
||||||
AND: [
|
// FIXME: bookings that overlap on one side will never be counted
|
||||||
{
|
startTime: {
|
||||||
id: eventId,
|
gte: startDate,
|
||||||
bookings: {
|
},
|
||||||
every: {
|
endTime: {
|
||||||
startTime: {
|
lte: endDate,
|
||||||
gte: startDate,
|
|
||||||
},
|
|
||||||
endTime: {
|
|
||||||
lte: endDate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (bookingsInPeriod >= limitingNumber) {
|
|
||||||
// This is used when getting availability
|
|
||||||
if (returnBusyTimes) {
|
|
||||||
return {
|
|
||||||
start: startDate,
|
|
||||||
end: endDate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError({
|
if (bookingsInPeriod < limitingNumber) return;
|
||||||
message: `booking_limit_reached`,
|
|
||||||
statusCode: 403,
|
throw new HttpError({
|
||||||
});
|
message: `booking_limit_reached`,
|
||||||
}
|
statusCode: 403,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
import { getErrorFromUnknown } from "../errors";
|
||||||
import { HttpError } from "../http-error";
|
import { HttpError } from "../http-error";
|
||||||
|
import { ascendingLimitKeys, intervalLimitKeyToUnit } from "../intervalLimit";
|
||||||
import { parseDurationLimit } from "../isDurationLimits";
|
import { parseDurationLimit } from "../isDurationLimits";
|
||||||
import { getTotalBookingDuration } from "./queries";
|
import { getTotalBookingDuration } from "./queries";
|
||||||
|
|
||||||
|
@ -11,20 +13,18 @@ export async function checkDurationLimits(
|
||||||
eventId: number
|
eventId: number
|
||||||
) {
|
) {
|
||||||
const parsedDurationLimits = parseDurationLimit(durationLimits);
|
const parsedDurationLimits = parseDurationLimit(durationLimits);
|
||||||
if (!parsedDurationLimits) {
|
if (!parsedDurationLimits) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const limitCalculations = Object.entries(parsedDurationLimits).map(
|
// not iterating entries to preserve types
|
||||||
async ([key, limitingNumber]) =>
|
const limitCalculations = ascendingLimitKeys.map((key) =>
|
||||||
await checkDurationLimit({ key, limitingNumber, eventStartDate, eventId })
|
checkDurationLimit({ key, limitingNumber: parsedDurationLimits[key], eventStartDate, eventId })
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(limitCalculations).catch((error) => {
|
try {
|
||||||
throw new HttpError({ message: error.message, statusCode: 401 });
|
return !!(await Promise.all(limitCalculations));
|
||||||
});
|
} catch (error) {
|
||||||
|
throw new HttpError({ message: getErrorFromUnknown(error).message, statusCode: 401 });
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkDurationLimit({
|
export async function checkDurationLimit({
|
||||||
|
@ -32,34 +32,27 @@ export async function checkDurationLimit({
|
||||||
eventId,
|
eventId,
|
||||||
key,
|
key,
|
||||||
limitingNumber,
|
limitingNumber,
|
||||||
returnBusyTimes = false,
|
|
||||||
}: {
|
}: {
|
||||||
eventStartDate: Date;
|
eventStartDate: Date;
|
||||||
eventId: number;
|
eventId: number;
|
||||||
key: string;
|
key: keyof IntervalLimit;
|
||||||
limitingNumber: number;
|
limitingNumber: number | undefined;
|
||||||
returnBusyTimes?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
{
|
{
|
||||||
// Take PER_DAY and turn it into day and PER_WEEK into week etc.
|
if (!limitingNumber) return;
|
||||||
const filter = key.split("_")[1].toLocaleLowerCase() as "day" | "week" | "month" | "year";
|
|
||||||
const startDate = dayjs(eventStartDate).startOf(filter).toDate();
|
const unit = intervalLimitKeyToUnit(key);
|
||||||
const endDate = dayjs(startDate).endOf(filter).toDate();
|
|
||||||
|
const startDate = dayjs(eventStartDate).startOf(unit).toDate();
|
||||||
|
const endDate = dayjs(eventStartDate).endOf(unit).toDate();
|
||||||
|
|
||||||
const totalBookingDuration = await getTotalBookingDuration({ eventId, startDate, endDate });
|
const totalBookingDuration = await getTotalBookingDuration({ eventId, startDate, endDate });
|
||||||
if (totalBookingDuration >= limitingNumber) {
|
|
||||||
// This is used when getting availability
|
|
||||||
if (returnBusyTimes) {
|
|
||||||
return {
|
|
||||||
start: startDate,
|
|
||||||
end: endDate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError({
|
if (totalBookingDuration < limitingNumber) return;
|
||||||
message: `duration_limit_reached`,
|
|
||||||
statusCode: 403,
|
throw new HttpError({
|
||||||
});
|
message: `duration_limit_reached`,
|
||||||
}
|
statusCode: 403,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,14 +10,15 @@ export const getTotalBookingDuration = async ({
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
}) => {
|
}) => {
|
||||||
// Aggregates the total booking time for a given event in a given time period
|
// Aggregates the total booking time for a given event in a given time period
|
||||||
const [totalBookingTime] = (await prisma.$queryRaw`
|
// FIXME: bookings that overlap on one side will never be counted
|
||||||
|
const [totalBookingTime] = await prisma.$queryRaw<[{ totalMinutes: number | null }]>`
|
||||||
SELECT SUM(EXTRACT(EPOCH FROM ("endTime" - "startTime")) / 60) as "totalMinutes"
|
SELECT SUM(EXTRACT(EPOCH FROM ("endTime" - "startTime")) / 60) as "totalMinutes"
|
||||||
FROM "Booking"
|
FROM "Booking"
|
||||||
WHERE "status" = 'accepted'
|
WHERE "status" = 'accepted'
|
||||||
AND "id" = ${eventId}
|
AND "eventTypeId" = ${eventId}
|
||||||
AND "startTime" >= ${startDate}
|
AND "startTime" >= ${startDate}
|
||||||
AND "endTime" <= ${endDate};
|
AND "endTime" <= ${endDate};
|
||||||
`) as { totalMinutes: number }[];
|
`;
|
||||||
|
|
||||||
return totalBookingTime.totalMinutes;
|
return totalBookingTime.totalMinutes ?? 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -99,7 +99,7 @@ export async function getTeamWithMembers(args: {
|
||||||
const where: Prisma.TeamFindFirstArgs["where"] = {};
|
const where: Prisma.TeamFindFirstArgs["where"] = {};
|
||||||
|
|
||||||
if (userId) where.members = { some: { userId } };
|
if (userId) where.members = { some: { userId } };
|
||||||
if (orgSlug) {
|
if (orgSlug && orgSlug !== slug) {
|
||||||
where.parent = getSlugOrRequestedSlug(orgSlug);
|
where.parent = getSlugOrRequestedSlug(orgSlug);
|
||||||
}
|
}
|
||||||
if (id) where.id = id;
|
if (id) where.id = id;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
const validationOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"];
|
import { ascendingLimitKeys } from "./intervalLimit";
|
||||||
|
|
||||||
export const validateIntervalLimitOrder = (input: IntervalLimit) => {
|
export const validateIntervalLimitOrder = (input: IntervalLimit) => {
|
||||||
// Sort limits by validationOrder
|
// Sort limits by validationOrder
|
||||||
const sorted = Object.entries(input)
|
const sorted = Object.entries(input)
|
||||||
|
@ -9,7 +10,7 @@ export const validateIntervalLimitOrder = (input: IntervalLimit) => {
|
||||||
})
|
})
|
||||||
.map(([key]) => key);
|
.map(([key]) => key);
|
||||||
|
|
||||||
const validationOrderWithoutMissing = validationOrderKeys.filter((key) => sorted.includes(key));
|
const validationOrderWithoutMissing = ascendingLimitKeys.filter((key) => sorted.includes(key));
|
||||||
|
|
||||||
return sorted.every((key, index) => validationOrderWithoutMissing[index] === key);
|
return sorted.every((key, index) => validationOrderWithoutMissing[index] === key);
|
||||||
};
|
};
|
||||||
|
|
|
@ -120,12 +120,9 @@ export interface RecurringEvent {
|
||||||
tzid?: string | undefined;
|
tzid?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IntervalLimit {
|
export type IntervalLimitUnit = "day" | "week" | "month" | "year";
|
||||||
PER_DAY?: number | undefined;
|
|
||||||
PER_WEEK?: number | undefined;
|
export type IntervalLimit = Partial<Record<`PER_${Uppercase<IntervalLimitUnit>}`, number | undefined>>;
|
||||||
PER_MONTH?: number | undefined;
|
|
||||||
PER_YEAR?: number | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AppsStatus = {
|
export type AppsStatus = {
|
||||||
appName: string;
|
appName: string;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const StepCard: React.FC<{ children: React.ReactNode }> = (props) => {
|
const StepCard: React.FC<{ children: React.ReactNode }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="border-subtle bg-default mt-10 rounded-md border p-4 dark:bg-black sm:p-8">
|
<div className="sm:border-subtle bg-default mt-10 border p-4 dark:bg-black sm:rounded-md sm:p-8">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,7 +101,7 @@ export function DataTable<TData, TValue>({
|
||||||
tableCTA={tableCTA}
|
tableCTA={tableCTA}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="rounded-md border"
|
className="border-subtle rounded-md border"
|
||||||
ref={tableContainerRef}
|
ref={tableContainerRef}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -10,7 +10,7 @@ export interface NavTabProps {
|
||||||
|
|
||||||
const HorizontalTabs = function ({ tabs, linkShallow, linkScroll, actions, ...props }: NavTabProps) {
|
const HorizontalTabs = function ({ tabs, linkShallow, linkScroll, actions, ...props }: NavTabProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 h-9 max-w-[calc(100%+40px)] lg:mb-5">
|
<div className="mb-4 h-9 max-w-full lg:mb-5">
|
||||||
<nav
|
<nav
|
||||||
className="no-scrollbar flex max-h-9 space-x-1 overflow-scroll rounded-md"
|
className="no-scrollbar flex max-h-9 space-x-1 overflow-scroll rounded-md"
|
||||||
aria-label="Tabs"
|
aria-label="Tabs"
|
||||||
|
|
|
@ -36,7 +36,10 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames("hover:bg-subtle data-[state=selected]:bg-subtle border-b", className)}
|
className={classNames(
|
||||||
|
"hover:bg-subtle data-[state=selected]:bg-subtle border-subtle border-b",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4026,6 +4026,12 @@ __metadata:
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@calcom/debugging@workspace:packages/debugging":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@calcom/debugging@workspace:packages/debugging"
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@calcom/discord@workspace:packages/app-store/discord":
|
"@calcom/discord@workspace:packages/app-store/discord":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@calcom/discord@workspace:packages/app-store/discord"
|
resolution: "@calcom/discord@workspace:packages/app-store/discord"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user