Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7911e9e2 | ||
|
|
cb09b5bc5f | ||
|
|
a99031bdd2 | ||
|
|
ada8a98362 | ||
|
|
e176e81bae | ||
|
|
b64dd66c2c | ||
|
|
7fd162601b | ||
|
|
c1f346c935 |
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Analyze bundle
|
||||
run: |
|
||||
cd apps/web
|
||||
npx -p nextjs-bundle-analysis report
|
||||
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||
|
||||
- name: Upload bundle
|
||||
uses: actions/upload-artifact@v2
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
||||
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||
|
||||
- name: Create Comment
|
||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
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 type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||
|
||||
type AvailabilityOption = {
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
export type AvailabilityOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
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) =>
|
||||
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format(
|
||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||
);
|
||||
Intl.DateTimeFormat(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hourCycle: hour12 ? "h12" : "h24",
|
||||
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
|
||||
|
||||
const EventTypeScheduleDetails = ({
|
||||
isManagedEventType,
|
||||
@@ -188,61 +162,57 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
);
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
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)
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon={Clock}
|
||||
headline={t("new_schedule_heading")}
|
||||
description={t("new_schedule_description")}
|
||||
buttonRaw={<NewScheduleButton fromEventType />}
|
||||
border={false}
|
||||
/>
|
||||
);
|
||||
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
||||
if (isManagedEventType) {
|
||||
options.push({
|
||||
value: 0,
|
||||
label: t("members_default_schedule"),
|
||||
isDefault: false,
|
||||
isManaged: 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) => ({
|
||||
value: schedule.id,
|
||||
label: schedule.name,
|
||||
isDefault: schedule.isDefault,
|
||||
isManaged: false,
|
||||
}));
|
||||
setOptions(options);
|
||||
|
||||
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
||||
if (isManagedEventType) {
|
||||
options.push({
|
||||
value: 0,
|
||||
label: t("members_default_schedule"),
|
||||
isDefault: false,
|
||||
isManaged: false,
|
||||
});
|
||||
}
|
||||
const scheduleId = formMethods.getValues("schedule");
|
||||
const value = options.find((option) =>
|
||||
scheduleId
|
||||
? option.value === scheduleId
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
formMethods.setValue("availability", value);
|
||||
},
|
||||
});
|
||||
|
||||
const value = options.find((option) =>
|
||||
watchSchedule
|
||||
? option.value === watchSchedule
|
||||
: isManagedEventType
|
||||
? option.value === 0
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
const availabilityValue = formMethods.watch("availability");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -251,27 +221,33 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
{t("availability")}
|
||||
{shouldLockIndicator("availability")}
|
||||
</label>
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<AvailabilitySelect
|
||||
value={value}
|
||||
options={options}
|
||||
onBlur={field.onBlur}
|
||||
isDisabled={shouldLockDisableProps("schedule").disabled}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{isLoading && <SelectSkeletonLoader />}
|
||||
{!isLoading && (
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Select
|
||||
placeholder={t("select")}
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
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>
|
||||
{value?.value !== 0 ? (
|
||||
{availabilityValue?.value !== 0 ? (
|
||||
<EventTypeScheduleDetails
|
||||
selectedScheduleValue={value}
|
||||
selectedScheduleValue={availabilityValue}
|
||||
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -63,6 +64,7 @@ type Props = {
|
||||
enabledWorkflowsNumber: number;
|
||||
formMethods: UseFormReturn<FormValues>;
|
||||
isUpdateMutationLoading?: boolean;
|
||||
availability?: AvailabilityOption;
|
||||
};
|
||||
|
||||
function getNavigation(props: {
|
||||
@@ -71,8 +73,10 @@ function getNavigation(props: {
|
||||
enabledAppsNumber: number;
|
||||
enabledWorkflowsNumber: number;
|
||||
installedAppsNumber: number;
|
||||
availability: AvailabilityOption | undefined;
|
||||
}) {
|
||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
||||
props;
|
||||
const duration =
|
||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||
|
||||
@@ -128,6 +132,7 @@ function EventTypeSingleLayout({
|
||||
enabledWorkflowsNumber,
|
||||
isUpdateMutationLoading,
|
||||
formMethods,
|
||||
availability,
|
||||
}: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
@@ -171,6 +176,7 @@ function EventTypeSingleLayout({
|
||||
enabledAppsNumber,
|
||||
installedAppsNumber,
|
||||
enabledWorkflowsNumber,
|
||||
availability,
|
||||
});
|
||||
navigation.splice(1, 0, {
|
||||
name: "availability",
|
||||
@@ -214,7 +220,7 @@ function EventTypeSingleLayout({
|
||||
});
|
||||
}
|
||||
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}/${
|
||||
eventType.slug
|
||||
@@ -374,6 +380,8 @@ function EventTypeSingleLayout({
|
||||
tabs={EventTypeTabs}
|
||||
sticky
|
||||
linkProps={{ shallow: true }}
|
||||
itemClassname="items-start"
|
||||
iconClassName="md:mt-px"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.8.3",
|
||||
"version": "2.8.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NextPageContext } from "next";
|
||||
import type { DocumentContext, DocumentProps } from "next/document";
|
||||
import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||
import { z } from "zod";
|
||||
@@ -7,15 +8,22 @@ import { getDirFromLang } from "@calcom/lib/i18n";
|
||||
import { csp } from "@lib/csp";
|
||||
|
||||
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> {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const { nonce } = csp(ctx.req || null, ctx.res || null);
|
||||
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")) {
|
||||
// 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 || "";
|
||||
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { PeriodType } from "@prisma/client";
|
||||
import type { SchedulingType } from "@prisma/client";
|
||||
import type { PeriodType, SchedulingType } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -18,10 +17,10 @@ import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
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 { eventTypeBookingFields } 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 { trpc } from "@calcom/trpc/react";
|
||||
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
|
||||
import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
|
||||
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
|
||||
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
|
||||
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
||||
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
||||
@@ -94,6 +94,7 @@ export type FormValues = {
|
||||
children: ChildrenEventType[];
|
||||
hosts: { userId: number; isFixed: boolean }[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
availability?: AvailabilityOption;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
@@ -262,6 +263,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
}, [defaultValues]);
|
||||
|
||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||
const availability = formMethods.watch("availability");
|
||||
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
|
||||
let numberOfActiveApps = 0;
|
||||
|
||||
@@ -378,6 +380,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
enabledWorkflowsNumber={eventType.workflows.length}
|
||||
eventType={eventType}
|
||||
team={team}
|
||||
availability={availability}
|
||||
isUpdateMutationLoading={updateMutation.isLoading}
|
||||
formMethods={formMethods}
|
||||
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||
@@ -385,13 +388,66 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
<Form
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values: FormValues) => {
|
||||
if (!values.children.length) return handleSubmit(values);
|
||||
const existingSlugEventTypes = values.children.filter((ch) =>
|
||||
ch.owner.eventTypeSlugs.includes(slug)
|
||||
);
|
||||
if (!existingSlugEventTypes.length) return handleSubmit(values);
|
||||
setSlugExistsChildrenDialogOpen(existingSlugEventTypes);
|
||||
handleSubmit={async (values) => {
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
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>
|
||||
</Form>
|
||||
|
||||
@@ -1150,11 +1150,13 @@
|
||||
"current_username": "Nom d'utilisateur actuel",
|
||||
"example_1": "Exemple 1",
|
||||
"example_2": "Exemple 2",
|
||||
"booking_question_identifier": "Identifiant de la question de réservation",
|
||||
"company_size": "Taille de l'entreprise",
|
||||
"what_help_needed": "Avez-vous besoin d'aide ?",
|
||||
"variable_format": "Format des variables",
|
||||
"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.",
|
||||
"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",
|
||||
"set_ping_link": "Définir le lien Ping",
|
||||
"rate_limit_exceeded": "Limite dépassée",
|
||||
@@ -1380,6 +1382,7 @@
|
||||
"add_limit": "Ajouter une limite",
|
||||
"team_name_required": "Nom d'équipe requis",
|
||||
"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",
|
||||
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
|
||||
"replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.",
|
||||
@@ -1394,6 +1397,7 @@
|
||||
"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_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",
|
||||
"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",
|
||||
@@ -1702,6 +1706,13 @@
|
||||
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
|
||||
"verify": "Vérifier",
|
||||
"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",
|
||||
"select_all": "Tout sélectionner",
|
||||
"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",
|
||||
"no_show_fee_charged": "Frais d'absence facturés",
|
||||
"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.",
|
||||
"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",
|
||||
|
||||
@@ -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.",
|
||||
"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",
|
||||
"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_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",
|
||||
|
||||
@@ -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");
|
||||
@@ -38,6 +38,8 @@ model Host {
|
||||
isFixed Boolean @default(false)
|
||||
|
||||
@@id([userId, eventTypeId])
|
||||
@@index([userId])
|
||||
@@index([eventTypeId])
|
||||
}
|
||||
|
||||
model EventType {
|
||||
@@ -108,6 +110,8 @@ model EventType {
|
||||
@@unique([userId, slug])
|
||||
@@unique([teamId, slug])
|
||||
@@unique([userId, parentId])
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
model Credential {
|
||||
@@ -122,6 +126,9 @@ model Credential {
|
||||
appId String?
|
||||
destinationCalendars DestinationCalendar[]
|
||||
invalid Boolean? @default(false)
|
||||
|
||||
@@index([userId])
|
||||
@@index([appId])
|
||||
}
|
||||
|
||||
enum IdentityProvider {
|
||||
@@ -141,6 +148,10 @@ model DestinationCalendar {
|
||||
eventTypeId Int? @unique
|
||||
credentialId Int?
|
||||
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([eventTypeId])
|
||||
@@index([credentialId])
|
||||
}
|
||||
|
||||
enum UserPermissionRole {
|
||||
@@ -210,6 +221,10 @@ model User {
|
||||
verifiedNumbers VerifiedNumber[]
|
||||
hosts Host[]
|
||||
|
||||
@@index([email])
|
||||
@@index([emailVerified])
|
||||
@@index([identityProvider])
|
||||
@@index([identityProviderId])
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
@@ -251,6 +266,8 @@ model Membership {
|
||||
disableImpersonation Boolean @default(false)
|
||||
|
||||
@@id([userId, teamId])
|
||||
@@index([teamId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
@@ -262,6 +279,7 @@ model VerificationToken {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@index([token])
|
||||
}
|
||||
|
||||
model BookingReference {
|
||||
@@ -278,6 +296,11 @@ model BookingReference {
|
||||
externalCalendarId String?
|
||||
deleted Boolean?
|
||||
credentialId Int?
|
||||
|
||||
@@index([bookingId])
|
||||
@@index([credentialId])
|
||||
@@index([type])
|
||||
@@index([uid])
|
||||
}
|
||||
|
||||
model Attendee {
|
||||
@@ -289,6 +312,9 @@ model Attendee {
|
||||
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||
bookingId Int?
|
||||
bookingSeat BookingSeat?
|
||||
|
||||
@@index([email])
|
||||
@@index([bookingId])
|
||||
}
|
||||
|
||||
enum BookingStatus {
|
||||
@@ -336,6 +362,13 @@ model Booking {
|
||||
/// @zod.custom(imports.bookingMetadataSchema)
|
||||
metadata Json?
|
||||
isRecorded Boolean @default(false)
|
||||
|
||||
@@index([eventTypeId])
|
||||
@@index([userId])
|
||||
@@index([destinationCalendarId])
|
||||
@@index([recurringEventId])
|
||||
@@index([uid])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Schedule {
|
||||
@@ -363,6 +396,7 @@ model Availability {
|
||||
Schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
|
||||
@@index([userId])
|
||||
@@index([eventTypeId])
|
||||
@@index([scheduleId])
|
||||
}
|
||||
@@ -374,6 +408,9 @@ model SelectedCalendar {
|
||||
externalId String
|
||||
|
||||
@@id([userId, integration, externalId])
|
||||
@@index([userId])
|
||||
@@index([integration])
|
||||
@@index([externalId])
|
||||
}
|
||||
|
||||
enum EventTypeCustomInputType {
|
||||
@@ -395,6 +432,8 @@ model EventTypeCustomInput {
|
||||
options Json?
|
||||
required Boolean
|
||||
placeholder String @default("")
|
||||
|
||||
@@index([eventTypeId])
|
||||
}
|
||||
|
||||
model ResetPasswordRequest {
|
||||
@@ -415,6 +454,9 @@ model ReminderMail {
|
||||
reminderType ReminderType
|
||||
elapsedMinutes Int
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([referenceId])
|
||||
@@index([reminderType])
|
||||
}
|
||||
|
||||
model Payment {
|
||||
@@ -432,6 +474,9 @@ model Payment {
|
||||
data Json
|
||||
externalId String @unique
|
||||
paymentOption PaymentOption? @default(ON_BOOKING)
|
||||
|
||||
@@index([bookingId])
|
||||
@@index([externalId])
|
||||
}
|
||||
|
||||
enum PaymentOption {
|
||||
@@ -486,6 +531,8 @@ model ApiKey {
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||
appId String?
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model HashedLink {
|
||||
@@ -512,6 +559,8 @@ model Account {
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@index([userId])
|
||||
@@index([type])
|
||||
}
|
||||
|
||||
model Session {
|
||||
@@ -520,6 +569,8 @@ model Session {
|
||||
userId Int
|
||||
expires DateTime
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
enum AppCategories {
|
||||
@@ -549,6 +600,8 @@ model App {
|
||||
Webhook Webhook[]
|
||||
ApiKey ApiKey[]
|
||||
enabled Boolean @default(false)
|
||||
|
||||
@@index([enabled])
|
||||
}
|
||||
|
||||
model App_RoutingForms_Form {
|
||||
@@ -565,6 +618,9 @@ model App_RoutingForms_Form {
|
||||
disabled Boolean @default(false)
|
||||
/// @zod.custom(imports.RoutingFormSettings)
|
||||
settings Json?
|
||||
|
||||
@@index([userId])
|
||||
@@index([disabled])
|
||||
}
|
||||
|
||||
model App_RoutingForms_FormResponse {
|
||||
@@ -576,6 +632,8 @@ model App_RoutingForms_FormResponse {
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([formFillerId, formId])
|
||||
@@index([formFillerId])
|
||||
@@index([formId])
|
||||
}
|
||||
|
||||
model Feedback {
|
||||
@@ -585,6 +643,9 @@ model Feedback {
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
rating String
|
||||
comment String?
|
||||
|
||||
@@index([userId])
|
||||
@@index([rating])
|
||||
}
|
||||
|
||||
enum WorkflowTriggerEvents {
|
||||
@@ -617,6 +678,8 @@ model WorkflowStep {
|
||||
numberRequired Boolean?
|
||||
sender String?
|
||||
numberVerificationPending Boolean @default(true)
|
||||
|
||||
@@index([workflowId])
|
||||
}
|
||||
|
||||
model Workflow {
|
||||
@@ -631,6 +694,9 @@ model Workflow {
|
||||
time Int?
|
||||
timeUnit TimeUnit?
|
||||
steps WorkflowStep[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
model WorkflowsOnEventTypes {
|
||||
@@ -639,6 +705,9 @@ model WorkflowsOnEventTypes {
|
||||
workflowId Int
|
||||
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||
eventTypeId Int
|
||||
|
||||
@@index([workflowId])
|
||||
@@index([eventTypeId])
|
||||
}
|
||||
|
||||
model Deployment {
|
||||
@@ -668,6 +737,9 @@ model WorkflowReminder {
|
||||
workflowStepId Int?
|
||||
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id])
|
||||
cancelled Boolean?
|
||||
|
||||
@@index([bookingUid])
|
||||
@@index([workflowStepId])
|
||||
}
|
||||
|
||||
enum WorkflowTemplates {
|
||||
@@ -688,6 +760,9 @@ model BookingSeat {
|
||||
attendeeId Int @unique
|
||||
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
|
||||
data Json?
|
||||
|
||||
@@index([bookingId])
|
||||
@@index([attendeeId])
|
||||
}
|
||||
|
||||
model VerifiedNumber {
|
||||
@@ -697,6 +772,9 @@ model VerifiedNumber {
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
phoneNumber String
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
model Feature {
|
||||
@@ -714,6 +792,9 @@ model Feature {
|
||||
createdAt DateTime? @default(now())
|
||||
updatedAt DateTime? @default(now()) @updatedAt
|
||||
updatedBy Int?
|
||||
|
||||
@@index([enabled])
|
||||
@@index([stale])
|
||||
}
|
||||
|
||||
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({
|
||||
where: {
|
||||
id: booking.eventTypeId || undefined,
|
||||
id: booking.eventTypeId,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
},
|
||||
select: {
|
||||
@@ -791,15 +792,16 @@ export const bookingsRouter = router({
|
||||
},
|
||||
});
|
||||
|
||||
return eventType && eventType.users.find((user) => booking.userId === user.id);
|
||||
};
|
||||
|
||||
if (booking.userId !== user.id && !(await authorized())) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
|
||||
if (eventType && !eventType.users.find((user) => booking.userId === user.id)) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
|
||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||
if (isConfirmed) throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
|
||||
// Do not move this before authorization check.
|
||||
// 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 (confirmed && booking.payment.length > 0 && !booking.paid) {
|
||||
@@ -815,14 +817,22 @@ export const bookingsRouter = router({
|
||||
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 locale = attendee.locale ?? "en";
|
||||
let translate = translations.get(locale);
|
||||
if (!translate) {
|
||||
translate = await getTranslation(locale, "common");
|
||||
translations.set(locale, translate);
|
||||
}
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: {
|
||||
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||
locale: attendee.locale ?? "en",
|
||||
translate,
|
||||
locale,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -864,6 +874,9 @@ export const bookingsRouter = router({
|
||||
recurringEventId,
|
||||
id: booking.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}))
|
||||
) {
|
||||
// FIXME: It might be best to retrieve recurringEventId from the booking itself.
|
||||
|
||||
@@ -25,9 +25,10 @@ export type VerticalTabItemProps = {
|
||||
isExternalLink?: boolean;
|
||||
linkProps?: Omit<ComponentProps<typeof Link>, "href">;
|
||||
avatar?: string;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
const VerticalTabItem = function ({
|
||||
const VerticalTabItem = ({
|
||||
name,
|
||||
href,
|
||||
info,
|
||||
@@ -35,11 +36,10 @@ const VerticalTabItem = function ({
|
||||
disableChevron,
|
||||
linkProps,
|
||||
...props
|
||||
}: VerticalTabItemProps) {
|
||||
}: VerticalTabItemProps) => {
|
||||
const { t } = useLocale();
|
||||
const { asPath } = useRouter();
|
||||
const isCurrent = asPath.startsWith(href);
|
||||
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
{!props.hidden && (
|
||||
@@ -60,7 +60,12 @@ const VerticalTabItem = function ({
|
||||
data-testid={`vertical-tab-${name}`}
|
||||
aria-current={isCurrent ? "page" : undefined}>
|
||||
{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">
|
||||
<span className="flex items-center space-x-2 rtl:space-x-reverse">
|
||||
|
||||
@@ -12,9 +12,18 @@ export interface NavTabProps {
|
||||
sticky?: boolean;
|
||||
linkProps?: VerticalTabItemProps["linkProps"];
|
||||
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 (
|
||||
<nav
|
||||
className={classNames(
|
||||
@@ -27,7 +36,13 @@ const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassna
|
||||
{sticky && <div className="pt-6" />}
|
||||
{props.children}
|
||||
{tabs.map((tab, idx) => (
|
||||
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={itemClassname} />
|
||||
<VerticalTabItem
|
||||
{...tab}
|
||||
key={idx}
|
||||
linkProps={linkProps}
|
||||
className={itemClassname}
|
||||
iconClassName={iconClassName}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user