Compare commits

...
8 Commits
Author SHA1 Message Date
Peer Richelsen 1a7911e9e2 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-19 08:57:31 +02:00
Crowdin Bot cb09b5bc5f New Crowdin translations by Github Action 2023-04-19 06:47:15 +00:00
Efraín Rochín a99031bdd2 fix: [CAL-1490] Google Cal/Outlook not actively syncing/self hosted (#8305) 2023-04-19 08:46:17 +02:00
ada8a98362 chore: add database indexes (#8361)
Co-authored-by: Mythie <me@lucasjamessmith.me>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-18 16:50:42 -07:00
Yagiz Nizipli e176e81bae perf: improve bookings performance (#8362) 2023-04-18 16:48:53 -07:00
zomars b64dd66c2c Fixes bundle comments
https://github.com/hashicorp/nextjs-bundle-analysis/issues/52#issuecomment-1513913373
2023-04-18 16:40:06 -07:00
Crowdin Bot 7fd162601b New Crowdin translations by Github Action 2023-04-18 23:29:50 +00:00
c1f346c935 Cal 1099 availability section defined hour mismatch (#7416)
* CAL-1099 Add prop for availability in formMethods

* fix: show correct availabilty name

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* CAL - 1099 / fix 12 hr time format bug

* fix: remove useEffect and fix vertical tab item ui

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Fixes merge conflicts

* Update index.tsx

* More conflict fixes

* Update packages/ui/components/navigation/tabs/VerticalTabs.tsx

* Type fixes

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: zomars <zomars@me.com>
2023-04-18 23:29:02 +00:00
13 changed files with 498 additions and 142 deletions

No files matched your search

+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
- name: Analyze bundle - name: Analyze bundle
run: | run: |
cd apps/web cd apps/web
npx -p nextjs-bundle-analysis report npx -p nextjs-bundle-analysis@0.5.0 report
- name: Upload bundle - name: Upload bundle
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
@@ -76,7 +76,7 @@ jobs:
id: fc id: fc
with: with:
issue-number: ${{ github.event.number }} issue-number: ${{ github.event.number }}
body-includes: "<!-- __NEXTJS_BUNDLE -->" body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
- name: Create Comment - name: Create Comment
uses: peter-evans/create-or-update-comment@v1.4.4 uses: peter-evans/create-or-update-comment@v1.4.4
@@ -1,21 +1,23 @@
import { SchedulingType } from "@prisma/client"; import { SchedulingType } from "@prisma/client";
import type { FormValues, EventTypeSetup } from "pages/event-types/[type]"; import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select"; import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select"; import { components } from "react-select";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { NewScheduleButton } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday"; import { weekdayNames } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui"; import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon"; import { ExternalLink, Globe } from "@calcom/ui/components/icon";
type AvailabilityOption = { import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
export type AvailabilityOption = {
label: string; label: string;
value: number; value: number;
isDefault: boolean; isDefault: boolean;
@@ -62,40 +64,12 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
); );
}; };
const AvailabilitySelect = ({
className = "",
options,
value,
...props
}: {
className?: string;
name: string;
value: AvailabilityOption | undefined;
options: AvailabilityOption[];
isDisabled?: boolean;
onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void;
}) => {
const { t } = useLocale();
return (
<Select
placeholder={t("select")}
options={options}
isDisabled={props.isDisabled}
isSearchable={false}
onChange={props.onChange}
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
defaultValue={value}
components={{ Option, SingleValue }}
isMulti={false}
/>
);
};
const format = (date: Date, hour12: boolean) => const format = (date: Date, hour12: boolean) =>
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format( Intl.DateTimeFormat(undefined, {
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")) hour: "numeric",
); minute: "numeric",
hourCycle: hour12 ? "h12" : "h24",
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
const EventTypeScheduleDetails = ({ const EventTypeScheduleDetails = ({
isManagedEventType, isManagedEventType,
@@ -188,61 +162,57 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
); );
const { watch } = useFormContext<FormValues>(); const { watch } = useFormContext<FormValues>();
const watchSchedule = watch("schedule"); const watchSchedule = watch("schedule");
const formMethods = useFormContext<FormValues>();
const [options, setOptions] = useState<AvailabilityOption[]>([]);
const { data, isLoading } = trpc.viewer.availability.list.useQuery(); const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
onSuccess: ({ schedules }) => {
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
isManaged: false,
}));
if (!data?.schedules.length && !isLoading) // We are showing a managed event for a team admin, so adding the option to let members choose their schedule
return ( if (isManagedEventType) {
<EmptyScreen options.push({
Icon={Clock} value: 0,
headline={t("new_schedule_heading")} label: t("members_default_schedule"),
description={t("new_schedule_description")} isDefault: false,
buttonRaw={<NewScheduleButton fromEventType />} isManaged: false,
border={false} });
/> }
);
const schedules = data?.schedules || []; // We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (
isChildrenManagedEventType &&
watchSchedule &&
!schedules.find((schedule) => schedule.id === watchSchedule)
) {
options.push({
value: watchSchedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
const options = schedules.map((schedule) => ({ setOptions(options);
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
isManaged: false,
}));
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule const scheduleId = formMethods.getValues("schedule");
if (isManagedEventType) { const value = options.find((option) =>
options.push({ scheduleId
value: 0, ? option.value === scheduleId
label: t("members_default_schedule"), : option.value === schedules.find((schedule) => schedule.isDefault)?.id
isDefault: false, );
isManaged: false,
});
}
// We are showing a managed event for a member and team owner selected their own schedule, so adding formMethods.setValue("availability", value);
// the managed schedule option },
if ( });
isChildrenManagedEventType &&
watchSchedule &&
!schedules.find((schedule) => schedule.id === watchSchedule)
) {
options.push({
value: watchSchedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
const value = options.find((option) => const availabilityValue = formMethods.watch("availability");
watchSchedule
? option.value === watchSchedule
: isManagedEventType
? option.value === 0
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -251,27 +221,33 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
{t("availability")} {t("availability")}
{shouldLockIndicator("availability")} {shouldLockIndicator("availability")}
</label> </label>
<Controller {isLoading && <SelectSkeletonLoader />}
name="schedule" {!isLoading && (
render={({ field }) => ( <Controller
<> name="schedule"
<AvailabilitySelect render={({ field }) => {
value={value} return (
options={options} <Select
onBlur={field.onBlur} placeholder={t("select")}
isDisabled={shouldLockDisableProps("schedule").disabled} options={options}
name={field.name} isSearchable={false}
onChange={(selected) => { onChange={(selected) => {
field.onChange(selected?.value || null); field.onChange(selected?.value || null);
}} if (selected?.value) formMethods.setValue("availability", selected);
/> }}
</> className="block w-full min-w-0 flex-1 rounded-sm text-sm"
)} value={availabilityValue}
/> components={{ Option, SingleValue }}
isMulti={false}
/>
);
}}
/>
)}
</div> </div>
{value?.value !== 0 ? ( {availabilityValue?.value !== 0 ? (
<EventTypeScheduleDetails <EventTypeScheduleDetails
selectedScheduleValue={value} selectedScheduleValue={availabilityValue}
isManagedEventType={isManagedEventType || isChildrenManagedEventType} isManagedEventType={isManagedEventType || isChildrenManagedEventType}
/> />
) : ( ) : (
@@ -51,6 +51,7 @@ import {
} from "@calcom/ui/components/icon"; } from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed"; import { EmbedButton, EmbedDialog } from "@components/Embed";
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
@@ -63,6 +64,7 @@ type Props = {
enabledWorkflowsNumber: number; enabledWorkflowsNumber: number;
formMethods: UseFormReturn<FormValues>; formMethods: UseFormReturn<FormValues>;
isUpdateMutationLoading?: boolean; isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption;
}; };
function getNavigation(props: { function getNavigation(props: {
@@ -71,8 +73,10 @@ function getNavigation(props: {
enabledAppsNumber: number; enabledAppsNumber: number;
enabledWorkflowsNumber: number; enabledWorkflowsNumber: number;
installedAppsNumber: number; installedAppsNumber: number;
availability: AvailabilityOption | undefined;
}) { }) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props; const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
props;
const duration = const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length; eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
@@ -128,6 +132,7 @@ function EventTypeSingleLayout({
enabledWorkflowsNumber, enabledWorkflowsNumber,
isUpdateMutationLoading, isUpdateMutationLoading,
formMethods, formMethods,
availability,
}: Props) { }: Props) {
const utils = trpc.useContext(); const utils = trpc.useContext();
const { t } = useLocale(); const { t } = useLocale();
@@ -171,6 +176,7 @@ function EventTypeSingleLayout({
enabledAppsNumber, enabledAppsNumber,
installedAppsNumber, installedAppsNumber,
enabledWorkflowsNumber, enabledWorkflowsNumber,
availability,
}); });
navigation.splice(1, 0, { navigation.splice(1, 0, {
name: "availability", name: "availability",
@@ -214,7 +220,7 @@ function EventTypeSingleLayout({
}); });
} }
return navigation; return navigation;
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]); }, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${ const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
eventType.slug eventType.slug
@@ -374,6 +380,8 @@ function EventTypeSingleLayout({
tabs={EventTypeTabs} tabs={EventTypeTabs}
sticky sticky
linkProps={{ shallow: true }} linkProps={{ shallow: true }}
itemClassname="items-start"
iconClassName="md:mt-px"
/> />
</div> </div>
<div className="p-2 md:mx-0 md:p-0 xl:hidden"> <div className="p-2 md:mx-0 md:p-0 xl:hidden">
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.8.3", "version": "2.8.4",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
+11 -3
View File
@@ -1,3 +1,4 @@
import type { NextPageContext } from "next";
import type { DocumentContext, DocumentProps } from "next/document"; import type { DocumentContext, DocumentProps } from "next/document";
import Document, { Head, Html, Main, NextScript } from "next/document"; import Document, { Head, Html, Main, NextScript } from "next/document";
import { z } from "zod"; import { z } from "zod";
@@ -7,15 +8,22 @@ import { getDirFromLang } from "@calcom/lib/i18n";
import { csp } from "@lib/csp"; import { csp } from "@lib/csp";
type Props = Record<string, unknown> & DocumentProps; type Props = Record<string, unknown> & DocumentProps;
function setHeader(ctx: NextPageContext, name: string, value: string) {
try {
ctx.res?.setHeader(name, value);
} catch (e) {
// Getting "Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client" when revalidate calendar chache
console.log(`Error setting header ${name}=${value} for ${ctx.asPath || "unknown asPath"}`, e);
}
}
class MyDocument extends Document<Props> { class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) { static async getInitialProps(ctx: DocumentContext) {
const { nonce } = csp(ctx.req || null, ctx.res || null); const { nonce } = csp(ctx.req || null, ctx.res || null);
if (!process.env.CSP_POLICY) { if (!process.env.CSP_POLICY) {
ctx.res?.setHeader("x-csp", "not-opted-in"); setHeader(ctx, "x-csp", "not-opted-in");
} else if (!ctx.res?.getHeader("x-csp")) { } else if (!ctx.res?.getHeader("x-csp")) {
// If x-csp not set by gSSP, then it's initialPropsOnly // If x-csp not set by gSSP, then it's initialPropsOnly
ctx.res?.setHeader("x-csp", "initialPropsOnly"); setHeader(ctx, "x-csp", "initialPropsOnly");
} }
const asPath = ctx.asPath || ""; const asPath = ctx.asPath || "";
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only // Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
+67 -11
View File
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { PeriodType } from "@prisma/client"; import type { PeriodType, SchedulingType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -18,10 +17,10 @@ import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry"; import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
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 type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar"; import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
@@ -34,6 +33,7 @@ import PageWrapper from "@components/PageWrapper";
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings // These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab"; import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
import { EventAppsTab } from "@components/eventtype/EventAppsTab"; import { EventAppsTab } from "@components/eventtype/EventAppsTab";
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab"; import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab"; import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab"; import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
@@ -94,6 +94,7 @@ export type FormValues = {
children: ChildrenEventType[]; children: ChildrenEventType[];
hosts: { userId: number; isFixed: boolean }[]; hosts: { userId: number; isFixed: boolean }[];
bookingFields: z.infer<typeof eventTypeBookingFields>; bookingFields: z.infer<typeof eventTypeBookingFields>;
availability?: AvailabilityOption;
}; };
export type CustomInputParsed = typeof customInputSchema._output; export type CustomInputParsed = typeof customInputSchema._output;
@@ -262,6 +263,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}, [defaultValues]); }, [defaultValues]);
const appsMetadata = formMethods.getValues("metadata")?.apps; const appsMetadata = formMethods.getValues("metadata")?.apps;
const availability = formMethods.watch("availability");
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0; const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
let numberOfActiveApps = 0; let numberOfActiveApps = 0;
@@ -378,6 +380,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
enabledWorkflowsNumber={eventType.workflows.length} enabledWorkflowsNumber={eventType.workflows.length}
eventType={eventType} eventType={eventType}
team={team} team={team}
availability={availability}
isUpdateMutationLoading={updateMutation.isLoading} isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods} formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"} disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
@@ -385,13 +388,66 @@ const EventTypePage = (props: EventTypeSetupProps) => {
<Form <Form
form={formMethods} form={formMethods}
id="event-type-form" id="event-type-form"
handleSubmit={async (values: FormValues) => { handleSubmit={async (values) => {
if (!values.children.length) return handleSubmit(values); const {
const existingSlugEventTypes = values.children.filter((ch) => periodDates,
ch.owner.eventTypeSlugs.includes(slug) periodCountCalendarDays,
); beforeBufferTime,
if (!existingSlugEventTypes.length) return handleSubmit(values); afterBufferTime,
setSlugExistsChildrenDialogOpen(existingSlugEventTypes); seatsPerTimeSlot,
seatsShowAttendees,
bookingLimits,
durationLimits,
recurringEvent,
locations,
metadata,
customInputs,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
minimumBookingNoticeInDurationType,
availability,
...input
} = values;
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (durationLimits) {
const isValid = validateIntervalLimitOrder(durationLimits);
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
updateMutation.mutate({
...input,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
periodEndDate: periodDates.endDate,
periodCountCalendarDays: periodCountCalendarDays === "1",
id: eventType.id,
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
bookingLimits,
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
customInputs,
});
}}> }}>
<div ref={animationParentRef}>{tabMap[tabName]}</div> <div ref={animationParentRef}>{tabMap[tabName]}</div>
</Form> </Form>
@@ -1150,11 +1150,13 @@
"current_username": "Nom d'utilisateur actuel", "current_username": "Nom d'utilisateur actuel",
"example_1": "Exemple 1", "example_1": "Exemple 1",
"example_2": "Exemple 2", "example_2": "Exemple 2",
"booking_question_identifier": "Identifiant de la question de réservation",
"company_size": "Taille de l'entreprise", "company_size": "Taille de l'entreprise",
"what_help_needed": "Avez-vous besoin d'aide ?", "what_help_needed": "Avez-vous besoin d'aide ?",
"variable_format": "Format des variables", "variable_format": "Format des variables",
"webhook_subscriber_url_reserved": "Le lien de l'abonné webhook est déjà défini", "webhook_subscriber_url_reserved": "Le lien de l'abonné webhook est déjà défini",
"custom_input_as_variable_info": "Ignorer tous les caractères spéciaux du libellé de champ supplémentaire (utiliser uniquement des lettres et des chiffres), utiliser des majuscules pour toutes les lettres et remplacer les espaces par des tirets bas.", "custom_input_as_variable_info": "Ignorer tous les caractères spéciaux du libellé de champ supplémentaire (utiliser uniquement des lettres et des chiffres), utiliser des majuscules pour toutes les lettres et remplacer les espaces par des tirets bas.",
"using_booking_questions_as_variables": "Comment puis-je utiliser les questions de réservation comme variables ?",
"download_desktop_app": "Télécharger l'application de bureau", "download_desktop_app": "Télécharger l'application de bureau",
"set_ping_link": "Définir le lien Ping", "set_ping_link": "Définir le lien Ping",
"rate_limit_exceeded": "Limite dépassée", "rate_limit_exceeded": "Limite dépassée",
@@ -1380,6 +1382,7 @@
"add_limit": "Ajouter une limite", "add_limit": "Ajouter une limite",
"team_name_required": "Nom d'équipe requis", "team_name_required": "Nom d'équipe requis",
"show_attendees": "Partagez les informations des participants entre les invités", "show_attendees": "Partagez les informations des participants entre les invités",
"how_booking_questions_as_variables": "Comment utiliser les questions de réservation comme variables ?",
"format": "Format", "format": "Format",
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.", "uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
"replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.", "replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.",
@@ -1394,6 +1397,7 @@
"billing_help_title": "Besoin d'autre chose ?", "billing_help_title": "Besoin d'autre chose ?",
"billing_help_description": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.", "billing_help_description": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.",
"billing_help_cta": "Contacter l'assistance", "billing_help_cta": "Contacter l'assistance",
"ignore_special_characters_booking_questions": "Ignorez les caractères spéciaux dans l'identifiant de votre question de réservation, utilisez uniquement des lettres et des chiffres.",
"retry": "Réessayer", "retry": "Réessayer",
"fetching_calendars_error": "Un problème est survenu lors de la récupération de vos calendriers. Veuillez <1>réessayer</1> ou contacter le service client.", "fetching_calendars_error": "Un problème est survenu lors de la récupération de vos calendriers. Veuillez <1>réessayer</1> ou contacter le service client.",
"calendar_connection_fail": "Échec de la connexion au calendrier", "calendar_connection_fail": "Échec de la connexion au calendrier",
@@ -1702,6 +1706,13 @@
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?", "can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
"verify": "Vérifier", "verify": "Vérifier",
"timezone_variable": "Fuseau horaire", "timezone_variable": "Fuseau horaire",
"timezone_info": "Le fuseau horaire du destinataire",
"event_end_time_variable": "Heure de fin de l'événement",
"event_end_time_info": "L'heure de fin de l'événement",
"cancel_url_variable": "Lien d'annulation",
"cancel_url_info": "Le lien d'annulation de la réservation",
"reschedule_url_variable": "Lien de replanification",
"reschedule_url_info": "Le lien de replanification de la réservation",
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement", "invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
"select_all": "Tout sélectionner", "select_all": "Tout sélectionner",
"default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants", "default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants",
@@ -1766,6 +1777,7 @@
"collect_no_show_fee": "Percevoir les frais d'absence", "collect_no_show_fee": "Percevoir les frais d'absence",
"no_show_fee_charged": "Frais d'absence facturés", "no_show_fee_charged": "Frais d'absence facturés",
"insights": "Statistiques", "insights": "Statistiques",
"testing_workflow_info_message": "Lors du test de ce workflow, sachez que les e-mails et les SMS ne peuvent être programmés qu'au moins 1 heure à l'avance",
"insights_no_data_found_for_filter": "Aucune donnée trouvée pour le filtre ou les dates sélectionnés.", "insights_no_data_found_for_filter": "Aucune donnée trouvée pour le filtre ou les dates sélectionnés.",
"acknowledge_booking_no_show_fee": "Je reconnais que si je ne participe pas à cet événement, des frais d'absence de {{amount, currency}} seront appliqués à ma carte.", "acknowledge_booking_no_show_fee": "Je reconnais que si je ne participe pas à cet événement, des frais d'absence de {{amount, currency}} seront appliqués à ma carte.",
"card_details": "Détails de la carte", "card_details": "Détails de la carte",
@@ -111,7 +111,7 @@
"hidden_team_owner_message": "Bạn cần tài khoản Pro để sử dụng nhóm, bạn sẽ bị ẩn cho đến khi bạn thanh toán.", "hidden_team_owner_message": "Bạn cần tài khoản Pro để sử dụng nhóm, bạn sẽ bị ẩn cho đến khi bạn thanh toán.",
"link_expires": "p.s. Link hết hạn trong vòng {{expiresIn}} tiếng.", "link_expires": "p.s. Link hết hạn trong vòng {{expiresIn}} tiếng.",
"upgrade_to_per_seat": "Nâng cấp từng tài khoản", "upgrade_to_per_seat": "Nâng cấp từng tài khoản",
"seat_options_doesnt_support_confirmation": "Tuỳ chọn chỗ ngồi không hỗ trợ yêu cầu xác nhận", "seat_options_doesnt_support_confirmation": "Tuỳ chọn chỗ ngồi không thể chọn cho yêu cầu xác nhận đặt lịch",
"team_upgrade_seats_details": "Trong tổng {{memberCount}} thành viên của nhóm bạn, {{unpaidCount}} thành viên chưa được thanh toán. Với {{seatPrice}}$/tháng mỗi thành viên, tổng chi phí cho nhóm bạn là {{totalCost}}$/tháng.", "team_upgrade_seats_details": "Trong tổng {{memberCount}} thành viên của nhóm bạn, {{unpaidCount}} thành viên chưa được thanh toán. Với {{seatPrice}}$/tháng mỗi thành viên, tổng chi phí cho nhóm bạn là {{totalCost}}$/tháng.",
"team_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói kế hoạch mới dành cho nhóm. Chúng tôi để ý thấy nhóm \"{{teamName}}\" của bạn cần được nâng cấp.", "team_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói kế hoạch mới dành cho nhóm. Chúng tôi để ý thấy nhóm \"{{teamName}}\" của bạn cần được nâng cấp.",
"team_upgrade_banner_action": "Nâng cấp tại đây", "team_upgrade_banner_action": "Nâng cấp tại đây",
@@ -0,0 +1,182 @@
-- CreateIndex
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
-- CreateIndex
CREATE INDEX "Account_type_idx" ON "Account"("type");
-- CreateIndex
CREATE INDEX "ApiKey_userId_idx" ON "ApiKey"("userId");
-- CreateIndex
CREATE INDEX "App_enabled_idx" ON "App"("enabled");
-- CreateIndex
CREATE INDEX "App_RoutingForms_Form_userId_idx" ON "App_RoutingForms_Form"("userId");
-- CreateIndex
CREATE INDEX "App_RoutingForms_Form_disabled_idx" ON "App_RoutingForms_Form"("disabled");
-- CreateIndex
CREATE INDEX "App_RoutingForms_FormResponse_formFillerId_idx" ON "App_RoutingForms_FormResponse"("formFillerId");
-- CreateIndex
CREATE INDEX "App_RoutingForms_FormResponse_formId_idx" ON "App_RoutingForms_FormResponse"("formId");
-- CreateIndex
CREATE INDEX "Attendee_email_idx" ON "Attendee"("email");
-- CreateIndex
CREATE INDEX "Attendee_bookingId_idx" ON "Attendee"("bookingId");
-- CreateIndex
CREATE INDEX "Availability_userId_idx" ON "Availability"("userId");
-- CreateIndex
CREATE INDEX "Booking_eventTypeId_idx" ON "Booking"("eventTypeId");
-- CreateIndex
CREATE INDEX "Booking_userId_idx" ON "Booking"("userId");
-- CreateIndex
CREATE INDEX "Booking_destinationCalendarId_idx" ON "Booking"("destinationCalendarId");
-- CreateIndex
CREATE INDEX "Booking_recurringEventId_idx" ON "Booking"("recurringEventId");
-- CreateIndex
CREATE INDEX "Booking_uid_idx" ON "Booking"("uid");
-- CreateIndex
CREATE INDEX "Booking_status_idx" ON "Booking"("status");
-- CreateIndex
CREATE INDEX "BookingReference_bookingId_idx" ON "BookingReference"("bookingId");
-- CreateIndex
CREATE INDEX "BookingReference_credentialId_idx" ON "BookingReference"("credentialId");
-- CreateIndex
CREATE INDEX "BookingReference_type_idx" ON "BookingReference"("type");
-- CreateIndex
CREATE INDEX "BookingReference_uid_idx" ON "BookingReference"("uid");
-- CreateIndex
CREATE INDEX "BookingSeat_bookingId_idx" ON "BookingSeat"("bookingId");
-- CreateIndex
CREATE INDEX "BookingSeat_attendeeId_idx" ON "BookingSeat"("attendeeId");
-- CreateIndex
CREATE INDEX "Credential_userId_idx" ON "Credential"("userId");
-- CreateIndex
CREATE INDEX "Credential_appId_idx" ON "Credential"("appId");
-- CreateIndex
CREATE INDEX "DestinationCalendar_userId_idx" ON "DestinationCalendar"("userId");
-- CreateIndex
CREATE INDEX "DestinationCalendar_eventTypeId_idx" ON "DestinationCalendar"("eventTypeId");
-- CreateIndex
CREATE INDEX "DestinationCalendar_credentialId_idx" ON "DestinationCalendar"("credentialId");
-- CreateIndex
CREATE INDEX "EventType_userId_idx" ON "EventType"("userId");
-- CreateIndex
CREATE INDEX "EventType_teamId_idx" ON "EventType"("teamId");
-- CreateIndex
CREATE INDEX "EventTypeCustomInput_eventTypeId_idx" ON "EventTypeCustomInput"("eventTypeId");
-- CreateIndex
CREATE INDEX "Feature_enabled_idx" ON "Feature"("enabled");
-- CreateIndex
CREATE INDEX "Feature_stale_idx" ON "Feature"("stale");
-- CreateIndex
CREATE INDEX "Feedback_userId_idx" ON "Feedback"("userId");
-- CreateIndex
CREATE INDEX "Feedback_rating_idx" ON "Feedback"("rating");
-- CreateIndex
CREATE INDEX "Host_userId_idx" ON "Host"("userId");
-- CreateIndex
CREATE INDEX "Host_eventTypeId_idx" ON "Host"("eventTypeId");
-- CreateIndex
CREATE INDEX "Membership_teamId_idx" ON "Membership"("teamId");
-- CreateIndex
CREATE INDEX "Membership_userId_idx" ON "Membership"("userId");
-- CreateIndex
CREATE INDEX "Payment_bookingId_idx" ON "Payment"("bookingId");
-- CreateIndex
CREATE INDEX "Payment_externalId_idx" ON "Payment"("externalId");
-- CreateIndex
CREATE INDEX "ReminderMail_referenceId_idx" ON "ReminderMail"("referenceId");
-- CreateIndex
CREATE INDEX "ReminderMail_reminderType_idx" ON "ReminderMail"("reminderType");
-- CreateIndex
CREATE INDEX "SelectedCalendar_userId_idx" ON "SelectedCalendar"("userId");
-- CreateIndex
CREATE INDEX "SelectedCalendar_integration_idx" ON "SelectedCalendar"("integration");
-- CreateIndex
CREATE INDEX "SelectedCalendar_externalId_idx" ON "SelectedCalendar"("externalId");
-- CreateIndex
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- CreateIndex
CREATE INDEX "VerificationToken_token_idx" ON "VerificationToken"("token");
-- CreateIndex
CREATE INDEX "VerifiedNumber_userId_idx" ON "VerifiedNumber"("userId");
-- CreateIndex
CREATE INDEX "VerifiedNumber_teamId_idx" ON "VerifiedNumber"("teamId");
-- CreateIndex
CREATE INDEX "Workflow_userId_idx" ON "Workflow"("userId");
-- CreateIndex
CREATE INDEX "Workflow_teamId_idx" ON "Workflow"("teamId");
-- CreateIndex
CREATE INDEX "WorkflowReminder_bookingUid_idx" ON "WorkflowReminder"("bookingUid");
-- CreateIndex
CREATE INDEX "WorkflowReminder_workflowStepId_idx" ON "WorkflowReminder"("workflowStepId");
-- CreateIndex
CREATE INDEX "WorkflowStep_workflowId_idx" ON "WorkflowStep"("workflowId");
-- CreateIndex
CREATE INDEX "WorkflowsOnEventTypes_workflowId_idx" ON "WorkflowsOnEventTypes"("workflowId");
-- CreateIndex
CREATE INDEX "WorkflowsOnEventTypes_eventTypeId_idx" ON "WorkflowsOnEventTypes"("eventTypeId");
-- CreateIndex
CREATE INDEX "users_email_idx" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_emailVerified_idx" ON "users"("emailVerified");
-- CreateIndex
CREATE INDEX "users_identityProvider_idx" ON "users"("identityProvider");
-- CreateIndex
CREATE INDEX "users_identityProviderId_idx" ON "users"("identityProviderId");
+81
View File
@@ -38,6 +38,8 @@ model Host {
isFixed Boolean @default(false) isFixed Boolean @default(false)
@@id([userId, eventTypeId]) @@id([userId, eventTypeId])
@@index([userId])
@@index([eventTypeId])
} }
model EventType { model EventType {
@@ -108,6 +110,8 @@ model EventType {
@@unique([userId, slug]) @@unique([userId, slug])
@@unique([teamId, slug]) @@unique([teamId, slug])
@@unique([userId, parentId]) @@unique([userId, parentId])
@@index([userId])
@@index([teamId])
} }
model Credential { model Credential {
@@ -122,6 +126,9 @@ model Credential {
appId String? appId String?
destinationCalendars DestinationCalendar[] destinationCalendars DestinationCalendar[]
invalid Boolean? @default(false) invalid Boolean? @default(false)
@@index([userId])
@@index([appId])
} }
enum IdentityProvider { enum IdentityProvider {
@@ -141,6 +148,10 @@ model DestinationCalendar {
eventTypeId Int? @unique eventTypeId Int? @unique
credentialId Int? credentialId Int?
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade) credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([eventTypeId])
@@index([credentialId])
} }
enum UserPermissionRole { enum UserPermissionRole {
@@ -210,6 +221,10 @@ model User {
verifiedNumbers VerifiedNumber[] verifiedNumbers VerifiedNumber[]
hosts Host[] hosts Host[]
@@index([email])
@@index([emailVerified])
@@index([identityProvider])
@@index([identityProviderId])
@@map(name: "users") @@map(name: "users")
} }
@@ -251,6 +266,8 @@ model Membership {
disableImpersonation Boolean @default(false) disableImpersonation Boolean @default(false)
@@id([userId, teamId]) @@id([userId, teamId])
@@index([teamId])
@@index([userId])
} }
model VerificationToken { model VerificationToken {
@@ -262,6 +279,7 @@ model VerificationToken {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@unique([identifier, token]) @@unique([identifier, token])
@@index([token])
} }
model BookingReference { model BookingReference {
@@ -278,6 +296,11 @@ model BookingReference {
externalCalendarId String? externalCalendarId String?
deleted Boolean? deleted Boolean?
credentialId Int? credentialId Int?
@@index([bookingId])
@@index([credentialId])
@@index([type])
@@index([uid])
} }
model Attendee { model Attendee {
@@ -289,6 +312,9 @@ model Attendee {
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade) booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
bookingId Int? bookingId Int?
bookingSeat BookingSeat? bookingSeat BookingSeat?
@@index([email])
@@index([bookingId])
} }
enum BookingStatus { enum BookingStatus {
@@ -336,6 +362,13 @@ model Booking {
/// @zod.custom(imports.bookingMetadataSchema) /// @zod.custom(imports.bookingMetadataSchema)
metadata Json? metadata Json?
isRecorded Boolean @default(false) isRecorded Boolean @default(false)
@@index([eventTypeId])
@@index([userId])
@@index([destinationCalendarId])
@@index([recurringEventId])
@@index([uid])
@@index([status])
} }
model Schedule { model Schedule {
@@ -363,6 +396,7 @@ model Availability {
Schedule Schedule? @relation(fields: [scheduleId], references: [id]) Schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int? scheduleId Int?
@@index([userId])
@@index([eventTypeId]) @@index([eventTypeId])
@@index([scheduleId]) @@index([scheduleId])
} }
@@ -374,6 +408,9 @@ model SelectedCalendar {
externalId String externalId String
@@id([userId, integration, externalId]) @@id([userId, integration, externalId])
@@index([userId])
@@index([integration])
@@index([externalId])
} }
enum EventTypeCustomInputType { enum EventTypeCustomInputType {
@@ -395,6 +432,8 @@ model EventTypeCustomInput {
options Json? options Json?
required Boolean required Boolean
placeholder String @default("") placeholder String @default("")
@@index([eventTypeId])
} }
model ResetPasswordRequest { model ResetPasswordRequest {
@@ -415,6 +454,9 @@ model ReminderMail {
reminderType ReminderType reminderType ReminderType
elapsedMinutes Int elapsedMinutes Int
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@index([referenceId])
@@index([reminderType])
} }
model Payment { model Payment {
@@ -432,6 +474,9 @@ model Payment {
data Json data Json
externalId String @unique externalId String @unique
paymentOption PaymentOption? @default(ON_BOOKING) paymentOption PaymentOption? @default(ON_BOOKING)
@@index([bookingId])
@@index([externalId])
} }
enum PaymentOption { enum PaymentOption {
@@ -486,6 +531,8 @@ model ApiKey {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
appId String? appId String?
@@index([userId])
} }
model HashedLink { model HashedLink {
@@ -512,6 +559,8 @@ model Account {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId]) @@unique([provider, providerAccountId])
@@index([userId])
@@index([type])
} }
model Session { model Session {
@@ -520,6 +569,8 @@ model Session {
userId Int userId Int
expires DateTime expires DateTime
user User? @relation(fields: [userId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
} }
enum AppCategories { enum AppCategories {
@@ -549,6 +600,8 @@ model App {
Webhook Webhook[] Webhook Webhook[]
ApiKey ApiKey[] ApiKey ApiKey[]
enabled Boolean @default(false) enabled Boolean @default(false)
@@index([enabled])
} }
model App_RoutingForms_Form { model App_RoutingForms_Form {
@@ -565,6 +618,9 @@ model App_RoutingForms_Form {
disabled Boolean @default(false) disabled Boolean @default(false)
/// @zod.custom(imports.RoutingFormSettings) /// @zod.custom(imports.RoutingFormSettings)
settings Json? settings Json?
@@index([userId])
@@index([disabled])
} }
model App_RoutingForms_FormResponse { model App_RoutingForms_FormResponse {
@@ -576,6 +632,8 @@ model App_RoutingForms_FormResponse {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@unique([formFillerId, formId]) @@unique([formFillerId, formId])
@@index([formFillerId])
@@index([formId])
} }
model Feedback { model Feedback {
@@ -585,6 +643,9 @@ model Feedback {
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
rating String rating String
comment String? comment String?
@@index([userId])
@@index([rating])
} }
enum WorkflowTriggerEvents { enum WorkflowTriggerEvents {
@@ -617,6 +678,8 @@ model WorkflowStep {
numberRequired Boolean? numberRequired Boolean?
sender String? sender String?
numberVerificationPending Boolean @default(true) numberVerificationPending Boolean @default(true)
@@index([workflowId])
} }
model Workflow { model Workflow {
@@ -631,6 +694,9 @@ model Workflow {
time Int? time Int?
timeUnit TimeUnit? timeUnit TimeUnit?
steps WorkflowStep[] steps WorkflowStep[]
@@index([userId])
@@index([teamId])
} }
model WorkflowsOnEventTypes { model WorkflowsOnEventTypes {
@@ -639,6 +705,9 @@ model WorkflowsOnEventTypes {
workflowId Int workflowId Int
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int eventTypeId Int
@@index([workflowId])
@@index([eventTypeId])
} }
model Deployment { model Deployment {
@@ -668,6 +737,9 @@ model WorkflowReminder {
workflowStepId Int? workflowStepId Int?
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id]) workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id])
cancelled Boolean? cancelled Boolean?
@@index([bookingUid])
@@index([workflowStepId])
} }
enum WorkflowTemplates { enum WorkflowTemplates {
@@ -688,6 +760,9 @@ model BookingSeat {
attendeeId Int @unique attendeeId Int @unique
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade) attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
data Json? data Json?
@@index([bookingId])
@@index([attendeeId])
} }
model VerifiedNumber { model VerifiedNumber {
@@ -697,6 +772,9 @@ model VerifiedNumber {
teamId Int? teamId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
phoneNumber String phoneNumber String
@@index([userId])
@@index([teamId])
} }
model Feature { model Feature {
@@ -714,6 +792,9 @@ model Feature {
createdAt DateTime? @default(now()) createdAt DateTime? @default(now())
updatedAt DateTime? @default(now()) @updatedAt updatedAt DateTime? @default(now()) @updatedAt
updatedBy Int? updatedBy Int?
@@index([enabled])
@@index([stale])
} }
enum FeatureType { enum FeatureType {
@@ -780,10 +780,11 @@ export const bookingsRouter = router({
}, },
}); });
const authorized = async () => { if (booking.userId !== user.id && booking.eventTypeId) {
// Only query database when it is explicitly required.
const eventType = await prisma.eventType.findFirst({ const eventType = await prisma.eventType.findFirst({
where: { where: {
id: booking.eventTypeId || undefined, id: booking.eventTypeId,
schedulingType: SchedulingType.COLLECTIVE, schedulingType: SchedulingType.COLLECTIVE,
}, },
select: { select: {
@@ -791,15 +792,16 @@ export const bookingsRouter = router({
}, },
}); });
return eventType && eventType.users.find((user) => booking.userId === user.id); if (eventType && !eventType.users.find((user) => booking.userId === user.id)) {
}; throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
}
if (booking.userId !== user.id && !(await authorized())) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
} }
const isConfirmed = booking.status === BookingStatus.ACCEPTED; // Do not move this before authorization check.
if (isConfirmed) throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" }); // This is done to avoid exposing extra information to the requester.
if (booking.status === BookingStatus.ACCEPTED) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
}
// If booking requires payment and is not paid, we don't allow confirmation // If booking requires payment and is not paid, we don't allow confirmation
if (confirmed && booking.payment.length > 0 && !booking.paid) { if (confirmed && booking.payment.length > 0 && !booking.paid) {
@@ -815,14 +817,22 @@ export const bookingsRouter = router({
return { message: "Booking confirmed", status: BookingStatus.ACCEPTED }; return { message: "Booking confirmed", status: BookingStatus.ACCEPTED };
} }
// Cache translations to avoid requesting multiple times.
const translations = new Map();
const attendeesListPromises = booking.attendees.map(async (attendee) => { const attendeesListPromises = booking.attendees.map(async (attendee) => {
const locale = attendee.locale ?? "en";
let translate = translations.get(locale);
if (!translate) {
translate = await getTranslation(locale, "common");
translations.set(locale, translate);
}
return { return {
name: attendee.name, name: attendee.name,
email: attendee.email, email: attendee.email,
timeZone: attendee.timeZone, timeZone: attendee.timeZone,
language: { language: {
translate: await getTranslation(attendee.locale ?? "en", "common"), translate,
locale: attendee.locale ?? "en", locale,
}, },
}; };
}); });
@@ -864,6 +874,9 @@ export const bookingsRouter = router({
recurringEventId, recurringEventId,
id: booking.id, id: booking.id,
}, },
select: {
id: true,
},
})) }))
) { ) {
// FIXME: It might be best to retrieve recurringEventId from the booking itself. // FIXME: It might be best to retrieve recurringEventId from the booking itself.
@@ -25,9 +25,10 @@ export type VerticalTabItemProps = {
isExternalLink?: boolean; isExternalLink?: boolean;
linkProps?: Omit<ComponentProps<typeof Link>, "href">; linkProps?: Omit<ComponentProps<typeof Link>, "href">;
avatar?: string; avatar?: string;
iconClassName?: string;
}; };
const VerticalTabItem = function ({ const VerticalTabItem = ({
name, name,
href, href,
info, info,
@@ -35,11 +36,10 @@ const VerticalTabItem = function ({
disableChevron, disableChevron,
linkProps, linkProps,
...props ...props
}: VerticalTabItemProps) { }: VerticalTabItemProps) => {
const { t } = useLocale(); const { t } = useLocale();
const { asPath } = useRouter(); const { asPath } = useRouter();
const isCurrent = asPath.startsWith(href); const isCurrent = asPath.startsWith(href);
return ( return (
<Fragment key={name}> <Fragment key={name}>
{!props.hidden && ( {!props.hidden && (
@@ -60,7 +60,12 @@ const VerticalTabItem = function ({
data-testid={`vertical-tab-${name}`} data-testid={`vertical-tab-${name}`}
aria-current={isCurrent ? "page" : undefined}> aria-current={isCurrent ? "page" : undefined}>
{props.icon && ( {props.icon && (
<props.icon className="h-[16px] w-[16px] stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0" /> <props.icon
className={classNames(
"h-[16px] w-[16px] stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0",
props.iconClassName
)}
/>
)} )}
<div className="h-fit"> <div className="h-fit">
<span className="flex items-center space-x-2 rtl:space-x-reverse"> <span className="flex items-center space-x-2 rtl:space-x-reverse">
@@ -12,9 +12,18 @@ export interface NavTabProps {
sticky?: boolean; sticky?: boolean;
linkProps?: VerticalTabItemProps["linkProps"]; linkProps?: VerticalTabItemProps["linkProps"];
itemClassname?: string; itemClassname?: string;
iconClassName?: string;
} }
const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassname, ...props }: NavTabProps) { const NavTabs = function ({
tabs,
className = "",
sticky,
linkProps,
itemClassname,
iconClassName,
...props
}: NavTabProps) {
return ( return (
<nav <nav
className={classNames( className={classNames(
@@ -27,7 +36,13 @@ const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassna
{sticky && <div className="pt-6" />} {sticky && <div className="pt-6" />}
{props.children} {props.children}
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={itemClassname} /> <VerticalTabItem
{...tab}
key={idx}
linkProps={linkProps}
className={itemClassname}
iconClassName={iconClassName}
/>
))} ))}
</nav> </nav>
); );