Compare commits

..
29 Commits
Author SHA1 Message Date
GitHub Actions cae8470350 chore: release v5.2.1
Post release / release (push) Canceled after 0s
2025-04-17 00:05:25 +00:00
Joe Au-YeungandBenny Joo a82d616b79 feat: Whitelist user's from workflow spam scanning (#20717)
* Add whitelistWorkflows to user

* Do not autolock whitelisted users

* Add endpoint to whitelist a user

* Get users data - include whitelistWorkflows

* Whitelist a user on the user admin page

* Fix typo

* Add test

* Use translations

* Add `updateWhitelistWorkflows` to `UserRepository`

* Use i18 for strings

* Rename to whitelistWorkflows

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-04-16 18:09:29 -04:00
Anik Dhabal Babu 515f3263e0 test: e2e test for the Routing Form to Event Booking flow (#20728)
* test: e2e test Routing Form submission to Booking flow

* update

* tweak
2025-04-16 16:09:15 -03:00
Lauris Skraucis aebbc50ba6 fix: EventTypeSettings atom default schedule title (#20725) 2025-04-16 21:26:30 +03:00
Benny Joo 7cdc78d02d update middleware (#20726) 2025-04-16 17:44:25 +00:00
Keith Williams df55dec553 chore: Rename @calcom/lib/date-fns to @calcom/lib/dayjs (#19870)
* chore: Rename date-fns to dayjs

* Fixed types
2025-04-16 16:26:42 +00:00
Benny Joo 398ad22868 fix wrong translation (#20727) 2025-04-16 15:37:21 +00:00
Lauris Skraucis 7ef1050946 feat: v2 get slots using teamSlug + eventTypeSlug + optional organizationSlug (#20554)
* feat: allow teamSlug + eventTypeSlug + optional organizationSlug as input

* feat: return team event slots

* test: get slots using teamSlug + eventTypeSlug + optional organizationalSlug

* docs: how to get team event slots
2025-04-16 18:29:47 +03:00
Peer Richelsen 7b8bbaaa86 added lindy (#20721) 2025-04-16 14:27:53 +00:00
Omar López ddfd3fda70 refactor: improve error handling in captureErrorsMiddleware (#20718) 2025-04-16 14:01:47 +00:00
Benny Joo 5972a6f77f perf: Fetch data server-side for Webhook Create page (#20675)
* perf: fetch data server-side for create webhooks page

* refactor

* refactor

* refactor

* improve skeleton styling

* fix styling
2025-04-16 10:59:18 -03:00
9bc703a437 feat: Embed - Show skeleton loader when pageType is provided (#20646)
* mvp done

* wip

* fix ts errors and other code improvements

* fix ts errors

* ensure mobile layout support

* Make skeleton responsive on screen resize

* refactor

* Add test for EmbedElement

* make skeleton closer to pixel perfect

* Address PR feedback

* Remove PR_TODO

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-04-16 13:40:51 +00:00
Syed Ali Shahbaz 15aaf7f51b fix: readonly ics error (#20651)
* readonly error fix

* warn instead of throwing err

* .

* .

* fix err

* dont throw error, only warn

* --

* remove skip writing as that's unnecessary

* fix api
2025-04-16 09:02:56 -03:00
Lauris SkraucisandAnik Dhabal Babu c89a1877ae feat: organization booking hooks (#20611)
* docs: request auth headers

* feat: useOrganizationsBookings hook

* feat: useOrganizationUserBookings hook

* rename hooks

* query params

* fix: organizations users bookings extract user from userId not auth

* fix: passing array from hook

* docs

* correct header doc

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-04-16 10:59:02 +03:00
Lauris Skraucis 15d2c0f858 feat: v2 return slots based on routing form response (#20637)
* feat: routing form response slots

* fix

* test routing form slots

* chore: bump libraries

* docs

* docs

* chore: api docs from main to merge main

* chore: regenerate api docs
2025-04-16 10:55:01 +03:00
Morgan 43a6663230 fix: routing form endpoints date filters validation (#20715) 2025-04-16 06:30:29 +00:00
58315e96c9 fix: don't allow seats for recurring events (#19257)
* fix: Don't allow seats for recurring events #19061

* update the commit with English

* Update common.json

* Update common.json

* Update common.json

* Add edge case when people have already enabled seats as well as recurring event

* fix type issue

* Handle case where seats and recurring is already enabled

* minor fix

* minor change

* typo

* Block backend functionality

* blocking new booking and other enhancements

* final-changes

* minor-changes

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-04-15 21:07:57 +00:00
sean-brydon 3e466022b8 fix availability CTA height (#20714) 2025-04-15 20:11:29 +00:00
Keith Williams 68c036cd0a fix: Tasker import and error handling for routing form responses (#20713) 2025-04-15 19:43:54 +00:00
Benny Joo 77b656024a feat: placeholder component for user dropdown to remove layout shift (#20696)
* feat

* refactor
2025-04-15 15:17:14 -04:00
amritandTushar Bhatt 89772a0dac fix: make padding equal on availability toggle (#20708)
* fix: trigger lingo.dev by removing duplicate value

* fix: uneven height in toggle

* address mrge bots comments

* use tailwind

* fix: make padding equal for availability toggle

* Update apps/web/modules/availability/availability-view.tsx

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-04-15 18:40:04 +00:00
Giridhar 381c462660 feat: email workflow templates translation (#20674)
* Add translations in respective files

* implement multilingual support for email templates

* fix: indentation in common.js & typos in common.js and update.handler.ts
2025-04-15 23:56:34 +05:30
Shawn CazaandTushar Bhatt 417acdaacb fix: Improves dialog window that appears when there is no availability in a month. Adding a description to clarify limits when rolling or range period types are in use. (#20451)
* init

* updated typing, english localization text, used existing calculatePeriodLimits and isTimeViolatingFutureLimit to determine if go to next month button should be disabled

* improving copy

* semantic dialog heading tag, proper EN localization,

* reverting

* force local to origin

* Translations by LLM

* intit tests

* moving NoAvailability to its own file.

* refactor overlay component to encpsulate logic in hooks

* improved tests

* remove unintended change

* fix: period data type import. Providing default periodData values

* fix: type errors on event object

* fix: updates case in type import

* removing translations

* remove unnecessary import

* remove unnecessary import

* improving mocks for booker and logger

* moving tests to __tests__ folder

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-04-15 23:46:01 +05:30
Kartik SainiandTushar Bhatt 1584dd3179 feat: Redirect to original event link for rescheduling cancelled booking (#18605)
* feat: redirect to original event link for rescheduling cancelled booking

* allow cancelled booking rescheduling when allowRescheduleForCancelledBooking is true

* chore

* fix test

* chore

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-04-15 14:09:18 -04:00
d66f983f56 feat: dry run behaviour for connect atoms (#20699)
* add dry run prop for mocking actual behaviour without actual api calls

* fix e2e tests

* fixup

* remove await present twice

* fix: adjust success toast for apple connect dry run

Co-Authored-By: rajiv@cal.com <rajiv@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: rajiv@cal.com <rajiv@cal.com>
2025-04-15 17:45:14 +00:00
Rajiv Sahal b1f50fe22d feat: dry run behaviour for AvailabilitySettings atom (#20686) 2025-04-15 22:24:21 +05:30
Rajiv Sahal ce995c39f2 feat: dry run behaviour for event type atoms (#20689) 2025-04-15 22:23:51 +05:30
Rajiv Sahal b99ea16b31 feat: dry run behaviour for CalendarSettings atom (#20690) 2025-04-15 22:23:05 +05:30
Lauris Skraucis 574d8882f4 refactor: required booking fields message (#20688) 2025-04-15 16:29:45 +00:00
232 changed files with 5842 additions and 1640 deletions
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+1 -1
View File
@@ -3,7 +3,7 @@ import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { createContext } from "@calcom/trpc/server/createContext";
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.172",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.173",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -146,8 +146,8 @@ export class BookingsService_2024_08_13 {
}
async hasRequiredBookingFieldsResponses(body: CreateBookingInput, eventType: EventType | null) {
const bookingFields = body.bookingFieldsResponses;
if (!bookingFields || !eventType || !eventType.bookingFields) {
const bookingFields = { ...body.bookingFieldsResponses, attendeePhoneNumber: body.attendee.phoneNumber };
if (!eventType?.bookingFields) {
return true;
}
@@ -156,11 +156,20 @@ export class BookingsService_2024_08_13 {
.parse(eventType.bookingFields)
.filter((field) => !field.editable.startsWith("system"));
if (!eventTypeBookingFields.length) {
return true;
}
for (const field of eventTypeBookingFields) {
if (field.required && !(field.name in bookingFields)) {
throw new BadRequestException(`
Missing required booking field response: ${field.name} - it is required by the event type booking fields, but missing in the bookingFieldsResponses.
You can fetch the event type with ID ${eventType.id} to see the required fields.`);
if (field.name === "attendeePhoneNumber") {
throw new BadRequestException(
`Missing attendee phone number - it is required by the event type. Pass it as "attendee.phoneNumber" in the request.`
);
}
throw new BadRequestException(
`Missing required booking field response: ${field.name} - it is required by the event type booking fields, but missing in the bookingFieldsResponses. You can fetch the event type with ID ${eventType.id} to see the required fields.`
);
}
}
@@ -185,7 +194,11 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputBooking(databaseBooking);
}
async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
async createRecurringBooking(
request: Request,
body: CreateRecurringBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
) {
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
const bookings = await handleNewRecurringBooking({
bookingData: bookingRequest.body,
@@ -202,7 +215,11 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputRecurringBookings(ids);
}
async createRecurringSeatedBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
async createRecurringSeatedBooking(
request: Request,
body: CreateRecurringBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
) {
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
const bookings = await handleNewRecurringBooking({
bookingData: bookingRequest.body,
@@ -219,7 +236,11 @@ export class BookingsService_2024_08_13 {
);
}
async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
async createRegularBooking(
request: Request,
body: CreateBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
) {
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
const booking = await handleNewBooking({
bookingData: bookingRequest.body,
@@ -244,7 +265,11 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputBooking(databaseBooking);
}
async createSeatedBooking(request: Request, body: CreateBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam) {
async createSeatedBooking(
request: Request,
body: CreateBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
) {
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
const booking = await handleNewBooking({
bookingData: bookingRequest.body,
@@ -129,7 +129,7 @@ describe("Platform Calendars Endpoints", () => {
it(`/GET/v2/calendars/${GOOGLE_CALENDAR}/connect: it should redirect to auth-url for google calendar OAuth with valid access token `, async () => {
const response = await request(app.getHttpServer())
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect`)
.get(`/v2/calendars/${GOOGLE_CALENDAR}/connect?redir=https://cal.com&isDryRun=false`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("Origin", CLIENT_REDIRECT_URI)
.expect(200);
@@ -34,6 +34,7 @@ import {
BadRequestException,
Post,
Body,
ParseBoolPipe,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
@@ -148,13 +149,14 @@ export class CalendarsController {
@Req() req: Request,
@Headers("Authorization") authorization: string,
@Param("calendar") calendar: string,
@Query("redir") redir?: string | null
@Query("redir") redir?: string | null,
@Query("isDryRun", ParseBoolPipe) isDryRun?: boolean
): Promise<ApiResponse<{ authUrl: string }>> {
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.connect(authorization, req, redir ?? "");
return await this.outlookService.connect(authorization, req, redir ?? "", isDryRun);
case GOOGLE_CALENDAR:
return await this.googleCalendarService.connect(authorization, req, redir ?? "");
return await this.googleCalendarService.connect(authorization, req, redir ?? "", isDryRun);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
@@ -179,18 +181,30 @@ export class CalendarsController {
): Promise<{ url: string }> {
// state params contains our user access token
const stateParams = new URLSearchParams(state);
const { accessToken, origin, redir } = z
.object({ accessToken: z.string(), origin: z.string(), redir: z.string().nullish().optional() })
const { accessToken, origin, redir, isDryRun } = z
.object({
accessToken: z.string(),
origin: z.string(),
redir: z.string().nullish().optional(),
isDryRun: z.string().nullish().optional(),
})
.parse({
accessToken: stateParams.get("accessToken"),
origin: stateParams.get("origin"),
redir: stateParams.get("redir"),
isDryRun: stateParams.get("isDryRun"),
});
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.save(code, accessToken, origin, redir ?? "");
return await this.outlookService.save(code, accessToken, origin, redir ?? "", isDryRun === "true");
case GOOGLE_CALENDAR:
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "");
return await this.googleCalendarService.save(
code,
accessToken,
origin,
redir ?? "",
isDryRun === "true"
);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
@@ -39,31 +39,38 @@ export class GoogleCalendarService implements OAuthCalendarApp {
async connect(
authorization: string,
req: Request,
redir?: string
redir?: string,
isDryRun?: boolean
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
const accessToken = authorization.replace("Bearer ", "");
const origin = req.get("origin") ?? req.get("host");
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir, isDryRun);
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
}
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
async save(
code: string,
accessToken: string,
origin: string,
redir?: string,
isDryRun?: boolean
): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir, isDryRun);
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
return await this.checkIfCalendarConnected(userId);
}
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
const oAuth2Client = await this.getOAuthClient(this.redirectUri);
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: CALENDAR_SCOPES,
prompt: "consent",
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
});
return authUrl;
@@ -115,7 +122,8 @@ export class GoogleCalendarService implements OAuthCalendarApp {
code: string,
accessToken: string,
origin: string,
redir?: string
redir?: string,
isDryRun?: boolean
) {
// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
@@ -123,6 +131,11 @@ export class GoogleCalendarService implements OAuthCalendarApp {
return { url: redir || origin };
}
// if isDryRun is true we know its a dry run so we just redirect straight away
if (isDryRun) {
return { url: redir || origin };
}
const parsedCode = z.string().parse(code);
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
@@ -33,24 +33,31 @@ export class OutlookService implements OAuthCalendarApp {
async connect(
authorization: string,
req: Request,
redir?: string
redir?: string,
isDryRun?: boolean
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
const accessToken = authorization.replace("Bearer ", "");
const origin = req.get("origin") ?? req.get("host");
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir, isDryRun);
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
}
async save(code: string, accessToken: string, origin: string, redir?: string): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir);
async save(
code: string,
accessToken: string,
origin: string,
redir?: string,
isDryRun?: boolean
): Promise<{ url: string }> {
return await this.saveCalendarCredentialsAndRedirect(code, accessToken, origin, redir, isDryRun);
}
async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
return await this.checkIfCalendarConnected(userId);
}
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string) {
async getCalendarRedirectUrl(accessToken: string, origin: string, redir?: string, isDryRun?: boolean) {
const { client_id } = await this.calendarsService.getAppKeys(OFFICE_365_CALENDAR_ID);
const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];
@@ -60,7 +67,7 @@ export class OutlookService implements OAuthCalendarApp {
client_id,
prompt: "select_account",
redirect_uri: this.redirectUri,
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}`,
state: `accessToken=${accessToken}&origin=${origin}&redir=${redir ?? ""}&isDryRun=${isDryRun}`,
};
const query = stringify(params);
@@ -148,13 +155,19 @@ export class OutlookService implements OAuthCalendarApp {
code: string,
accessToken: string,
origin: string,
redir?: string
redir?: string,
isDryRun?: boolean
) {
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
if (!code || code === "undefined") {
return { url: redir || origin };
}
// if isDryRun is true we know its a dry run so we just redirect straight away
if (isDryRun) {
return { url: redir || origin };
}
const parsedCode = z.string().parse(code);
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
@@ -8,6 +8,7 @@ import { MeModule } from "@/ee/me/me.module";
import { ProviderModule } from "@/ee/provider/provider.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module";
import { SlotsModule_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.module";
import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
@@ -33,6 +34,7 @@ import { Module } from "@nestjs/common";
SlotsModule_2024_04_15,
SlotsModule_2024_09_04,
TeamsModule,
RoutingFormsModule,
],
})
export class PlatformEndpointsModule implements NestModule {
@@ -1,5 +1,11 @@
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
OPTIONAL_API_KEY_HEADER,
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
@@ -8,15 +14,12 @@ import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.g
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { OrganizationsUsersService } from "@/modules/organizations/users/index/services/organizations-users-service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
import { GetOrganizationsBookingsInput } from "./inputs/get-org-bookings.input";
import { GetBookingsOutput_2024_08_13, GetOrganizationsBookingsInput } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId/bookings",
@@ -24,11 +27,11 @@ import { GetOrganizationsBookingsInput } from "./inputs/get-org-bookings.input";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Bookings")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
export class OrganizationsBookingsController {
constructor(
private readonly bookingsService: BookingsService_2024_08_13,
private readonly orgUsersService: OrganizationsUsersService
) {}
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
@Get("/")
@ApiOperation({ summary: "Get Organization Bookings" })
@@ -152,7 +152,7 @@ export class OrganizationsEventTypesController {
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
@Get("/teams/:teamId/event-types")
@ApiOperation({ summary: "Get a team event type" })
@ApiOperation({ summary: "Get team event types" })
async getTeamEventTypes(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
@@ -40,7 +40,7 @@ const SCALE = "SCALE";
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Orgs")
@DocsTags("Managed Orgs")
@ApiHeader(X_CAL_CLIENT_ID_HEADER)
@ApiHeader(X_CAL_SECRET_KEY_HEADER)
export class OrganizationsOrganizationsController {
@@ -178,9 +178,12 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
});
it("should get routing form responses", async () => {
const createdAt = new Date(routingFormResponse.createdAt);
createdAt.setHours(createdAt.getHours() - 1);
const isoStringCreatedAt = createdAt.toISOString();
return request(app.getHttpServer())
.get(
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?skip=0&take=2&sortUpdatedAt=asc&sortCreatedAt=desc`
`/v2/organizations/${org.id}/routing-forms/${routingForm.id}/responses?skip=0&take=2&sortUpdatedAt=asc&sortCreatedAt=desc&afterCreatedAt=${isoStringCreatedAt}`
)
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
.expect(200)
@@ -1,15 +1,6 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import {
IsOptional,
IsString,
IsEnum,
IsISO8601,
IsDate,
IsNumber,
IsArray,
ArrayMinSize,
} from "class-validator";
import { IsOptional, IsString, IsEnum, IsISO8601, IsNumber, IsArray, ArrayMinSize } from "class-validator";
enum SortOrder {
ASC = "asc",
@@ -44,9 +35,7 @@ export class GetRoutingFormResponsesParams {
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
afterCreatedAt?: Date;
afterCreatedAt?: string;
@ApiPropertyOptional({
type: String,
@@ -55,9 +44,7 @@ export class GetRoutingFormResponsesParams {
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
beforeCreatedAt?: Date;
beforeCreatedAt?: string;
@ApiPropertyOptional({
type: String,
@@ -66,9 +53,7 @@ export class GetRoutingFormResponsesParams {
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
afterUpdatedAt?: Date;
afterUpdatedAt?: string;
@ApiPropertyOptional({
type: String,
@@ -77,9 +62,7 @@ export class GetRoutingFormResponsesParams {
})
@IsOptional()
@IsISO8601()
@Transform(({ value }) => value && new Date(value))
@IsDate()
beforeUpdatedAt?: Date;
beforeUpdatedAt?: string;
@ApiPropertyOptional({ type: String, description: "Filter by responses routed to a specific booking" })
@IsOptional()
@@ -15,10 +15,10 @@ export class OrganizationsRoutingFormsRepository {
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
afterUpdatedAt?: string;
beforeUpdatedAt?: string;
teamIds?: number[];
}
) {
@@ -61,10 +61,10 @@ export class OrganizationsRoutingFormsRepository {
options?: {
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
afterUpdatedAt?: string;
beforeUpdatedAt?: string;
routedToBookingUid?: string;
}
) {
@@ -17,10 +17,10 @@ export class OrganizationsRoutingFormsResponsesService {
options?: {
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
afterUpdatedAt?: string;
beforeUpdatedAt?: string;
routedToBookingUid?: string;
}
) {
@@ -14,10 +14,10 @@ export class OrganizationsRoutingFormsService {
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
afterUpdatedAt?: string;
beforeUpdatedAt?: string;
teamIds?: number[];
}
) {
@@ -18,6 +18,18 @@ export class OrganizationsTeamsRepository {
});
}
async findOrgTeamBySlug(organizationId: number, teamSlug: string) {
return this.dbRead.prisma.team.findUnique({
where: {
slug_parentId: {
slug: teamSlug,
parentId: organizationId,
},
isOrganization: false,
},
});
}
async findOrgTeams(organizationId: number) {
return this.dbRead.prisma.team.findMany({
where: {
@@ -5,7 +5,6 @@ import { OrganizationsRoutingFormsResponsesService } from "@/modules/organizatio
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { RedisService } from "@/modules/redis/redis.service";
import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { Module } from "@nestjs/common";
@@ -13,8 +13,8 @@ export class OrganizationsTeamsRoutingFormsResponsesRepository {
take: number,
options?: {
sortCreatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
routedToBookingUid?: string;
}
) {
@@ -15,10 +15,10 @@ export class OrganizationsTeamsRoutingFormsRepository {
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
afterUpdatedAt?: string;
beforeUpdatedAt?: string;
}
) {
const {
@@ -18,10 +18,10 @@ export class OrganizationsTeamsRoutingFormsResponsesService {
options?: {
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
afterUpdatedAt?: string;
beforeUpdatedAt?: string;
routedToBookingUid?: string;
}
) {
@@ -15,10 +15,10 @@ export class OrganizationsTeamsRoutingFormsService {
name?: string;
sortCreatedAt?: "asc" | "desc";
sortUpdatedAt?: "asc" | "desc";
afterCreatedAt?: Date;
beforeCreatedAt?: Date;
afterUpdatedAt?: Date;
beforeUpdatedAt?: Date;
afterCreatedAt?: string;
beforeCreatedAt?: string;
afterUpdatedAt?: string;
beforeUpdatedAt?: string;
}
) {
return this.routingFormsRepository.getTeamRoutingForms(teamId, skip, take, options);
@@ -1,6 +1,10 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import {
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
@@ -10,11 +14,10 @@ import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
import { OrganizationUsersBookingsService } from "@/modules/organizations/users/bookings/services/organization-users-bookings.service";
import { Controller, UseGuards, Get, Query, ParseIntPipe, Param } from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetBookingsInput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
import { User } from "@calcom/prisma/client";
@Controller({
path: "/v2/organizations/:orgId/users/:userId/bookings",
@@ -22,6 +25,9 @@ import { User } from "@calcom/prisma/client";
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsUserInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Orgs / Users / Bookings")
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
export class OrganizationsUsersBookingsController {
constructor(private readonly organizationUsersBookingsService: OrganizationUsersBookingsService) {}
@@ -30,13 +36,13 @@ export class OrganizationsUsersBookingsController {
@PlatformPlan("ESSENTIALS")
@ApiOperation({ summary: "Get all bookings of an organization user" })
async getOrganizationUserBookings(
@GetUser() user: User,
@Param("orgId", ParseIntPipe) orgId: number,
@Param("userId", ParseIntPipe) userId: number,
@Query() query: GetBookingsInput_2024_08_13
): Promise<GetBookingsOutput_2024_08_13> {
const bookings = await this.organizationUsersBookingsService.getOrganizationUserBookings(
orgId,
user,
userId,
query
);
@@ -1,18 +1,21 @@
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { Injectable } from "@nestjs/common";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { GetBookingsInput_2024_08_13 } from "@calcom/platform-types";
import { User } from "@calcom/prisma/client";
@Injectable()
export class OrganizationUsersBookingsService {
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
constructor(
private readonly bookingsService: BookingsService_2024_08_13,
private readonly usersRepository: UsersRepository
) {}
async getOrganizationUserBookings(
orgId: number,
user: Pick<User, "id" | "email">,
queryParams: GetBookingsInput_2024_08_13
) {
async getOrganizationUserBookings(orgId: number, userId: number, queryParams: GetBookingsInput_2024_08_13) {
const user = await this.usersRepository.findById(userId);
if (!user) {
throw new NotFoundException(`getOrganizationUserBookings - User ${userId} not found`);
}
return await this.bookingsService.getBookings(queryParams, { orgId, id: user.id, email: user.email });
}
}
@@ -0,0 +1,44 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_HEADER } from "@/lib/docs/headers";
import { ResponseSlotsOutput } from "@/modules/routing-forms/outputs/response-slots.output";
import { RoutingFormsService } from "@/modules/routing-forms/services/routing-forms.service";
import { Controller, HttpCode, HttpStatus, Param, Post, Query, Req } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
import { Request } from "express";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types";
@Controller({
path: "/v2/routing-forms/:routingFormId",
version: API_VERSIONS_VALUES,
})
@ApiTags("Routing forms")
@ApiHeader(API_KEY_HEADER)
export class RoutingFormsController {
constructor(private readonly routingFormsService: RoutingFormsService) {}
@Post("/calculate-slots")
@ApiOperation({
summary: "Calculate slots based on routing form response",
description:
"It will not actually save the response just return the routed event type and slots when it can be booked.",
})
@HttpCode(HttpStatus.OK)
async calculateSlotsBasedOnRoutingFormResponse(
@Req() request: Request,
@Query() query: GetAvailableSlotsInput_2024_09_04,
@Param("routingFormId") routingFormId: string
): Promise<ResponseSlotsOutput> {
const responseSlots = await this.routingFormsService.calculateSlotsBasedOnRoutingFormResponse(
request,
routingFormId,
query
);
return {
status: SUCCESS_STATUS,
data: responseSlots,
};
}
}
@@ -0,0 +1,355 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import {
ResponseSlotsOutput,
ResponseSlotsOutputData,
} from "@/modules/routing-forms/outputs/response-slots.output";
import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { App_RoutingForms_Form, EventType, User } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { RoutingFormsRepositoryFixture } from "test/fixtures/repository/routing-forms.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { Team } from "@calcom/prisma/client";
describe("Routing forms endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let routingFormsRepositoryFixture: RoutingFormsRepositoryFixture;
const teammateEmailTwo = `routing-forms-teammate-1-${randomString()}`;
const teammateEmailOne = `routing-forms-teammate-2-${randomString()}`;
let team: Team;
let teammateOne: User;
let teammateTwo: User;
let collectiveEventTypeFootball: EventType;
let collectiveEventTypeBasketball: EventType;
let routingForm: App_RoutingForms_Form;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
AppModule,
PrismaModule,
UsersModule,
TokensModule,
SchedulesModule_2024_06_11,
SlotsModule_2024_09_04,
],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
routingFormsRepositoryFixture = new RoutingFormsRepositoryFixture(moduleRef);
teammateOne = await userRepositoryFixture.create({
email: teammateEmailOne,
name: teammateEmailOne,
username: teammateEmailOne,
});
teammateTwo = await userRepositoryFixture.create({
email: teammateEmailTwo,
name: teammateEmailTwo,
username: teammateEmailTwo,
});
const teamSlug = `routing-forms-team-${randomString()}`;
team = await teamRepositoryFixture.create({
name: teamSlug,
slug: teamSlug,
isOrganization: false,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: teammateOne.id } },
team: { connect: { id: team.id } },
accepted: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: teammateTwo.id } },
team: { connect: { id: team.id } },
accepted: true,
});
const collectiveEventTypeFootballSlug = `routing-forms-collective-event-type-football-${randomString()}`;
collectiveEventTypeFootball = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team.id },
},
title: "Collective Event Type Football",
slug: collectiveEventTypeFootballSlug,
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
schedule: {
create: {
name: "football",
timeZone: "Europe/London",
user: { connect: { id: teammateOne.id } },
availability: {
create: {
days: [1],
startTime: new Date("1970-01-01T09:00:00Z"),
endTime: new Date("1970-01-01T11:00:00Z"),
},
},
},
},
users: {
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }],
},
hosts: {
create: [
{
userId: teammateOne.id,
isFixed: true,
},
{
userId: teammateTwo.id,
isFixed: true,
},
],
},
});
const collectiveEventTypeBasketballSlug = `routing-forms-collective-event-type-basketball-${randomString()}`;
collectiveEventTypeBasketball = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team.id },
},
title: "Collective Event Type Basketball",
slug: collectiveEventTypeBasketballSlug,
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
schedule: {
create: {
name: "basketball",
timeZone: "Europe/London",
user: { connect: { id: teammateTwo.id } },
availability: {
create: {
days: [2],
startTime: new Date("1970-01-01T11:00:00Z"),
endTime: new Date("1970-01-01T13:00:00Z"),
},
},
},
},
users: {
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }],
},
hosts: {
create: [
{
userId: teammateOne.id,
isFixed: true,
},
{
userId: teammateTwo.id,
isFixed: true,
},
],
},
});
routingForm = await routingFormsRepositoryFixture.create({
name: "football or basketball",
description: "Do you want to play football or basketball?",
routes: [
{
id: "baba89aa-4567-489a-bcde-f1961b1ae10c",
action: {
type: "eventTypeRedirectUrl",
value: `team/${team.slug}/${collectiveEventTypeFootballSlug}`,
eventTypeId: collectiveEventTypeFootball.id,
},
queryValue: {
id: "a8a8b9aa-0123-4456-b89a-b1961b1ae10c",
type: "group",
children1: {
"99ab8aaa-4567-489a-bcde-f1961b1ae5cd": {
type: "rule",
properties: {
field: "dcd8b978-fa11-47be-801a-5ca2cd4ec16e",
value: ["755c11d1-fa46-4d03-8e15-d2c4ab3407d7"],
operator: "select_equals",
valueSrc: ["value"],
valueType: ["select"],
valueError: [null],
},
},
},
},
attributesQueryValue: { id: "b8998aab-cdef-4012-b456-71961b1ae10c", type: "group" },
attributeRoutingConfig: {},
fallbackAttributesQueryValue: { id: "ab98888a-89ab-4cde-b012-31961b1ae10c", type: "group" },
},
{
id: "88a98b98-0123-4456-b89a-b1961b1b1a04",
action: {
type: "eventTypeRedirectUrl",
value: `team/${team.slug}/${collectiveEventTypeBasketballSlug}`,
eventTypeId: collectiveEventTypeBasketball.id,
},
queryValue: {
id: "bab8b9a8-cdef-4012-b456-71961b1b1a04",
type: "group",
children1: {
"9988a98a-0123-4456-b89a-b1961b1b1f8b": {
type: "rule",
properties: {
field: "dcd8b978-fa11-47be-801a-5ca2cd4ec16e",
value: ["4cbb1537-0ab9-4286-b4bf-08b08ff23372"],
operator: "select_equals",
valueSrc: ["value"],
valueType: ["select"],
valueError: [null],
},
},
},
},
attributesQueryValue: { id: "99a89a9b-89ab-4cde-b012-31961b1b1a04", type: "group" },
attributeRoutingConfig: {},
fallbackAttributesQueryValue: { id: "88989a98-4567-489a-bcde-f1961b1b1a04", type: "group" },
},
{
id: "9b9ab9bb-cdef-4012-b456-71961b1a8b78",
action: {
type: "customPageMessage",
value: "Thank you for your interest! We will be in touch soon.",
},
isFallback: true,
queryValue: { id: "9b9ab9bb-cdef-4012-b456-71961b1a8b78", type: "group" },
attributesQueryValue: { id: "88b988ab-cdef-4012-b456-71961b1ad81a", type: "group" },
fallbackAttributesQueryValue: { id: "a8b89a9a-89ab-4cde-b012-31961b1ad81a", type: "group" },
},
],
createdAt: new Date("2025-04-09T15:10:46.651Z"),
updatedAt: new Date("2025-04-09T15:11:37.696Z"),
fields: [
{
id: "dcd8b978-fa11-47be-801a-5ca2cd4ec16e",
type: "select",
label: "sport",
options: [
{ id: "755c11d1-fa46-4d03-8e15-d2c4ab3407d7", label: "football" },
{ id: "4cbb1537-0ab9-4286-b4bf-08b08ff23372", label: "basketball" },
],
required: true,
},
],
user: {
connect: { id: teammateOne.id },
},
disabled: false,
settings: { emailOwnerOnSubmission: true },
team: {
connect: { id: team.id },
},
position: 0,
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
describe("Calculate slots", () => {
const slotsQuery = "start=2060-01-12&end=2060-01-16";
it("should return correct slots for option 1", async () => {
const body = {
sport: "football",
};
const reserveResponse = await request(app.getHttpServer())
.post(`/v2/routing-forms/${routingForm.id}/calculate-slots?${slotsQuery}`)
.send(body)
.expect(200);
const responseBody: ResponseSlotsOutput = reserveResponse.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const responseBodyData: ResponseSlotsOutputData = responseBody.data;
expect(responseBodyData.eventTypeId).toEqual(collectiveEventTypeFootball.id);
expect(responseBodyData.slots).toEqual({
"2060-01-12": [
{
start: "2060-01-12T09:00:00.000Z",
},
{
start: "2060-01-12T10:00:00.000Z",
},
],
});
});
it("should return correct slots for option 2", async () => {
const body = {
sport: "basketball",
};
const reserveResponse = await request(app.getHttpServer())
.post(`/v2/routing-forms/${routingForm.id}/calculate-slots?${slotsQuery}`)
.send(body)
.expect(200);
const responseBody: ResponseSlotsOutput = reserveResponse.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const responseBodyData: ResponseSlotsOutputData = responseBody.data;
expect(responseBodyData.eventTypeId).toEqual(collectiveEventTypeBasketball.id);
expect(responseBodyData.slots).toEqual({
"2060-01-13": [
{
start: "2060-01-13T11:00:00.000Z",
},
{
start: "2060-01-13T12:00:00.000Z",
},
],
});
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(teammateOne.email);
await userRepositoryFixture.deleteByEmail(teammateTwo.email);
await teamRepositoryFixture.delete(team.id);
await app.close();
});
});
@@ -0,0 +1,32 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsNumber, ValidateNested } from "class-validator";
import { ApiResponseWithoutData, SlotsOutput_2024_09_04 } from "@calcom/platform-types";
import { RangeSlotsOutput_2024_09_04 } from "@calcom/platform-types";
@ApiExtraModels(SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04)
export class ResponseSlotsOutputData {
@IsNumber()
@ApiProperty()
eventTypeId!: number;
@ValidateNested()
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(SlotsOutput_2024_09_04) },
{ $ref: getSchemaPath(RangeSlotsOutput_2024_09_04) },
],
})
@Type(() => Object)
slots!: SlotsOutput_2024_09_04 | RangeSlotsOutput_2024_09_04;
}
export class ResponseSlotsOutput extends ApiResponseWithoutData {
@ValidateNested()
@ApiProperty({
type: ResponseSlotsOutputData,
})
@Type(() => ResponseSlotsOutputData)
data!: ResponseSlotsOutputData;
}
@@ -1,10 +1,17 @@
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RoutingFormsController } from "@/modules/routing-forms/controllers/routing-forms.controller";
import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository";
import { RoutingFormsService } from "@/modules/routing-forms/services/routing-forms.service";
import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [RoutingFormsRepository],
imports: [PrismaModule, TeamsEventTypesModule, SlotsModule_2024_09_04, StripeModule],
providers: [RoutingFormsRepository, RoutingFormsService, OrganizationsRepository],
controllers: [RoutingFormsController],
exports: [RoutingFormsRepository],
})
export class RoutingFormsModule {}
@@ -0,0 +1,96 @@
import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { Request } from "express";
import { getRoutedUrl } from "@calcom/platform-libraries";
import { ById_2024_09_04_type, GetAvailableSlotsInput_2024_09_04 } from "@calcom/platform-types";
@Injectable()
export class RoutingFormsService {
constructor(
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
private readonly slotsService: SlotsService_2024_09_04
) {}
async calculateSlotsBasedOnRoutingFormResponse(
request: Request,
formId: string,
slotsQuery: GetAvailableSlotsInput_2024_09_04
) {
const eventTypeId = await this.getRoutedEventTypeId(request, formId);
if (!eventTypeId) {
throw new NotFoundException("Event type not found.");
}
const slots = await this.slotsService.getAvailableSlots({
type: ById_2024_09_04_type,
eventTypeId,
...slotsQuery,
});
return {
eventTypeId,
slots,
};
}
private async getRoutedEventTypeId(request: Request, formId: string) {
const routingUrl = await this.getRoutingUrl(request, formId);
if (!this.isEventTypeRedirectUrl(routingUrl)) {
throw new NotFoundException("Routed to a non cal.com event type URL.");
}
const { teamId, eventTypeSlug } = this.extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl);
const eventType = await this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(teamId, eventTypeSlug);
return eventType?.id;
}
private async getRoutingUrl(request: Request, formId: string) {
const params = Object.fromEntries(new URLSearchParams(request.body));
const routedUrlData = await getRoutedUrl({
req: request,
query: { ...params, "cal.isBookingDryRun": "true", form: formId },
});
const destination = routedUrlData?.redirect?.destination;
if (!destination) {
throw new NotFoundException("Route to which the form response should be redirected not found.");
}
return new URL(destination);
}
private extractTeamIdAndEventTypeSlugFromRedirectUrl(routingUrl: URL) {
const eventTypeSlug = this.extractEventTypeFromRoutedUrl(routingUrl);
const teamId = this.extractTeamIdFromRoutedUrl(routingUrl);
if (!teamId) {
throw new NotFoundException("Team ID not found in the routed URL.");
}
if (!eventTypeSlug) {
throw new NotFoundException("Event type slug not found in the routed URL.");
}
return { teamId, eventTypeSlug };
}
private isEventTypeRedirectUrl(routingUrl: URL) {
const routingSearchParams = routingUrl.searchParams;
return routingSearchParams.get("cal.action") === "eventTypeRedirectUrl";
}
private extractTeamIdFromRoutedUrl(routingUrl: URL) {
const routingSearchParams = routingUrl.searchParams;
return Number(routingSearchParams.get("cal.teamId"));
}
private extractEventTypeFromRoutedUrl(routingUrl: URL) {
const pathNameParams = routingUrl.pathname.split("/");
return pathNameParams[pathNameParams.length - 1];
}
}
@@ -49,11 +49,14 @@ describe("Slots 2024-09-04 Endpoints", () => {
const nonOrgUserEmailOne = `slots-2024-09-04-non-org-user-one-${randomString()}@api.com`;
const orgSlug = `slots-2024-09-04-organization-${randomString()}`;
let organization: Team;
const teamSlug = `slots-2024-09-04-organization-team-${randomString()}`;
let team: Team;
let orgUserOne: User;
let orgUserTwo: User;
let collectiveEventTypeId: number;
let collectiveEventTypeSlug: string;
let roundRobinEventTypeId: number;
let collectiveBookingId: number;
let roundRobinBookingId: number;
@@ -91,7 +94,6 @@ describe("Slots 2024-09-04 Endpoints", () => {
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
const orgSlug = `slots-2024-09-04-organization-${randomString()}`;
organization = await organizationsRepositoryFixture.create({
name: orgSlug,
isOrganization: true,
@@ -143,7 +145,8 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
team = await teamRepositoryFixture.create({
name: `slots-2024-09-04-team-${randomString()}`,
name: teamSlug,
slug: teamSlug,
isOrganization: false,
parent: { connect: { id: organization.id } },
});
@@ -178,6 +181,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
},
});
collectiveEventTypeId = collectiveEventType.id;
collectiveEventTypeSlug = collectiveEventType.slug;
const roundRobinEventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "ROUND_ROBIN",
@@ -282,6 +286,34 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
});
it("should get collective team event slots in UTC using teamSlug, eventTypeSlug and organizationSlug", async () => {
return request(app.getHttpServer())
.get(
`/v2/slots?organizationSlug=${orgSlug}&teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(200)
.then(async (response) => {
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const slots = responseBody.data;
expect(slots).toBeDefined();
const days = Object.keys(slots);
expect(days.length).toEqual(5);
expect(slots).toEqual(expectedSlotsUTC);
});
});
it("should not get collective team event slots in UTC using teamSlug, eventTypeSlug if organizationSlug is missing", async () => {
return request(app.getHttpServer())
.get(
`/v2/slots?teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(404);
});
it("should get round robin team event slots in UTC", async () => {
return request(app.getHttpServer())
.get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`)
@@ -54,10 +54,12 @@ describe("Slots 2024-09-04 Endpoints", () => {
let outsider: User;
let outsiderApiKeyString: string;
const teamSlug = `slots-2024-09-04-team-${randomString()}`;
let team: Team;
let teammateOne: User;
let teammateTwo: User;
let collectiveEventTypeId: number;
let collectiveEventTypeSlug: string;
let collectiveEventTypeWithoutHostsId: number;
let roundRobinEventTypeId: number;
let collectiveBookingId: number;
@@ -121,7 +123,8 @@ describe("Slots 2024-09-04 Endpoints", () => {
outsiderApiKeyString = unrelatedUserKeyString;
team = await teamRepositoryFixture.create({
name: `slots-2024-09-04-team-${randomString()}`,
name: teamSlug,
slug: teamSlug,
isOrganization: false,
});
@@ -167,6 +170,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
},
});
collectiveEventTypeId = collectiveEventType.id;
collectiveEventTypeSlug = collectiveEventType.slug;
const collectiveEventTypeWithoutHosts = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
@@ -246,6 +250,25 @@ describe("Slots 2024-09-04 Endpoints", () => {
});
});
it("should get collective team event slots in UTC using teamSlug and eventTypeSlug", async () => {
return request(app.getHttpServer())
.get(
`/v2/slots?teamSlug=${teamSlug}&eventTypeSlug=${collectiveEventTypeSlug}&start=2050-09-05&end=2050-09-09`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
.expect(200)
.then(async (response) => {
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const slots = responseBody.data;
expect(slots).toBeDefined();
const days = Object.keys(slots);
expect(days.length).toEqual(5);
expect(slots).toEqual(expectedSlotsUTC);
});
});
it("should get round robin team event slots in UTC", async () => {
return request(app.getHttpServer())
.get(`/v2/slots?eventTypeId=${roundRobinEventTypeId}&start=2050-09-05&end=2050-09-09`)
@@ -60,7 +60,7 @@ export class SlotsController_2024_09_04 {
@ApiOperation({
summary: "Find out when is an event type ready to be booked.",
description: `
There are 4 ways to get available slots:
There are 4 ways to get available slots for event type of an individual user:
1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'
@@ -70,6 +70,14 @@ export class SlotsController_2024_09_04 {
4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.
And 3 ways to get available slots for team event type:
1. By team event type id. Example '/v2/slots?teamEventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'
2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'
3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'
All of them require "start" and "end" query parameters which define the time range for which available slots should be checked.
Optional parameters are:
- timeZone: Time zone in which the available slots should be returned. Defaults to UTC.
@@ -117,19 +125,28 @@ export class SlotsController_2024_09_04 {
name: "eventTypeSlug",
required: false,
description:
"The slug of the event type for which available slots should be checked. If slug is provided then username must be provided too and if relevant organizationSlug too.",
"The slug of the event type for which available slots should be checked. If slug is provided then username or teamSlug must be provided too and if relevant organizationSlug too.",
example: "event-type-slug",
})
@ApiQuery({
name: "username",
required: false,
description: "The username of the user to get event types for.",
description:
"The username of the user who owns event type with eventTypeSlug - used when slots are checked for individual user event type.",
example: "bob",
})
@ApiQuery({
name: "teamSlug",
required: false,
description:
"The slug of the team who owns event type with eventTypeSlug - used when slots are checked for team event type.",
example: "team-slug",
})
@ApiQuery({
name: "organizationSlug",
required: false,
description: "The slug of the organization to which user with username belongs.",
description:
"The slug of the organization to which user with username belongs or team with teamSlug belongs.",
example: "org-slug",
})
@ApiQuery({
@@ -1,12 +1,23 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { DateTime } from "luxon";
import { dynamicEvent } from "@calcom/platform-libraries";
import { GetSlotsInput_2024_09_04 } from "@calcom/platform-types";
import {
ById_2024_09_04,
ByUsernameAndEventTypeSlug_2024_09_04,
ByTeamSlugAndEventTypeSlug_2024_09_04,
GetSlotsInput_2024_09_04,
ById_2024_09_04_type,
ByUsernameAndEventTypeSlug_2024_09_04_type,
ByTeamSlugAndEventTypeSlug_2024_09_04_type,
} from "@calcom/platform-types";
@Injectable()
export class SlotsInputService_2024_09_04 {
@@ -14,7 +25,10 @@ export class SlotsInputService_2024_09_04 {
private readonly eventTypeRepository: EventTypesRepository_2024_06_14,
private readonly usersRepository: UsersRepository,
private readonly organizationsUsersRepository: OrganizationsUsersRepository,
private readonly organizationsRepository: OrganizationsRepository
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
private readonly organizationsRepository: OrganizationsRepository,
private readonly teamsRepository: TeamsRepository,
private readonly teamsEventTypesRepository: TeamsEventTypesRepository
) {}
async transformGetSlotsQuery(query: GetSlotsInput_2024_09_04) {
@@ -47,11 +61,11 @@ export class SlotsInputService_2024_09_04 {
}
private async getEventType(input: GetSlotsInput_2024_09_04) {
if ("eventTypeId" in input) {
if (input.type === ById_2024_09_04_type) {
return this.eventTypeRepository.getEventTypeById(input.eventTypeId);
}
if ("eventTypeSlug" in input) {
if (input.type === ByUsernameAndEventTypeSlug_2024_09_04_type) {
const user = await this.getEventTypeUser(input);
if (!user) {
throw new NotFoundException(`User with username ${input.username} not found`);
@@ -59,27 +73,48 @@ export class SlotsInputService_2024_09_04 {
return this.eventTypeRepository.getUserEventTypeBySlug(user.id, input.eventTypeSlug);
}
if (input.type === ByTeamSlugAndEventTypeSlug_2024_09_04_type) {
const team = await this.getEventTypeTeam(input);
if (!team) {
throw new NotFoundException(`Team with slug ${input.teamSlug} not found`);
}
return this.teamsEventTypesRepository.getEventTypeByTeamIdAndSlug(team.id, input.eventTypeSlug);
}
return input.duration ? { ...dynamicEvent, length: input.duration } : dynamicEvent;
}
private async getEventTypeUser(input: GetSlotsInput_2024_09_04) {
if ("eventTypeSlug" in input) {
if ("organizationSlug" in input && input.organizationSlug) {
const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug);
if (!organization) {
throw new NotFoundException(
`slots-input.service.ts: Organization with slug ${input.organizationSlug} not found`
);
}
return await this.organizationsUsersRepository.getOrganizationUserByUsername(
organization.id,
input.username
);
}
private async getEventTypeUser(input: ByUsernameAndEventTypeSlug_2024_09_04) {
if (!input.organizationSlug) {
return await this.usersRepository.findByUsername(input.username);
}
const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug);
if (!organization) {
throw new NotFoundException(
`slots-input.service.ts: Organization with slug ${input.organizationSlug} not found`
);
}
return await this.organizationsUsersRepository.getOrganizationUserByUsername(
organization.id,
input.username
);
}
private async getEventTypeTeam(input: ByTeamSlugAndEventTypeSlug_2024_09_04) {
if (!input.organizationSlug) {
return await this.teamsRepository.findTeamBySlug(input.teamSlug);
}
const organization = await this.organizationsRepository.findOrgBySlug(input.organizationSlug);
if (!organization) {
throw new NotFoundException(
`slots-input.service.ts: Organization with slug ${input.organizationSlug} not found`
);
}
return await this.organizationsTeamsRepository.findOrgTeamBySlug(organization.id, input.teamSlug);
}
private adjustStartTime(startTime: string) {
@@ -1,6 +1,7 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { OrganizationsUsersRepository } from "@/modules/organizations/users/index/organizations-users.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SlotsController_2024_09_04 } from "@/modules/slots/slots-2024-09-04/controllers/slots.controller";
@@ -9,12 +10,20 @@ import { SlotsOutputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/
import { SlotsService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots.service";
import { SlotsRepository_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.repository";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, EventTypesModule_2024_06_14, StripeModule, TeamsModule, MembershipsModule],
imports: [
PrismaModule,
EventTypesModule_2024_06_14,
StripeModule,
TeamsModule,
MembershipsModule,
TeamsEventTypesModule,
],
providers: [
SlotsRepository_2024_09_04,
SlotsService_2024_09_04,
@@ -23,6 +32,7 @@ import { Module } from "@nestjs/common";
SlotsOutputService_2024_09_04,
OrganizationsUsersRepository,
OrganizationsRepository,
OrganizationsTeamsRepository,
],
controllers: [SlotsController_2024_09_04],
exports: [SlotsService_2024_09_04],
@@ -110,4 +110,14 @@ export class TeamsRepository {
where: { id: teamId },
});
}
async findTeamBySlug(slug: string) {
return this.dbRead.prisma.team.findFirst({
where: {
slug,
isOrganization: false,
parentId: null,
},
});
}
}
File diff suppressed because it is too large Load Diff
@@ -1,14 +1,5 @@
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { SkeletonLoader } from "@calcom/features/webhooks/pages/webhooks-view";
import { SkeletonLoader } from "@calcom/features/webhooks/pages/webhooks-skeleton";
export default function Loading() {
return (
<SettingsHeader
title="Webhooks"
description="Receive meeting data in real-time when something happens in Cal.com"
borderInShellHeader={true}
CTA={null}>
<SkeletonLoader />
</SettingsHeader>
);
return <SkeletonLoader />;
}
@@ -1,11 +1,9 @@
import { createRouterCaller } from "app/_trpc/context";
import { _generateMetadata, getTranslate } from "app/_utils";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { CreateNewWebhookButton } from "@calcom/features/webhooks/components";
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
import { APP_NAME } from "@calcom/lib/constants";
import { UserPermissionRole } from "@calcom/prisma/enums";
@@ -25,20 +23,11 @@ const WebhooksViewServerWrapper = async () => {
redirect("/auth/login");
}
const t = await getTranslate();
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
const caller = await createRouterCaller(webhookRouter);
const data = await caller.getByViewer();
return (
<SettingsHeader
title={t("webhooks")}
description={t("add_webhook_description", { appName: APP_NAME })}
CTA={data && data.webhookGroups.length > 0 ? <CreateNewWebhookButton isAdmin={isAdmin} /> : null}
borderInShellHeader={(data && data.profiles.length === 1) || !data?.webhookGroups?.length}>
<WebhooksView data={data} isAdmin={isAdmin} />
</SettingsHeader>
);
return <WebhooksView data={data} isAdmin={isAdmin} />;
};
export default WebhooksViewServerWrapper;
@@ -0,0 +1,5 @@
import { SkeletonLoader } from "@calcom/features/webhooks/pages/webhook-new-skeleton";
export default function Loading() {
return <SkeletonLoader />;
}
@@ -1,9 +1,10 @@
import { getTranslate, _generateMetadata } from "app/_utils";
import { Suspense } from "react";
import { createRouterCaller } from "app/_trpc/context";
import { _generateMetadata } from "app/_utils";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { NewWebhookView, SkeletonLoader } from "@calcom/features/webhooks/pages/webhook-new-view";
import { NewWebhookView } from "@calcom/features/webhooks/pages/webhook-new-view";
import { APP_NAME } from "@calcom/lib/constants";
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -12,19 +13,17 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const t = await getTranslate();
const [appsCaller, webhookCaller] = await Promise.all([
createRouterCaller(appsRouter),
createRouterCaller(webhookRouter),
]);
return (
<SettingsHeader
title={t("add_webhook")}
description={t("add_webhook_description", { appName: APP_NAME })}
borderInShellHeader={true}
backButton>
<Suspense fallback={<SkeletonLoader />}>
<NewWebhookView />
</Suspense>
</SettingsHeader>
);
const [installedApps, webhooks] = await Promise.all([
appsCaller.integrations({ variant: "other", onlyInstalled: true }),
webhookCaller.list(),
]);
return <NewWebhookView webhooks={webhooks} installedApps={installedApps} />;
};
export default Page;
@@ -10,7 +10,7 @@ import dayjs from "@calcom/dayjs";
import "@calcom/dayjs/locales";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import { formatTime } from "@calcom/lib/date-fns";
import { formatTime } from "@calcom/lib/dayjs";
import { getPaymentAppData } from "@calcom/lib/getPaymentAppData";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -97,7 +97,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
} as const;
}
// If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
const enrichedBookingUser = booking.user
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
: null;
const eventUrl = await buildEventUrlFromBooking({
eventType,
dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null,
profileEnrichedBookingUser: enrichedBookingUser,
});
// If booking is already REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
// If the booking is CANCELLED and allowRescheduleForCancelledBooking is false, we redirect the user to the original event link.
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
if (
@@ -107,7 +120,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
) {
return {
redirect: {
destination: `/booking/${uid}`,
destination: booking.status === BookingStatus.CANCELLED ? eventUrl : `/booking/${uid}`,
permanent: false,
},
};
@@ -122,18 +135,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
const enrichedBookingUser = booking.user
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
: null;
const eventUrl = await buildEventUrlFromBooking({
eventType,
dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null,
profileEnrichedBookingUser: enrichedBookingUser,
});
const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date();
if (isBookingInPast && !eventType.allowReschedulingPastBookings) {
const destinationUrlSearchParams = new URLSearchParams();
@@ -184,6 +185,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
destinationUrlSearchParams.set("rescheduleUid", seatReferenceUid || bookingUid);
if (allowRescheduleForCancelledBooking) {
destinationUrlSearchParams.set("allowRescheduleForCancelledBooking", "true");
}
// TODO: I think we should just forward all the query params here including coep flag
if (coepFlag) {
destinationUrlSearchParams.set("flag.coep", coepFlag as string);
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
@@ -9,7 +10,7 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { User } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/client";
import { BookingStatus, RedirectType } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
@@ -24,6 +25,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const session = await getServerSession({ req });
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(params);
const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting, email } = query;
const allowRescheduleForCancelledBooking = query.allowRescheduleForCancelledBooking === "true";
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug);
const isOrgContext = currentOrgDomain && isValidOrgDomain;
@@ -61,7 +63,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const orgSlug = isValidOrgDomain ? currentOrgDomain : null;
const name = team.parent?.name ?? team.name ?? null;
const booking = rescheduleUid ? await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id) : null;
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
if (booking?.status === BookingStatus.CANCELLED && !allowRescheduleForCancelledBooking) {
return {
redirect: {
permanent: false,
destination: `/team/${teamSlug}/${meetingSlug}`,
},
};
}
}
const fromRedirectOfNonOrgLink = context.query.orgRedirection === "true";
const isUnpublished = team.parent ? !team.parent.slug : !team.slug;
-2
View File
@@ -217,8 +217,6 @@ export const config = {
"/org/:orgSlug",
"/team/:slug/:type",
"/team/:slug",
"/:user/:type",
"/:user",
],
};
@@ -29,7 +29,7 @@ export default function Apps({ apps, category }: CategoryDataProps) {
</Link>
{category && (
<span className="text-default gap-1">
<span>{t("slash_separator")}</span>
<span>&nbsp;/&nbsp;</span>
{t("category_apps", { category: category[0].toUpperCase() + category?.slice(1) })}
</span>
)}
@@ -216,9 +216,9 @@ export const AvailabilityCTA = () => {
}
return (
<div className="flex gap-2">
<div className="flex items-center gap-2">
<ToggleGroup
className="min-h-10 hidden md:flex md:items-center"
className="hidden h-fit md:block"
defaultValue={searchParams?.get("type") ?? "mine"}
onValueChange={(value) => {
if (!value) return;
@@ -32,7 +32,7 @@ import {
formatToLocalizedDate,
formatToLocalizedTime,
formatToLocalizedTimezone,
} from "@calcom/lib/date-fns";
} from "@calcom/lib/dayjs";
import type { nameObjectSchema } from "@calcom/lib/event";
import { getEventName } from "@calcom/lib/event";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
@@ -6,7 +6,7 @@ import { Controller, useForm } from "react-hook-form";
import { TimezoneSelect } from "@calcom/features/components/timezone-select";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { formatLocalizedDateTime } from "@calcom/lib/date-fns";
import { formatLocalizedDateTime } from "@calcom/lib/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localeOptions } from "@calcom/lib/i18n";
import { nameOfDay } from "@calcom/lib/weekday";
+1 -1
View File
@@ -72,7 +72,7 @@ vi.mock("@calcom/lib/constants", () => {
};
});
vi.mock("@calcom/lib/date-fns", () => {
vi.mock("@calcom/lib/dayjs", () => {
return {};
});
@@ -8,7 +8,7 @@ import { useState, useEffect, useRef } from "react";
import dayjs from "@calcom/dayjs";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { TRANSCRIPTION_STOPPED_ICON, RECORDING_DEFAULT_ICON } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.2.0",
"version": "5.2.1",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+39 -2
View File
@@ -246,9 +246,13 @@ test.describe("pro user", () => {
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
await expect(cancelledHeadline).toBeVisible();
const bookingCancelledId = new URL(page.url()).pathname.split("/booking/")[1];
const { slug: eventSlug } = await pro.getFirstEventAsOwner();
await page.goto(`/reschedule/${bookingCancelledId}`);
// Should be redirected to the booking details page which shows the cancelled headline
await expect(page.locator('[data-testid="cancelled-headline"]')).toBeVisible();
// Should be redirected to the original event link
await expect(page).toHaveURL(new RegExp(`/${pro.username}/${eventSlug}`));
});
test("can book an event that requires confirmation and then that booking can be accepted by organizer", async ({
@@ -654,3 +658,36 @@ test.describe("Event type with disabled cancellation and rescheduling", () => {
expect(responseBody.message).toBe("This event type does not allow cancellations");
});
});
test("Should throw error when both seatsPerTimeSlot and recurringEvent are set", async ({ page, users }) => {
const user = await users.create({
name: `Test-user-${randomString(4)}`,
eventTypes: [
{
title: "Seats With Recurrence",
slug: "seats-with-recurrence",
length: 30,
seatsPerTimeSlot: 3,
recurringEvent: {
freq: 1,
count: 4,
interval: 1,
},
},
],
});
// Way to book the event
await page.goto(`/${user.username}/seats-with-recurrence`);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
page.locator("[data-testid=confirm-book-button]").click();
// Expect an error message to be displayed
const alertError = page.locator("[data-testid=booking-fail]");
await expect(alertError).toBeVisible();
await expect(alertError).toContainText(
"Could not book the meeting. Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring."
);
});
+33
View File
@@ -387,6 +387,39 @@ test.describe("Event Types tests", () => {
await unCheckDisplayLocation(page);
});
});
test("Should not allow enabling both recurring event and offer seats at the same time", async ({
page,
}) => {
const nonce = randomString(3);
const eventTitle = `Conflict event ${nonce}`;
await createNewEventType(page, { eventTitle });
await page.goto("/event-types");
await page.click(`text=${eventTitle}`);
// Go to Advanced tab and enable offerSeats
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
const offerSeatsToggle = page.locator("[data-testid=offer-seats-toggle]");
await offerSeatsToggle.click();
// Try enabling recurring - should be disabled
await page.click("[data-testid=vertical-tab-recurring]");
const recurringEventToggle = page.locator("[data-testid=recurring-event-check]");
await expect(recurringEventToggle).toBeDisabled();
// Go back and disable offerSeats
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
await offerSeatsToggle.click(); // turn it off
// Enable recurring now
await page.click("[data-testid=vertical-tab-recurring]");
await recurringEventToggle.click();
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeVisible();
// After enabling recurring, offerSeats should now be disabled
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
await expect(offerSeatsToggle).toBeDisabled();
});
});
});
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "الحجوزات الأكثر إلغاءً",
"name_or_email": "الاسم أو البريد الإلكتروني",
"salesforce_round_robin_skip_fallback_to_lead_owner": "إذا لم يتم العثور على جهة اتصال، يتم الرجوع إلى مالك العميل المحتمل إذا كان موجودًا",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "التاريخ والوقت",
"email_reminder_upcoming_event_notice" : "هذا تذكير بالحدث القادم.",
"email_reminder_triggered_by_workflow" : "تم تشغيل هذا التذكير بواسطة سير العمل في Cal.",
"event_upper_case": "حدث",
"scheduling_by": "جدولة بواسطة",
"experience_review_prompt": "كيف كانت تجربتك الأخيرة؟",
"improve_customer_experience_message": "نحن نعمل دائمًا على تحسين تجربة عملائنا.",
"meeting_satisfaction_question": "ما مدى رضاك عن اجتماعك الأخير؟",
"meeting_not_joined_question": "لم تحضر الاجتماع؟",
"reschedule_cta_short": "أعد الجدولة من هنا",
"you_and_conjunction": "& أنت",
"email_survey_triggered_by_workflow": "تم تشغيل هذا الاستبيان بواسطة سير العمل في كال."
}
+12 -1
View File
@@ -3053,5 +3053,16 @@
"most_cancelled_bookings": "Ən çox ləğv edilmiş rezervasiyalar",
"name_or_email": "Ad və ya E-poçt",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Əgər əlaqə tapılmazsa, mövcud olduğu təqdirdə potensial müştəri sahibinə qayıt",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Tarix & Saat",
"email_reminder_upcoming_event_notice" : "Bu, qarşıdan gələn tədbiriniz haqqında xatırlatmadır.",
"email_reminder_triggered_by_workflow" : "Bu xatırladıcı Calda iş axını ilə işə salınıb.",
"event_upper_case": "Hadisə",
"scheduling_by": "Təqvimləşdirmə edən",
"experience_review_prompt": "Son təcrübəniz necə oldu?",
"improve_customer_experience_message": "Həmişə müştəri təcrübəsini yaxşılaşdırmağa çalışırıq.",
"meeting_not_joined_question": "görüşə qoşulmadınız?",
"reschedule_cta_short": "Buradan yenidən təyin edin.",
"you_and_conjunction": "Siz &",
"email_survey_triggered_by_workflow": "Bu sorğu Cal-da Workflow tərəfindən başladıldı."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Най-много отменени резервации",
"name_or_email": "Име или имейл",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ако не е намерен контакт, връщане към собственика на лийда, ако съществува",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Дата и час",
"email_reminder_upcoming_event_notice" : "Това е напомняне за предстоящото ви събитие.",
"email_reminder_triggered_by_workflow" : "Това напомняне е задействано от работен процес в Cal.",
"event_upper_case": "Събитие",
"scheduling_by": "График от",
"experience_review_prompt": "Как бе вашето последно преживяване?",
"improve_customer_experience_message": "Винаги се стремим да подобрим клиентското изживяване.",
"meeting_satisfaction_question": "Колко сте доволни от последната си среща?",
"meeting_not_joined_question": "не присъствахте на срещата?",
"reschedule_cta_short": "Пренасрочете тук.",
"you_and_conjunction": "Вие &",
"email_survey_triggered_by_workflow": "Тази анкета беше стартирана от Workflow в Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Reserves més cancel·lades",
"name_or_email": "Nom o correu electrònic",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no es troba cap contacte, recorre al propietari del lead si existeix",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Data i hora",
"email_reminder_upcoming_event_notice": "Aquest és un recordatori del vostre proper esdeveniment.",
"email_reminder_triggered_by_workflow": "Aquest recordatori s'ha activat per un flux de treball a Cal.",
"event_upper_case": "Esdeveniment",
"scheduling_by": "Planificat per",
"experience_review_prompt": "Com ha estat la vostra experiència recent?",
"improve_customer_experience_message": "Sempre millorem l'experiència dels nostres clients.",
"meeting_satisfaction_question": "Què satisfet esteu amb la vostra última reunió?",
"meeting_not_joined_question": "no vas assistir a la reunió?",
"reschedule_cta_short": "Reprograma aquí.",
"you_and_conjunction": "Vosaltres &",
"email_survey_triggered_by_workflow": "Aquesta enquesta va ser activada per un Workflow a Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Nejčastěji zrušené rezervace",
"name_or_email": "Jméno nebo e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Pokud není nalezen žádný kontakt, použít vlastníka leadu, pokud existuje",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Datum a čas",
"email_reminder_upcoming_event_notice" : "Toto je připomínka vaší nadcházející události.",
"email_reminder_triggered_by_workflow" : "Toto připomenutí bylo spuštěno pracovním postupem v Cal.",
"event_upper_case": "Událost",
"scheduling_by": "Plánování podle",
"experience_review_prompt": "Jaké byly vaše nedávné zkušenosti?",
"improve_customer_experience_message": "Neustále pracujeme na zlepšování zákaznických zkušeností.",
"meeting_satisfaction_question": "Jak jste spokojeni s vaším posledním jednáním?",
"meeting_not_joined_question": "nejste se zúčastnili schůzky?",
"reschedule_cta_short": "Přeplánujte zde.",
"you_and_conjunction": "Vy &",
"email_survey_triggered_by_workflow": "Tento průzkum byl spuštěn pracovním postupem v Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Flest annullerede bookinger",
"name_or_email": "Navn eller e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Hvis ingen kontakt findes, fald tilbage til lead-ejeren, hvis denne eksisterer",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Dato og tid",
"email_reminder_upcoming_event_notice" : "Dette er en påmindelse om din kommende begivenhed.",
"email_reminder_triggered_by_workflow" : "Denne påmindelse blev udløst af en arbejdsgang i Cal.",
"event_upper_case": "Begivenhed",
"scheduling_by": "Planlægning af",
"experience_review_prompt": "Hvordan var din seneste oplevelse?",
"improve_customer_experience_message": "Vi arbejder altid på at forbedre kundeoplevelsen.",
"meeting_satisfaction_question": "Hvor tilfreds var du med dit seneste møde?",
"meeting_not_joined_question": "deltog ikke i mødet?",
"reschedule_cta_short": "Planlæg igen her.",
"you_and_conjunction": "Du &",
"email_survey_triggered_by_workflow": "Denne undersøgelse blev udløst af en Arbejdsgang i Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Am häufigsten abgesagte Termine",
"name_or_email": "Name oder E-Mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Wenn kein Kontakt gefunden wird, auf den Lead-Eigentümer zurückgreifen, falls vorhanden",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Datum und Uhrzeit",
"email_reminder_upcoming_event_notice" : "Dies ist eine Erinnerung an Ihre bevorstehende Veranstaltung.",
"email_reminder_triggered_by_workflow" : "Diese Erinnerung wurde durch einen Workflow in Cal ausgelöst.",
"event_upper_case": "Ereignis",
"scheduling_by": "Terminplanung durch",
"experience_review_prompt": "Wie war Ihre letzte Erfahrung?",
"improve_customer_experience_message": "Wir arbeiten ständig daran, das Kundenerlebnis zu verbessern.",
"meeting_satisfaction_question": "Wie zufrieden waren Sie mit Ihrer letzten Besprechung?",
"meeting_not_joined_question": "nicht am Meeting teilgenommen?",
"reschedule_cta_short": "Hier neu planen.",
"you_and_conjunction": "Sie &",
"email_survey_triggered_by_workflow": "Diese Umfrage wurde durch einen Workflow in Cal ausgelöst."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Περισσότερες ακυρωμένες κρατήσεις",
"name_or_email": "Όνομα ή Email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Αν δεν βρεθεί επαφή, επιστροφή στον ιδιοκτήτη του lead εάν υπάρχει",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Ημερομηνία & Ώρα",
"email_reminder_upcoming_event_notice" : "Αυτή είναι μια υπενθύμιση για την επερχόμενη εκδήλωσή σας.",
"email_reminder_triggered_by_workflow" : "Αυτή η υπενθύμιση ενεργοποιήθηκε από μια ροή εργασίας στο Cal.",
"event_upper_case": "Γεγονός",
"scheduling_by": "Προγραμματισμός από",
"experience_review_prompt": "Πώς ήταν η πρόσφατη εμπειρία σας;",
"improve_customer_experience_message": "Προσπαθούμε συνεχώς να βελτιώνουμε την εμπειρία των πελατών.",
"meeting_satisfaction_question": "Πόσο ικανοποιημένοι ήσασταν με την τελευταία σας συνάντηση;",
"meeting_not_joined_question": "δεν συμμετείχατε στη συνάντηση;",
"reschedule_cta_short": "Επαναπρογραμματίστε εδώ.",
"you_and_conjunction": "Εσείς &",
"email_survey_triggered_by_workflow": "Αυτή η έρευνα ενεργοποιήθηκε από μια ροή εργασίας στο Cal."
}
+22 -1
View File
@@ -1869,6 +1869,9 @@
"wrong_code": "Wrong verification code",
"not_verified": "Not yet verified",
"no_availability_in_month": "No availability in {{month}}",
"no_availability_rolling": "Scheduling is only available up to {{days}} in advance. Please check again soon.",
"no_availability_range":
"Scheduling ended on {{date}}. Please check again soon.",
"view_previous_month": "View previous month",
"view_next_month": "View next month",
"send_code": "Send code",
@@ -3050,6 +3053,9 @@
"tip_username_plus": "Tip: You can a '+' between usernames: cal.com/anna+brian to make a dynamic group meeting",
"user_has_no_team_yet": "You don't have a team yet",
"no_team_members": "You don't have team members yet",
"recurring_event_doesnt_support_seats": "Recurring event doesn't support seats feature. Make it non-recurring to enable seats.",
"seats_doesnt_support_recurring": "Seats feature doesn't support recurring event. Disable seats feature to make it recurring.",
"recurring_event_seats_error": "Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring.",
"segment": "Segment",
"rename": "Rename",
"rename_segment": "Rename Segment",
@@ -3098,7 +3104,6 @@
"oauth_client_not_found": "OAuth Client not found.",
"subscribe_to_platform": "Subscribe to Platform",
"error_accessing_webhooks": "Error while trying to access webhooks.",
"slash_separator": "&nbsp;/&nbsp;",
"webhook_update_failed": "Failed to update webhook.",
"webhook_create_failed": "Failed to create webhook.",
"invalid_link": "Invalid Link",
@@ -3119,5 +3124,21 @@
"resend_in_seconds": "Resend in {{seconds}} seconds",
"resend": "Resend",
"verification_email_sent": "Verification email sent",
"remove_whitelist_status": "Remove whitelist status",
"whitelist_user_workflows": "Whitelist user workflows",
"user_workflows_whitelisted": "User workflows whitelisted",
"user_workflows_unwhitelisted": "User workflows unwhitelisted",
"date_and_time": "Date & Time",
"email_reminder_upcoming_event_notice": "This is a reminder about your upcoming event.",
"email_reminder_triggered_by_workflow": "This reminder was triggered by a Workflow in Cal.",
"event_upper_case": "Event",
"scheduling_by": "Scheduling by",
"experience_review_prompt": "How was your recent experience?",
"improve_customer_experience_message": "We're always looking to improve our customer's experience.",
"meeting_satisfaction_question": "How satisfied were you with your recent meeting?",
"meeting_not_joined_question": "didn't join the meeting?",
"reschedule_cta_short": "Reschedule here.",
"you_and_conjunction": "You &",
"email_survey_triggered_by_workflow": "This survey was triggered by a Workflow in Cal.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Reservas más canceladas",
"name_or_email": "Nombre o correo electrónico",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no se encuentra ningún contacto, recurrir al propietario del lead si existe",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Fecha y hora",
"email_reminder_upcoming_event_notice" : "Este es un recordatorio sobre tu próximo evento.",
"email_reminder_triggered_by_workflow" : "Este recordatorio fue activado por un Workflow en Cal.",
"event_upper_case": "Evento",
"scheduling_by": "Programado por",
"experience_review_prompt": "¿Cómo fue tu última experiencia?",
"improve_customer_experience_message": "Siempre buscamos mejorar la experiencia del cliente.",
"meeting_satisfaction_question": "¿Qué tan satisfecho estuviste con tu última reunión?",
"meeting_not_joined_question": "¿no asististe a la reunión?",
"reschedule_cta_short": "Reprograma aquí.",
"you_and_conjunction": "Tú &",
"email_survey_triggered_by_workflow": "Esta encuesta se activó mediante un Workflow en Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Reservas más canceladas",
"name_or_email": "Nombre o correo electrónico",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no se encuentra ningún contacto, recurrir al propietario del lead si existe",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Fecha y hora",
"email_reminder_upcoming_event_notice" : "Este es un recordatorio sobre su próximo evento.",
"email_reminder_triggered_by_workflow" : "Este recordatorio fue activado por un flujo de trabajo en Cal.",
"event_upper_case": "Evento",
"scheduling_by": "Programado por",
"experience_review_prompt": "¿Cómo fue tu experiencia reciente?",
"improve_customer_experience_message": "Siempre buscamos mejorar la experiencia de nuestros clientes.",
"meeting_satisfaction_question": "¿Qué tan satisfecho estuvo con su última reunión?",
"meeting_not_joined_question": "¿no asististe a la reunión?",
"reschedule_cta_short": "Reprogramar aquí.",
"you_and_conjunction": "Tú &",
"email_survey_triggered_by_workflow": "Esta encuesta fue activada por un Workflow en Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Enim tühistatud broneeringud",
"name_or_email": "Nimi või e-post",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Kui kontakti ei leita, kasuta võimalusel liidi omanikku",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Kuupäev ja kellaaeg",
"email_reminder_upcoming_event_notice" : "See on meeldetuletus teie eelseisva sündmuse kohta.",
"email_reminder_triggered_by_workflow" : "Selle meeldetuletuse käivitas Cali töövoog.",
"event_upper_case": "Sündmus",
"scheduling_by": "Plaanitud",
"experience_review_prompt": "Kuidas oli teie hiljutine kogemus?",
"improve_customer_experience_message": "Me püüame pidevalt klindikogemust parandada.",
"meeting_satisfaction_question": "Kui rahul olite oma viimase kohtumisega?",
"meeting_not_joined_question": "ei osalenud koosolekul?",
"reschedule_cta_short": "Broneeri uuesti siit.",
"you_and_conjunction": "Sina &",
"email_survey_triggered_by_workflow": "See küsitlus käivitati Cali töövooga."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Bertan behera utzitako erreserba gehienak",
"name_or_email": "Izena edo helbide elektronikoa",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Kontakturik aurkitzen ez bada, itzuli bezeroen arduradunera existitzen bada",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Data eta ordua",
"email_reminder_upcoming_event_notice" : "Hau zure hurrengo ekitaldiari buruzko abisua da.",
"email_reminder_triggered_by_workflow" : "Abisu hau Cal-eko lan-fluxu batek abiarazi zuen.",
"event_upper_case": "Gertaera",
"scheduling_by": "Antolatzailea",
"experience_review_prompt": "Zein izan da zure azken esperientzia?",
"improve_customer_experience_message": "Bezeroaren esperientzia hobetzen saiatzen gara beti.",
"meeting_satisfaction_question": "Zenbat pozik zaude azken bilerarekin?",
"meeting_not_joined_question": "ez zenuen bileran parte hartu?",
"reschedule_cta_short": "Berriz programatu hemen.",
"you_and_conjunction": "Zu &",
"email_survey_triggered_by_workflow": "Galdeketa hau Cal-eko Workflow baten bidez aktibatu da."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Eniten perutut varaukset",
"name_or_email": "Nimi tai sähköposti",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Jos yhteyshenkilöä ei löydy, käytä liidin omistajaa, jos sellainen on olemassa",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lisää uudet merkkijonot tämän yläpuolelle ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lisää uudet merkkijonot tämän yläpuolelle ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Päivämäärä ja aika",
"email_reminder_upcoming_event_notice" : "Tämä on muistutus tulevasta tapahtumastasi.",
"email_reminder_triggered_by_workflow" : "Tämän muistutuksen laukaisi Cal.",
"event_upper_case": "Tapahtuma",
"scheduling_by": "Aikatauluttaja",
"experience_review_prompt": "Minkälainen oli viimeisin kokemuksesi?",
"improve_customer_experience_message": "Pyrimme jatkuvasti parantamaan asiakaskokemusta.",
"meeting_satisfaction_question": "Kuinka tyytyväinen olit viime kokoukseen?",
"meeting_not_joined_question": "et osallistunut kokoukseen?",
"reschedule_cta_short": "Uudelleenaikatauluta tästä.",
"you_and_conjunction": "Sinä &",
"email_survey_triggered_by_workflow": "Tämä kysely käynnistettiin Workflow-ohjelmalla Cal-palvelussa."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Réservations les plus annulées",
"name_or_email": "Nom ou e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si aucun contact n'est trouvé, se rabattre sur le propriétaire du lead s'il existe",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time" : "Date et heure",
"email_reminder_upcoming_event_notice" : "Ceci est un rappel concernant votre événement à venir.",
"email_reminder_triggered_by_workflow" : "Ce rappel a été déclenché par un workflow dans Cal.",
"event_upper_case": "Événement",
"scheduling_by": "Planifié par",
"experience_review_prompt": "Comment s'est déroulée votre dernière expérience ?",
"improve_customer_experience_message": "Nous cherchons toujours à améliorer l'expérience client.",
"meeting_satisfaction_question": "Dans quelle mesure étiez-vous satisfait de votre dernière réunion ?",
"meeting_not_joined_question": "vous n'avez pas participé à la réunion ?",
"reschedule_cta_short": "Reprogrammer ici.",
"you_and_conjunction": "Vous &",
"email_survey_triggered_by_workflow": "Ce questionnaire a été déclenché par un Workflow dans Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "הזמנות שבוטלו הכי הרבה",
"name_or_email": "שם או אימייל",
"salesforce_round_robin_skip_fallback_to_lead_owner": "אם לא נמצא איש קשר, חזור לבעל הליד אם הוא קיים",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "תאריך ושעה",
"email_reminder_upcoming_event_notice": "זאת תזכורת לגבי האירוע הקרוב שלך.",
"email_reminder_triggered_by_workflow": "תזכורת זו הופעלה על ידי Workflow ב-Cal.",
"event_upper_case": "אירוע",
"scheduling_by": "תזמון על ידי",
"experience_review_prompt": "איך הייתה החוויה האחרונה שלך?",
"improve_customer_experience_message": ".אנחנו תמיד משפרים את חווית הלקוח",
"meeting_satisfaction_question": "?עד כמה היית מרוצה מהפגישה האחרונה שלך",
"meeting_not_joined_question": "?לא הצטרפת לפגישה",
"reschedule_cta_short": ".קבע מחדש כאן",
"you_and_conjunction": "את/ה &",
"email_survey_triggered_by_workflow": "הסקר הזה הופעל על ידי Workflow ב-Cal."
}
+13 -1
View File
@@ -440,5 +440,17 @@
"ooo_select_reason": "Odaberi razlog",
"enterprise_license_locally": "Možete testirati ovu značajku lokalno, ali ne u proizvodnji.",
"enterprise_license_sales": "Da biste se nadogradili na poslovnu verziju, obratite se našem timu za prodaju. Ako je već licencni ključ na mjestu, kontaktirajte support@cal.com za pomoć.",
"apply_to_all_event_types": "Primjenjuje se na sve, uključujući buduće vrste događaja"
"apply_to_all_event_types": "Primjenjuje se na sve, uključujući buduće vrste događaja",
"date_and_time": "Datum i vrijeme",
"email_reminder_upcoming_event_notice": "Ovo je podsjetnik za vaš nadolazeći događaj.",
"email_reminder_triggered_by_workflow": "Ovaj podsjetnik je pokrenut putem Workflowa u Calu.",
"event_upper_case": "Događaj",
"scheduling_by": "Zakazano od strane",
"experience_review_prompt": "Kako je bilo vaše nedavno iskustvo?",
"improve_customer_experience_message": "Stalno poboljšavamo iskustvo korisnika.",
"meeting_satisfaction_question": "Koliko ste zadovoljni svojim posljednjim sastankom?",
"meeting_not_joined_question": "niste prisustvovali sastanku?",
"reschedule_cta_short": "Zakažite ponovno ovdje.",
"you_and_conjunction": "Vi &",
"email_survey_triggered_by_workflow": "Ova anketa je pokrenuta Workflow-om u Calu."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Legtöbbször lemondott foglalások",
"name_or_email": "Név vagy e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ha nem található kapcsolat, visszalépés a lead tulajdonoshoz, ha létezik",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Dátum és idő",
"email_reminder_upcoming_event_notice": "Ez egy emlékeztető a közelgő eseményedről.",
"email_reminder_triggered_by_workflow": "Ezt az emlékeztetőt egy Cal-beli Workflow indította.",
"event_upper_case": "Esemény",
"scheduling_by": "Ütemezve által",
"experience_review_prompt": "Milyen volt a legutóbbi élményed?",
"improve_customer_experience_message": "Folyamatosan javítjuk az ügyfélélményt.",
"meeting_satisfaction_question": "Mennyire elégedett volt a legutóbbi találkozójával?",
"meeting_not_joined_question": "nem vett részt a találkozón?",
"reschedule_cta_short": "Újraütemezés itt.",
"you_and_conjunction": "Te &",
"email_survey_triggered_by_workflow": "Ezt a kérdőívet egy Workflow indította a Cal-ban."
}
+13 -1
View File
@@ -153,5 +153,17 @@
"ooo_select_reason": "Pilih alasan",
"enterprise_license_locally": "Anda dapat menguji fitur ini secara lokal tetapi tidak pada produksi.",
"enterprise_license_sales": "Untuk meningkatkan ke edisi perusahaan, hubungi tim penjualan kami. Jika kunci lisensi sudah ada, hubungi support@cal.com untuk bantuan.",
"apply_to_all_event_types": "Terapkan untuk semua, termasuk jenis acara di masa depan"
"apply_to_all_event_types": "Terapkan untuk semua, termasuk jenis acara di masa depan",
"date_and_time": "Tanggal & Waktu",
"email_reminder_upcoming_event_notice": "Ini adalah pengingat tentang acara mendatang Anda.",
"email_reminder_triggered_by_workflow": "Pengingat ini dipicu oleh Workflow di Cal.",
"event_upper_case": "Acara",
"scheduling_by": "Dijadwalkan oleh",
"experience_review_prompt": "Bagaimana pengalaman terakhir Anda?",
"improve_customer_experience_message": "Kami selalu berusaha meningkatkan pengalaman pelanggan.",
"meeting_satisfaction_question": "Seberapa puas Anda dengan rapat terakhir Anda?",
"meeting_not_joined_question": "tidak bergabung dengan rapat?",
"reschedule_cta_short": "Jadwal ulang di sini.",
"you_and_conjunction": "Anda &",
"email_survey_triggered_by_workflow": "Survei ini dipicu oleh Workflow di Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Prenotazioni più cancellate",
"name_or_email": "Nome o email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Se non viene trovato alcun contatto, passa al proprietario del lead se esiste",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Data e ora",
"email_reminder_upcoming_event_notice": "Questo è un promemoria per il tuo prossimo evento.",
"email_reminder_triggered_by_workflow": "Questo promemoria è stato attivato da un Workflow in Cal.",
"event_upper_case": "Evento",
"scheduling_by": "Pianificato da",
"experience_review_prompt": "Come è stata la tua recente esperienza?",
"improve_customer_experience_message": "Cerchiamo sempre di migliorare l'esperienza dei clienti.",
"meeting_satisfaction_question": "Quanto sei soddisfatto del tuo ultimo incontro?",
"meeting_not_joined_question": "non hai partecipato all'incontro?",
"reschedule_cta_short": "Riprogramma qui.",
"you_and_conjunction": "Tu &",
"email_survey_triggered_by_workflow": "Questo sondaggio è stato attivato da un Workflow in Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "最もキャンセルされた予約",
"name_or_email": "名前またはメールアドレス",
"salesforce_round_robin_skip_fallback_to_lead_owner": "連絡先が見つからない場合、リード所有者が存在すればそちらにフォールバックする",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "日付と時刻",
"email_reminder_upcoming_event_notice": "これは今後のイベントについてのお知らせです。",
"email_reminder_triggered_by_workflow": "このリマインダーはCalのワークフローによってトリガーされました。",
"event_upper_case": "イベント",
"scheduling_by": "スケジュール担当",
"experience_review_prompt": "最近の体験はいかがでしたか?",
"improve_customer_experience_message": "私たちは常にお客様の体験向上に努めています。",
"meeting_satisfaction_question": "直近のミーティングに満足されましたか?",
"meeting_not_joined_question": "ミーティングに参加していませんか?",
"reschedule_cta_short": "こちらから再スケジュール",
"you_and_conjunction": "あなた &",
"email_survey_triggered_by_workflow": "このアンケートはCalのワークフローによってトリガーされました。"
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "ការកក់ដែលត្រូវបានលុបចោលច្រើនបំផុត",
"name_or_email": "ឈ្មោះ ឬអ៊ីមែល",
"salesforce_round_robin_skip_fallback_to_lead_owner": "ប្រសិនបើមិនមានទំនាក់ទំនងត្រូវបានរកឃើញទេ ត្រឡប់ទៅម្ចាស់ Lead ប្រសិនបើមាន",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "កាលបរិច្ឆេទ និងពេលវេលា",
"email_reminder_upcoming_event_notice": "នេះគឺជាការរំលឹកអំពីព្រឹត្តិការណ៍ដែលនឹងមានឡើងរបស់អ្នក។",
"email_reminder_triggered_by_workflow": "ការរំលឹកនេះត្រូវបានបង្កើតឡើងដោយលំហូរការងារនៅក្នុង Cal។",
"event_upper_case": "ព្រឹត្តិការណ៍",
"scheduling_by": "ការកំណត់ពេលវេលាដោយ",
"experience_review_prompt": "តើបទពិសោធន៍ថ្មីៗរបស់អ្នកយ៉ាងណា?",
"improve_customer_experience_message": "យើងតែងតែរកមធ្យោបាយកែលម្អបទពិសោធន៍អតិថិជន។",
"meeting_satisfaction_question": "តើអ្នកពេញចិត្តប៉ុណ្ណាជាមួយការប្រជុំថ្មីៗរបស់អ្នក?",
"meeting_not_joined_question": "មិនបានចូលរួមការប្រជុំ?",
"reschedule_cta_short": "កំណត់ពេលវេលាថ្មីនៅទីនេះ។",
"you_and_conjunction": "អ្នក &",
"email_survey_triggered_by_workflow": "ការស្ទង់មតិនេះត្រូវបានបង្កើតឡើងដោយលំហូរការងារនៅក្នុង Cal។"
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "가장 많이 취소된 예약",
"name_or_email": "이름 또는 이메일",
"salesforce_round_robin_skip_fallback_to_lead_owner": "연락처를 찾을 수 없는 경우, 리드 소유자가 있으면 대체",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "날짜 및 시간",
"email_reminder_upcoming_event_notice": "다가오는 일정에 대한 알림입니다.",
"email_reminder_triggered_by_workflow": "이 알림은 Cal의 워크플로우에 의해 트리거되었습니다.",
"event_upper_case": "이벤트",
"scheduling_by": "일정 계획 담당자",
"experience_review_prompt": "최근 경험은 어떠셨나요?",
"improve_customer_experience_message": "우리는 항상 고객 경험을 개선하기 위해 노력합니다.",
"meeting_satisfaction_question": "최근 미팅에 얼마나 만족하셨나요?",
"meeting_not_joined_question": "미팅에 참여하지 않으셨나요?",
"reschedule_cta_short": "여기서 재예약하세요.",
"you_and_conjunction": "당신 &",
"email_survey_triggered_by_workflow": "이 설문은 Cal의 워크플로우에 의해 트리거되었습니다."
}
+13 -1
View File
@@ -149,5 +149,17 @@
"ooo_select_reason": "Izvēlieties iemeslu",
"enterprise_license_locally": "Jūs varat testēt šo funkciju vietēji, bet ne ražošanas vidē.",
"enterprise_license_sales": "Lai uzlabotu uz uzņēmuma izdevumu, lūdzu, sazinieties ar mūsu pārdošanas komandu. Ja licences atslēga jau ir, lūdzu, sazinieties ar support@cal.com uz palīdzību.",
"apply_to_all_event_types": "Piemēroties visiem, ieskaitot arī nākotnes notikumu veidus"
"apply_to_all_event_types": "Piemēroties visiem, ieskaitot arī nākotnes notikumu veidus",
"date_and_time": "Datums un laiks",
"email_reminder_upcoming_event_notice": "Šis ir atgādinājums par jūsu gaidāmo pasākumu.",
"email_reminder_triggered_by_workflow": "Šo atgādinājumu ir iedarbinājis Cal darbplūsma.",
"event_upper_case": "Notikums",
"scheduling_by": "Ieplānoja",
"experience_review_prompt": "Kāda bija jūsu pēdējā pieredze?",
"improve_customer_experience_message": "Mēs pastāvīgi uzlabojam klientu pieredzi.",
"meeting_satisfaction_question": "Cik apmierināts bijāt ar savu pēdējo tikšanos?",
"meeting_not_joined_question": "nepiedalījāties sanāksmē?",
"reschedule_cta_short": "Pārzīmēt šeit.",
"you_and_conjunction": "Jūs &",
"email_survey_triggered_by_workflow": "Šo aptauju aktivizēja darbplūsma Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Meest geannuleerde boekingen",
"name_or_email": "Naam of e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Als er geen contactpersoon wordt gevonden, val dan terug op de lead-eigenaar als deze bestaat",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Datum en tijd",
"email_reminder_upcoming_event_notice": "Dit is een herinnering voor je aankomende evenement.",
"email_reminder_triggered_by_workflow": "Deze herinnering is geactiveerd door een Workflow in Cal.",
"event_upper_case": "Evenement",
"scheduling_by": "Gepland door",
"experience_review_prompt": "Hoe was uw recente ervaring?",
"improve_customer_experience_message": "We werken continu aan betere klantervaringen.",
"meeting_satisfaction_question": "Hoe tevreden was u met uw laatste vergadering?",
"meeting_not_joined_question": "heeft u niet deelgenomen aan de vergadering?",
"reschedule_cta_short": "Hier opnieuw plannen.",
"you_and_conjunction": "U &",
"email_survey_triggered_by_workflow": "Deze enquête is geactiveerd door een Workflow in Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Flest kansellerte bestillinger",
"name_or_email": "Navn eller e-post",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Hvis ingen kontakt blir funnet, fall tilbake til leadeier hvis den eksisterer",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Dato og klokkeslett",
"email_reminder_upcoming_event_notice": "Dette er en påminnelse om din kommende hendelse.",
"email_reminder_triggered_by_workflow": "Denne påminnelsen ble utløst av en Workflow i Cal.",
"event_upper_case": "Arrangement",
"scheduling_by": "Planlagt av",
"experience_review_prompt": "Hvordan var din siste opplevelse?",
"improve_customer_experience_message": "Vi forbedrer hele tiden kundeopplevelsen.",
"meeting_satisfaction_question": "Hvor fornøyd var du med ditt siste møte?",
"meeting_not_joined_question": "deltok ikke i møtet?",
"reschedule_cta_short": "Planlegg på nytt her.",
"you_and_conjunction": "Du &",
"email_survey_triggered_by_workflow": "Denne undersøkelsen ble utløst av en arbeidsflyt i Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Najczęściej anulowane rezerwacje",
"name_or_email": "Imię lub email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Jeśli nie znaleziono kontaktu, powróć do właściciela leada, jeśli istnieje",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Data i godzina",
"email_reminder_upcoming_event_notice": "To przypomnienie o nadchodzącym wydarzeniu.",
"email_reminder_triggered_by_workflow": "To przypomnienie zostało wyzwolone przez Workflow w Cal.",
"event_upper_case": "Wydarzenie",
"scheduling_by": "Zaplanowane przez",
"experience_review_prompt": "Jakie były Twoje ostatnie wrażenia?",
"improve_customer_experience_message": "Ciągle ulepszamy doświadczenia klientów.",
"meeting_satisfaction_question": "Jak zadowolony byłeś ze swojego ostatniego spotkania?",
"meeting_not_joined_question": "nie uczestniczyłeś w spotkaniu?",
"reschedule_cta_short": "Przełóż tutaj.",
"you_and_conjunction": "Ty &",
"email_survey_triggered_by_workflow": "Ta ankieta została uruchomiona przez Workflow w Cal."
}
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Reservas mais canceladas",
"name_or_email": "Nome ou e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Se nenhum contato for encontrado, recorrer ao proprietário do lead, se existir",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Data e horário",
"email_reminder_upcoming_event_notice": "Este é um lembrete sobre seu próximo evento.",
"email_reminder_triggered_by_workflow": "Este lembrete foi acionado por um Workflow no Cal.",
"event_upper_case": "Evento",
"scheduling_by": "Agendado por",
"experience_review_prompt": "Como foi sua experiência recente?",
"improve_customer_experience_message": "Sempre buscamos melhorar a experiência do cliente.",
"meeting_satisfaction_question": "Quão satisfeito você está com sua última reunião?",
"meeting_not_joined_question": "não participou da reunião?",
"reschedule_cta_short": "Reagende aqui.",
"you_and_conjunction": "Você &",
"email_survey_triggered_by_workflow": "Esta pesquisa foi acionada por um Workflow no Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Reservas mais canceladas",
"name_or_email": "Nome ou e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Se nenhum contato for encontrado, recorrer ao proprietário do lead, se existir",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Data e hora",
"email_reminder_upcoming_event_notice": "Este é um lembrete sobre o teu próximo evento.",
"email_reminder_triggered_by_workflow": "Este lembrete foi acionado por um Workflow no Cal.",
"event_upper_case": "Evento",
"scheduling_by": "Agendado por",
"experience_review_prompt": "Como foi a sua experiência recente?",
"improve_customer_experience_message": "Estamos sempre a melhorar a experiência do cliente.",
"meeting_satisfaction_question": "Quão satisfeito está com a sua última reunião?",
"meeting_not_joined_question": "não participou na reunião?",
"reschedule_cta_short": "Remarcar aqui.",
"you_and_conjunction": "Você &",
"email_survey_triggered_by_workflow": "Este questionário foi acionado por um Workflow no Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Cele mai multe rezervări anulate",
"name_or_email": "Nume sau email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Dacă nu este găsit niciun contact, se revine la proprietarul lead-ului dacă acesta există",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Data și ora",
"email_reminder_upcoming_event_notice": "Acesta este un memento privind evenimentul tău viitor.",
"email_reminder_triggered_by_workflow": "Acest memento a fost declanșat de un Workflow în Cal.",
"event_upper_case": "Eveniment",
"scheduling_by": "Programat de",
"experience_review_prompt": "Cum a fost experiența ta recentă?",
"improve_customer_experience_message": "Îmbunătățim constant experiența clienților.",
"meeting_satisfaction_question": "Cât de mulțumit ai fost de ultima întâlnire?",
"meeting_not_joined_question": "nu ai participat la întâlnire?",
"reschedule_cta_short": "Reprogramează aici.",
"you_and_conjunction": "Tu &",
"email_survey_triggered_by_workflow": "Acest sondaj a fost declanșat de un Workflow în Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Наиболее часто отменяемые бронирования",
"name_or_email": "Имя или электронная почта",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Если контакт не найден, использовать владельца лида, если он существует",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Дата и время",
"email_reminder_upcoming_event_notice": "Это напоминание о вашем предстоящем мероприятии.",
"email_reminder_triggered_by_workflow": "Это напоминание было активировано Workflow в Cal.",
"event_upper_case": "Событие",
"scheduling_by": "Запланировано",
"experience_review_prompt": "Как прошёл ваш последний опыт?",
"improve_customer_experience_message": "Мы постоянно улучшаем клиентский опыт.",
"meeting_satisfaction_question": "Насколько вы довольны последней встречей?",
"meeting_not_joined_question": "не присоединились к встрече?",
"reschedule_cta_short": "Перенести здесь.",
"you_and_conjunction": "Вы &",
"email_survey_triggered_by_workflow": "Этот опрос был запущен рабочим процессом в Cal."
}
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Najviac zrušených rezervácií",
"name_or_email": "Meno alebo email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ak sa nenájde žiadny kontakt, použije sa vlastník leadu, ak existuje",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Pridajte svoje nové reťazce nad túto líniu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Pridajte svoje nové reťazce nad túto líniu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Dátum a čas",
"email_reminder_upcoming_event_notice": "Toto je pripomienka k vášmu pripravovanému podujatiu.",
"email_reminder_triggered_by_workflow": "Toto pripomenutie bolo spustené pracovným postupom v Cal.",
"event_upper_case": "Udalosť",
"scheduling_by": "Naplánované",
"experience_review_prompt": "Aký bol váš posledný zážitok?",
"improve_customer_experience_message": "Neustále zlepšujeme zákaznícku skúsenosť.",
"meeting_satisfaction_question": "Ako spokojní ste boli s posledným stretnutím?",
"meeting_not_joined_question": "nezúčastnili ste sa stretnutia?",
"reschedule_cta_short": "Preplánujte tu.",
"you_and_conjunction": "Vy &",
"email_survey_triggered_by_workflow": "Tento prieskum bol aktivovaný pracovným postupom v Cal."
}
+13 -1
View File
@@ -54,5 +54,17 @@
"ooo_select_reason": "Vybrať dôvod",
"enterprise_license_locally": "Túto funkciu môžete otestovať lokálne, ale nie v produkčnom prostredí.",
"enterprise_license_sales": "Na upgradovanie na podnikateľskú verziu kontaktujte náš obchodný tím. Ak už je licenčný kľúč na mieste, kontaktujte support@cal.com pre pomoc.",
"apply_to_all_event_types": "Platí pre všetky, vrátane budúcich typov udalostí"
"apply_to_all_event_types": "Platí pre všetky, vrátane budúcich typov udalostí",
"date_and_time": "Dátum a čas",
"email_reminder_upcoming_event_notice": "Toto je pripomienka k vášmu pripravovanému podujatiu.",
"email_reminder_triggered_by_workflow": "Toto pripomenutie bolo spustené pracovným postupom v Cal.",
"event_upper_case": "Udalosť",
"scheduling_by": "Naplánované",
"experience_review_prompt": "Aký bol váš posledný zážitok?",
"improve_customer_experience_message": "Neustále zlepšujeme zákaznícku skúsenosť.",
"meeting_satisfaction_question": "Ako spokojní ste boli s posledným stretnutím?",
"meeting_not_joined_question": "nezúčastnili ste sa stretnutia?",
"reschedule_cta_short": "Preplánujte tu.",
"you_and_conjunction": "Vy &",
"email_survey_triggered_by_workflow": "Tento prieskum bol spustený pracovným postupom v Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Najviše otkazanih rezervacija",
"name_or_email": "Ime ili email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ako kontakt nije pronađen, vrati se na vlasnika potencijalnog klijenta ako postoji",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Датум и време",
"email_reminder_upcoming_event_notice": "Ово је подсетник за ваш предстојећи догађај.",
"email_reminder_triggered_by_workflow": "Овај подсетник је покренут путем Workflow-а у Cal-у.",
"event_upper_case": "Догађај",
"scheduling_by": "Планирање до",
"experience_review_prompt": "Како је било ваше последње искуство?",
"improve_customer_experience_message": "Стално побољшавамо клијентско искуство.",
"meeting_satisfaction_question": "Колико сте задовољни последњим састанком?",
"meeting_not_joined_question": "нисте присуствовали састанку?",
"reschedule_cta_short": "Презакажите овде.",
"you_and_conjunction": "Ви &",
"email_survey_triggered_by_workflow": "Ова анкета је покренута Воркфловом у Цал-у."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Flest avbokade bokningar",
"name_or_email": "Namn eller e-post",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Om ingen kontakt hittas, återgå till lead-ägaren om sådan finns",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Datum och tid",
"email_reminder_upcoming_event_notice": "Det här är en påminnelse om ditt kommande evenemang.",
"email_reminder_triggered_by_workflow": "Den här påminnelsen utlöstes av ett Workflow i Cal.",
"event_upper_case": "Evenemang",
"scheduling_by": "Schemalagt av",
"experience_review_prompt": "Hur var din senaste upplevelse?",
"improve_customer_experience_message": "Vi förbättrar ständigt kundupplevelsen.",
"meeting_satisfaction_question": "Hur nöjd var du med ditt senaste möte?",
"meeting_not_joined_question": "deltog inte i mötet?",
"reschedule_cta_short": "Boka om här.",
"you_and_conjunction": "Du &",
"email_survey_triggered_by_workflow": "Den här enkäten utlöstes av ett Workflow i Cal."
}
+13 -1
View File
@@ -198,5 +198,17 @@
"ooo_select_reason": "காரணம் தேர்ந்தெடு",
"enterprise_license_locally": "இந்த சாதனையை நீங்கள் உள்ளூரில் சோதிக்கலாம் ஆனால் தயாரிப்பில் இல்லை.",
"enterprise_license_sales": "பணியில் பதிவேற்றம் செய்து தொழில் பதிப்பிற்கு மாற்றுங்கள் என்றால் எங்கள் விற்பனை குழுவுடன் தொடர்பு கொள்ளுங்கள். ஒரு உரிமை முக்கிய தொடர்பு ஏற்கனவே உள்ளது என்றால் support@cal.com க்கு தொடர்பு கொள்ளுங்கள் உதவிக்கு.",
"apply_to_all_event_types": "எல்லா நிகழ்வுகள் தொடர்புகளிலும் பயன்படுத்துங்கள், எதிர்கால நிகழ்வுகள் தொடர்புகள் உட்பட."
"apply_to_all_event_types": "எல்லா நிகழ்வுகள் தொடர்புகளிலும் பயன்படுத்துங்கள், எதிர்கால நிகழ்வுகள் தொடர்புகள் உட்பட.",
"date_and_time": "தேதி மற்றும் நேரம்",
"email_reminder_upcoming_event_notice": "இது உங்கள் வரவிருக்கும் நிகழ்வைப் பற்றிய நினைவூட்டல்.",
"email_reminder_triggered_by_workflow": "இந்த நினைவூட்டல் Cal-ல் உள்ள ஒரு Workflow மூலம் தூண்டப்பட்டது.",
"event_upper_case": "நிகழ்வு",
"scheduling_by": "ஷெடியூல் செய்தவர்",
"experience_review_prompt": "உங்களது சமீபத்திய அனுபவம் எப்படி இருந்தது?",
"improve_customer_experience_message": "நாங்கள் வாடிக்கையாளர் அனுபவத்தை மேம்படுத்துவதில் தீவிரமாக உள்ளோம்.",
"meeting_satisfaction_question": "உங்கள் கடைசி கூட்டத்தில் எவ்வளவு திருப்தியாக இருந்தீர்கள்?",
"meeting_not_joined_question": "கூட்டத்தில் சேரவில்லையா?",
"reschedule_cta_short": "இங்கே மீண்டும் ஷெடியூல் செய்யவும்.",
"you_and_conjunction": "நீங்கள் &",
"email_survey_triggered_by_workflow": "இந்த ஆய்வு Cal-ல் உள்ள பணிப்பாய்வு மூலம் தூண்டப்பட்டது."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "En çok iptal edilen rezervasyonlar",
"name_or_email": "İsim veya E-posta",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Hiçbir kişi bulunamazsa, eğer varsa müşteri adayı sahibine geri dön",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Tarih ve saat",
"email_reminder_upcoming_event_notice": "Bu, yaklaşan etkinliğinizle ilgili bir hatırlatmadır.",
"email_reminder_triggered_by_workflow": "Bu hatırlatma, Cal'daki bir İş Akışı tarafından tetiklendi.",
"event_upper_case": "Etkinlik",
"scheduling_by": "Planlayan",
"experience_review_prompt": "Son deneyiminiz nasıldı?",
"improve_customer_experience_message": "Müşteri deneyimini sürekli iyileştiriyoruz.",
"meeting_satisfaction_question": "Son toplantınızdan ne kadar memnun kaldınız?",
"meeting_not_joined_question": "toplantıya katılmadınız mı?",
"reschedule_cta_short": "Buradan yeniden planla.",
"you_and_conjunction": "Sen &",
"email_survey_triggered_by_workflow": "Bu anket, Cal'da bir İş Akışı tarafından tetiklendi."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Найбільше скасованих бронювань",
"name_or_email": "Ім'я або електронна пошта",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Якщо контакт не знайдено, використовувати власника ліда, якщо він існує",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Дата та час",
"email_reminder_upcoming_event_notice": "Це нагадування про вашу майбутню подію.",
"email_reminder_triggered_by_workflow": "Це нагадування було ініційовано Workflow у Cal.",
"event_upper_case": "Подія",
"scheduling_by": "Заплановано",
"experience_review_prompt": "Як пройшов ваш останній досвід?",
"improve_customer_experience_message": "Ми постійно покращуємо клієнтський досвід.",
"meeting_satisfaction_question": "Наскільки ви задоволені останньою зустріччю?",
"meeting_not_joined_question": "не приєдналися до зустрічі?",
"reschedule_cta_short": "Перенести тут.",
"you_and_conjunction": "Ви &",
"email_survey_triggered_by_workflow": "Це опитування було запущено робочим процесом у Cal."
}
+13 -1
View File
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "Lịch hẹn bị hủy nhiều nhất",
"name_or_email": "Tên hoặc email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Nếu không tìm thấy liên hệ, chuyển sang chủ sở hữu khách hàng tiềm năng nếu tồn tại",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "Ngày và giờ",
"email_reminder_upcoming_event_notice": "Đây là lời nhắc về sự kiện sắp tới của bạn.",
"email_reminder_triggered_by_workflow": "Lời nhắc này được kích hoạt bởi Quy trình làm việc trong Cal.",
"event_upper_case": "Sự kiện",
"scheduling_by": "Được lập lịch bởi",
"experience_review_prompt": "Trải nghiệm gần đây của bạn thế nào?",
"improve_customer_experience_message": "Chúng tôi luôn cải thiện trải nghiệm khách hàng.",
"meeting_satisfaction_question": "Bạn hài lòng thế nào với cuộc họp gần đây?",
"meeting_not_joined_question": "không tham gia cuộc họp?",
"reschedule_cta_short": "Đặt lại lịch tại đây.",
"you_and_conjunction": "Bạn &",
"email_survey_triggered_by_workflow": "Khảo sát này được kích hoạt bởi Quy trình làm việc trong Cal."
}
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "最多取消的预约",
"name_or_email": "姓名或电子邮箱",
"salesforce_round_robin_skip_fallback_to_lead_owner": "如果未找到联系人,则在存在潜在客户所有者的情况下回退到潜在客户所有者",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "日期和时间",
"email_reminder_upcoming_event_notice": "这是关于您即将到来的活动的提醒。",
"email_reminder_triggered_by_workflow": "此提醒由 Cal 中的工作流触发。",
"event_upper_case": "活动",
"scheduling_by": "由...安排",
"experience_review_prompt": "您最近的体验如何?",
"improve_customer_experience_message": "我们始终致力于改善客户体验。",
"meeting_satisfaction_question": "您对最近的会议满意吗?",
"meeting_not_joined_question": "没有参加会议?",
"reschedule_cta_short": "在此重新安排。",
"you_and_conjunction": "你 &",
"email_survey_triggered_by_workflow": "该调查由 Cal 中的工作流触发。"
}
@@ -3053,5 +3053,17 @@
"most_cancelled_bookings": "最多取消的預約",
"name_or_email": "姓名或電子郵件",
"salesforce_round_robin_skip_fallback_to_lead_owner": "如果找不到聯絡人,則回退到潛在客戶擁有者(如果存在)",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"date_and_time": "日期和時間",
"email_reminder_upcoming_event_notice": "這是關於您即將到來的活動的提醒。",
"email_reminder_triggered_by_workflow": "此提醒由 Cal 中的工作流程觸發。",
"event_upper_case": "活動",
"scheduling_by": "由...安排",
"experience_review_prompt": "您最近的體驗如何?",
"improve_customer_experience_message": "我們始終致力於改善客戶體驗。",
"meeting_satisfaction_question": "您對最近的會議滿意嗎?",
"meeting_not_joined_question": "沒有參加會議?",
"reschedule_cta_short": "在此重新安排。",
"you_and_conjunction": "你 &",
"email_survey_triggered_by_workflow": "該調查由 Cal 中的工作流程觸發。"
}
@@ -3,8 +3,8 @@ import type { Session } from "next-auth";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { getUsernameList } from "@calcom/lib/defaultEvents";
@@ -12,7 +12,7 @@ import { EventRepository } from "@calcom/lib/server/repository/event";
import { UserRepository } from "@calcom/lib/server/repository/user";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { RedirectType } from "@calcom/prisma/client";
import { BookingStatus, RedirectType } from "@calcom/prisma/client";
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
@@ -33,10 +33,12 @@ async function processReschedule({
props,
rescheduleUid,
session,
allowRescheduleForCancelledBooking,
}: {
props: Props;
session: Session | null;
rescheduleUid: string | string[] | undefined;
allowRescheduleForCancelledBooking?: boolean;
}) {
if (!rescheduleUid) return;
@@ -52,7 +54,12 @@ async function processReschedule({
}
// if no booking found, no eventTypeId (dynamic) or it matches this eventData - return void (success).
if (booking === null || !booking.eventTypeId || booking?.eventTypeId === props.eventData?.id) {
if (
booking === null ||
!booking.eventTypeId ||
(booking?.eventTypeId === props.eventData?.id &&
(booking.status !== BookingStatus.CANCELLED || allowRescheduleForCancelledBooking))
) {
props.booking = booking;
props.rescheduleUid = Array.isArray(rescheduleUid) ? rescheduleUid[0] : rescheduleUid;
return;
@@ -82,20 +89,32 @@ async function processReschedule({
async function processSeatedEvent({
props,
bookingUid,
allowRescheduleForCancelledBooking,
}: {
props: Props;
bookingUid: string | string[] | undefined;
allowRescheduleForCancelledBooking?: boolean;
}) {
if (!bookingUid) return;
props.booking = await getBookingForSeatedEvent(`${bookingUid}`);
props.bookingUid = Array.isArray(bookingUid) ? bookingUid[0] : bookingUid;
const booking = await getBookingForSeatedEvent(`${bookingUid}`);
if (booking?.status === BookingStatus.CANCELLED && !allowRescheduleForCancelledBooking) {
return {
redirect: {
permanent: false,
destination: `${props.slug}`,
},
};
} else {
props.booking = booking;
props.bookingUid = Array.isArray(bookingUid) ? bookingUid[0] : bookingUid;
}
}
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const session = await getServerSession({ req: context.req });
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid, bookingUid } = context.query;
const allowRescheduleForCancelledBooking = context.query.allowRescheduleForCancelledBooking === "true";
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
const org = isValidOrgDomain ? currentOrgDomain : null;
if (!org) {
@@ -162,12 +181,24 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
};
if (rescheduleUid) {
const processRescheduleResult = await processReschedule({ props, rescheduleUid, session });
const processRescheduleResult = await processReschedule({
props,
rescheduleUid,
session,
allowRescheduleForCancelledBooking,
});
if (processRescheduleResult) {
return processRescheduleResult;
}
} else if (bookingUid) {
await processSeatedEvent({ props, bookingUid });
const processSeatResult = await processSeatedEvent({
props,
bookingUid,
allowRescheduleForCancelledBooking,
});
if (processSeatResult) {
return processSeatResult;
}
}
return {
@@ -180,6 +211,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid, bookingUid } = context.query;
const allowRescheduleForCancelledBooking = context.query.allowRescheduleForCancelledBooking === "true";
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
const isOrgContext = currentOrgDomain && isValidOrgDomain;
@@ -243,14 +275,25 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
rescheduleUid: null,
orgBannerUrl: eventData?.owner?.profile?.organization?.bannerUrl ?? null,
};
if (rescheduleUid) {
const processRescheduleResult = await processReschedule({ props, rescheduleUid, session });
const processRescheduleResult = await processReschedule({
props,
rescheduleUid,
session,
allowRescheduleForCancelledBooking,
});
if (processRescheduleResult) {
return processRescheduleResult;
}
} else if (bookingUid) {
await processSeatedEvent({ props, bookingUid });
const processSeatResult = await processSeatedEvent({
props,
bookingUid,
allowRescheduleForCancelledBooking,
});
if (processSeatResult) {
return processSeatResult;
}
}
return {
@@ -13,7 +13,7 @@ import type { z } from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
import { weekdayToWeekIndex, type WeekDays } from "@calcom/lib/date-fns";
import { weekdayToWeekIndex, type WeekDays } from "@calcom/lib/dayjs";
import type { HttpError } from "@calcom/lib/http-error";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import logger from "@calcom/lib/logger";
File diff suppressed because it is too large Load Diff
+102 -6
View File
@@ -5,13 +5,13 @@ description: Overview of all the hooks associated with bookings.
Optimize your development workflow using our custom hooks. Our custom hooks are designed to simplify the integration of the Cal.com API into your products. With these hooks, you can effortlessly manage state, handle side effects, and optimize performance, allowing you to focus on building exceptional user experiences without the hassle of complex API interactions.
Below is a list of hooks you can find that are associated with bookings:
Below is a list of hooks you can find that are associated with bookings. Notably, all of these hooks are available only for platform customers.
### 1. `useBookings`
### 1. get all bookings - `useBookings`
The useBookings hook returns an array of all the booking data, allowing you to filter bookings by status, such as ***upcoming,*** ***recurring***, ***past,*** ***cancelled,*** or ***unconfirmed.*** It also supports pagination through the take and skip parameters. Additionally, various properties can be passed to useBookings, which can be referenced in the [following](https://github.com/calcom/cal.com/blob/307b2946a2cb6dcdb32dbee6c4f448e8a01c4285/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts#L32) documentation for detailed information.
The useBookings hook returns an array of bookings that belong to the user whose access token is passed to [\<CalProvider\>](https://cal.com/docs/platform/atoms/cal-provider). You can filter bookings by status, event type id, attendee email etc. - [see this class](https://github.com/calcom/cal.com/blob/307b2946a2cb6dcdb32dbee6c4f448e8a01c4285/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts#L32) for filters you can pass to the hook.
Below code snippet shows how to use the useBookings hook to fetch bookings for a specific user.
Below code snippet shows how to use the useBookings hook to fetch bookings for a specific user using take, skip and status filters:
```js
import { useBookings } from "@calcom/atoms";
@@ -41,7 +41,7 @@ export default function Bookings() {
}
```
### 2. `useBooking`
### 2. get specific booking - `useBooking`
The useBooking hook returns detailed information about a single booking. Simply provide the booking's unique identifier ***(bookingUid)*** as a parameter to fetch its associated data.
@@ -68,7 +68,7 @@ export default function Booking() {
}
```
### 3. `useCancelBooking`
### 3. cancel booking - `useCancelBooking`
The useCancelBooking hook allows you to cancel a booking. This hook returns a mutation function that handles the cancellation process. To cancel a booking, you'll need to provide at least the booking's unique identifier ***(uid).*** While the uid is the only mandatory parameter, you can enhance the cancellation process by including optional parameters such as ***cancellationReason*** to specify why the booking is being canceled, and ***allRemainingBookings*** to specify whether to cancel all future recurring bookings.
@@ -98,4 +98,100 @@ export default function CancelBooking() {
</>
);
}
```
### 4. get all bookings of a user as an organization admin - `useOrganizationUserBookings`
The useBookings hooks returns bookings of user associated with the access token passed to the [\<CalProvider\>](https://cal.com/docs/platform/atoms/cal-provider), but this hook
allows organization admins to fetch bookings of any organization member.
The access token passed to the [\<CalProvider\>](https://cal.com/docs/platform/atoms/cal-provider) must belong
to a managed user who is an organization admin. That means you have to create a managed user and then add an accepted membership with an admin or owner role by making a request to the [organizations memberships endpoint](https://cal.com/docs/api-reference/v2/orgs-memberships/create-a-membership). Example body:
```js
{
"userId": 1006,
"accepted": true,
"role": "ADMIN"
}
```
then you have to pass the access token of this managed user to the [CalProvider](https://cal.com/docs/platform/atoms/cal-provider) to then finally be able to
use the "useOrganizationUserBookings" by specifying "userId" which will return an array of that user's bookings. You can filter bookings by status, event type id, attendee email etc. - [see this class](https://github.com/calcom/cal.com/blob/307b2946a2cb6dcdb32dbee6c4f448e8a01c4285/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts#L32) for filters you can pass to the hook.
Below code snippet shows how to use the "useOrganizationUserBookings" hook to fetch bookings for a specific user using take, skip and status filters:
```js
import { useOrganizationUserBookings } from "@calcom/atoms";
export default function Bookings() {
const someUserId = 1218;
const { isLoading: isLoadingUpcomingBookings, data: upcomingBookings } = useOrganizationUserBookings(someUserId, {
take: 50,
skip: 0,
status: ["upcoming"],
});
return (
<>
<h1>Upcoming bookings</h1>
{isLoadingUpcomingBookings && <p>Loading...</p>}
{!isLoadingUpcomingBookings && !upcomingBookings && <p>No upcoming bookings found</p>}
{!isLoading &&
upcomingBookings &&
(Boolean(upcomingBookings?.length)) &&
[...upcomingBookings].map((booking) => {
return (
<div key={booking.id}><h1>{booking.title}</h1></div>
);
})}
</>
);
}
```
### 5. get all organization bookings as an organization admin - `useOrganizationBookings`
The useBookings hooks returns bookings of user associated with the access token passed to the [\<CalProvider\>](https://cal.com/docs/platform/atoms/cal-provider), but this hook
allows organization admins to fetch bookings of the whole organization.
The access token passed to the [\<CalProvider\>](https://cal.com/docs/platform/atoms/cal-provider) must belong
to a managed user who is an organization admin. That means you have to create a managed user and then add an accepted membership with an admin or owner role by making a request to the [organizations memberships endpoint](https://cal.com/docs/api-reference/v2/orgs-memberships/create-a-membership). Example body:
```js
{
"userId": 1006,
"accepted": true,
"role": "ADMIN"
}
```
then you have to pass the access token of this managed user to the [CalProvider](https://cal.com/docs/platform/atoms/cal-provider) to then finally be able to
use the "useOrganizationBookings" which will return an array of organization bookings. You can filter bookings by status, user ids, event type ids, attendee email etc. - [see this class](https://github.com/calcom/cal.com/blob/307b2946a2cb6dcdb32dbee6c4f448e8a01c4285/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts#L32) for filters you can pass to the hook + you can use "userIds" filter like in example below.
Below code snippet shows how to use the "useOrganizationBookings" hook to fetch bookings for a specific users using take, skip and userIds filters:
```js
import { useOrganizationBookings } from "@calcom/atoms";
export default function Bookings() {
const { isLoading: isLoadingUpcomingBookings, data: upcomingBookings } = useOrganizationBookings({
take: 50,
skip: 0,
userIds: [10, 11],
});
return (
<>
<h1>Upcoming bookings</h1>
{isLoadingUpcomingBookings && <p>Loading...</p>}
{!isLoadingUpcomingBookings && !upcomingBookings && <p>No upcoming bookings found</p>}
{!isLoading &&
upcomingBookings &&
(Boolean(upcomingBookings?.length)) &&
[...upcomingBookings].map((booking) => {
return (
<div key={booking.id}><h1>{booking.title}</h1></div>
);
})}
</>
);
}
```

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