Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e251a5afea | ||
|
|
62d162f001 | ||
|
|
8811ebcc33 | ||
|
|
c0529a26c4 | ||
|
|
a28d9bb840 | ||
|
|
06ec89e787 | ||
|
|
a4d733fb9d | ||
|
|
c3d56f736a | ||
|
|
c629d08a9c | ||
|
|
50a41109d0 | ||
|
|
ca5b83f876 | ||
|
|
c29e37a03f | ||
|
|
14ae664941 | ||
|
|
0949bbf167 | ||
|
|
d0a1e90199 | ||
|
|
077f29305d | ||
|
|
4c528afe65 | ||
|
|
57ed383554 | ||
|
|
d53cabd163 | ||
|
|
817d8738a9 | ||
|
|
d6dbbd1472 | ||
|
|
4021801698 | ||
|
|
b38d8e7ae4 | ||
|
|
123a39bd9a | ||
|
|
596dc3fbd7 | ||
|
|
058d58a6f3 | ||
|
|
4caec83c8f | ||
|
|
3ddef59761 | ||
|
|
c7edfd5bd3 | ||
|
|
23d1a21e23 | ||
|
|
e2c54b1fc4 | ||
|
|
622b2795f5 | ||
|
|
c0298248dc | ||
|
|
db07738514 | ||
|
|
700f0aeb35 | ||
|
|
f3fd043255 | ||
|
|
a0e4580f4e | ||
|
|
659b60aac0 | ||
|
|
7131996722 | ||
|
|
42cbd582f2 | ||
|
|
5481248c1a | ||
|
|
bd4a949a7c | ||
|
|
b768cc5b44 | ||
|
|
de80ca2aaa | ||
|
|
4afbe26506 | ||
|
|
f22d585c7d | ||
|
|
1ee4fa0906 | ||
|
|
1fc376c51a | ||
|
|
9382b862e4 | ||
|
|
c2662638a5 | ||
|
|
c2291164fa | ||
|
|
9d01025a1f | ||
|
|
86af6bd626 | ||
|
|
2f4dd328c3 | ||
|
|
6df994389b | ||
|
|
93e93a9c7a | ||
|
|
9087f5307b | ||
|
|
5f6032131b | ||
|
|
4e90b65884 | ||
|
|
b7792d0d0a | ||
|
|
c8cd260112 | ||
|
|
242268d580 | ||
|
|
3999f84eff | ||
|
|
995bf09dde | ||
|
|
59aff41ca3 | ||
|
|
b0e3e4c82f | ||
|
|
3e0d53228e | ||
|
|
df2ef84251 | ||
|
|
dcd07d1cc6 | ||
|
|
5f0cc805a9 | ||
|
|
c5677f1dd9 | ||
|
|
bf9be5915c | ||
|
|
4cf3627d52 | ||
|
|
0424304a50 | ||
|
|
7ab1288fa6 | ||
|
|
c7f4f52efa | ||
|
|
f70e8c5189 | ||
|
|
fcc2e0f2ae | ||
|
|
731f97ebe4 | ||
|
|
474f81f08e | ||
|
|
76a2822753 | ||
|
|
b8d4fccbab | ||
|
|
4f14775a4d | ||
|
|
acbcd59c31 | ||
|
|
05c1ad885b | ||
|
|
d73d0ebb6e | ||
|
|
a3971c127b | ||
|
|
912d3cf5b2 | ||
|
|
73c35ba5a2 | ||
|
|
55f1019908 | ||
|
|
863fc695ec | ||
|
|
d931fb1f9c | ||
|
|
fd054dfb8d | ||
|
|
699e08b2ee | ||
|
|
e1228ca03c | ||
|
|
9ad69c20cf | ||
|
|
9a5efd39ce | ||
|
|
497b0804d3 | ||
|
|
8b50f419e5 | ||
|
|
9208827406 | ||
|
|
8891381d89 | ||
|
|
ab39f0c8b6 | ||
|
|
cd2382e091 | ||
|
|
154c54a2aa | ||
|
|
e949a4c529 | ||
|
|
64b321ac9c | ||
|
|
e90e060739 | ||
|
|
123074f4ba | ||
|
|
8115c86e37 | ||
|
|
92a48b466c | ||
|
|
7a4298dc00 |
@@ -5,6 +5,7 @@
|
||||
# - DAILY.CO VIDEO
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# - HUBSPOT
|
||||
# - HUDDLE01
|
||||
# - OFFICE 365
|
||||
# - SLACK
|
||||
# - STRIPE
|
||||
@@ -137,3 +138,7 @@ REVERT_PUBLIC_TOKEN=
|
||||
# NOTE: If you're self hosting Revert, update this URL to point to your own instance.
|
||||
REVERT_API_URL=https://api.revert.dev/
|
||||
# *********************************************************************************************************
|
||||
|
||||
# - Huddle01
|
||||
# Used for the huddle01 integration
|
||||
HUDDLE01_API_TOKEN=
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@
|
||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||
#
|
||||
|
||||
# To enable enterprise-only features please add your envrionment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
CALCOM_LICENSE_KEY=
|
||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||
@@ -128,7 +128,7 @@ GOOGLE_API_CREDENTIALS=
|
||||
# Inbox to send user feedback
|
||||
SEND_FEEDBACK_EMAIL=
|
||||
|
||||
# Sengrid
|
||||
# Sendgrid
|
||||
# Used for email reminders in workflows and internal sync services
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_EMAIL=
|
||||
@@ -262,7 +262,7 @@ ORGANIZATIONS_ENABLED=
|
||||
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
|
||||
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sing-ups with
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
|
||||
# existing organizations based on email domain address
|
||||
ORGANIZATIONS_AUTOLINK=
|
||||
|
||||
|
||||
@@ -83,11 +83,12 @@ jobs:
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-api-v1-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for API v1 build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-api-v1-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-api-v1-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v1 build. Skipping build."
|
||||
else
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
fi
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
shell: bash
|
||||
|
||||
@@ -49,14 +49,15 @@ jobs:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for API v2 build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-api-v2-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v2 build. Skipping build."
|
||||
else
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
fi
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
shell: bash
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build atoms
|
||||
name: Build Atoms
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
@@ -28,13 +28,14 @@ jobs:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-atoms-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for Atoms build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-atoms-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-atoms-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for atoms build. Skipping build."
|
||||
else
|
||||
rm -rf packages/platform/atoms/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/atoms run build
|
||||
fi
|
||||
rm -rf packages/platform/atoms/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/atoms run build
|
||||
shell: bash
|
||||
|
||||
@@ -26,16 +26,17 @@ jobs:
|
||||
path: |
|
||||
**/docs/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
# Log cache hit
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-docs-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for Docs build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-docs-build.outputs.cache-hit != 'true'
|
||||
working-directory: docs
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-docs-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for Docs build. Skipping build."
|
||||
else
|
||||
npm install -g mintlify
|
||||
mintlify dev &
|
||||
sleep 5 # Let it run for 5 seconds
|
||||
kill $!
|
||||
fi
|
||||
npm install -g mintlify
|
||||
mintlify dev &
|
||||
sleep 5 # Let it run for 5 seconds
|
||||
kill $!
|
||||
shell: bash
|
||||
|
||||
+2
-2
@@ -108,10 +108,10 @@ To develop locally:
|
||||
nvm use
|
||||
```
|
||||
|
||||
You first might need to install the specific version and then use it:
|
||||
You first might need to install the specific version:
|
||||
|
||||
```sh
|
||||
nvm install && nvm use
|
||||
nvm install
|
||||
```
|
||||
|
||||
You can install nvm from [here](https://github.com/nvm-sh/nvm).
|
||||
|
||||
@@ -168,7 +168,10 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||
metadata: jsonSchema.nullable(),
|
||||
customInputs: customInputSchema.array().optional(),
|
||||
link: z.string().optional(),
|
||||
hashedLink: z.object({ link: z.string() }).optional().nullable(),
|
||||
hashedLink: z
|
||||
.array(z.object({ link: z.string() }))
|
||||
.optional()
|
||||
.default([]),
|
||||
bookingFields: eventTypeBookingFields.optional().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -66,11 +66,8 @@ describe("GET /api/slots", () => {
|
||||
buildMockData();
|
||||
await handler(req, res);
|
||||
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||
{
|
||||
"slots": {},
|
||||
}
|
||||
`);
|
||||
const response = JSON.parse(res._getData());
|
||||
expect(response.slots).toEqual(expect.objectContaining({}));
|
||||
});
|
||||
test("Returns and event type available slots with passed timeZone", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
@@ -87,11 +84,8 @@ describe("GET /api/slots", () => {
|
||||
buildMockData();
|
||||
await handler(req, res);
|
||||
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||
{
|
||||
"slots": {},
|
||||
}
|
||||
`);
|
||||
const response = JSON.parse(res._getData());
|
||||
expect(response.slots).toEqual(expect.objectContaining({}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.45",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.52",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -111,8 +111,9 @@ export class BookingsController_2024_04_15 {
|
||||
@Query() queryParams: GetBookingsInput_2024_04_15
|
||||
): Promise<GetBookingsOutput_2024_04_15> {
|
||||
const { filters, cursor, limit } = queryParams;
|
||||
const bookingListingByStatus = filters?.status ?? Status_2024_04_15["upcoming"];
|
||||
const bookings = await getAllUserBookings({
|
||||
bookingListingByStatus: [filters.status],
|
||||
bookingListingByStatus: [bookingListingByStatus],
|
||||
skip: cursor ?? 0,
|
||||
take: limit ?? 10,
|
||||
filters,
|
||||
@@ -355,13 +356,14 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
|
||||
req.body = { ...req.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
Object.assign(clone, { userId, ...oAuthParams, platformBookingLocation });
|
||||
clone.body = { ...clone.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
private async createNextApiRecurringBookingRequest(
|
||||
@@ -369,17 +371,18 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
Object.assign(req, {
|
||||
Object.assign(clone, {
|
||||
userId,
|
||||
...oAuthParams,
|
||||
platformBookingLocation,
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
});
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
private handleBookingErrors(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -16,7 +17,7 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule],
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule, BookingSeatModule],
|
||||
providers: [
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
|
||||
@@ -29,6 +29,25 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdsWithAttendeesWithBookingSeatAndUserAndEvent(ids: number[]) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUid(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
@@ -50,6 +69,23 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdWithAttendeesWithBookingSeatAndUserAndEvent(id: number) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithAttendeesAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
@@ -63,6 +99,31 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecurringByUid(uid: string) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
recurringEventId: uid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecurringByUidWithAttendeesAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
|
||||
@@ -34,7 +34,14 @@ import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
|
||||
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInputPipe,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
RescheduleBookingInputPipe,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
CreateBookingInput,
|
||||
@@ -166,7 +173,8 @@ export class BookingsController_2024_08_13 {
|
||||
})
|
||||
async rescheduleBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: RescheduleBookingInput_2024_08_13,
|
||||
@Body(new RescheduleBookingInputPipe())
|
||||
body: RescheduleBookingInput,
|
||||
@Req() request: Request
|
||||
): Promise<RescheduleBookingOutput_2024_08_13> {
|
||||
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body);
|
||||
@@ -185,7 +193,8 @@ export class BookingsController_2024_08_13 {
|
||||
async cancelBooking(
|
||||
@Req() request: Request,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: CancelBookingInput_2024_08_13
|
||||
@Body(new CancelBookingInputPipe())
|
||||
body: CancelBookingInput
|
||||
): Promise<CancelBookingOutput_2024_08_13> {
|
||||
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body);
|
||||
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { User } from "@prisma/client";
|
||||
import { DateTime } from "luxon";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Seated bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = "seated-bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
const seatedTventTypeSlug = "peer-coding-seated";
|
||||
const recurringSeatedTventTypeSlug = "peer-coding-recurring-seated";
|
||||
let seatedEventTypeId: number;
|
||||
let recurringSeatedEventTypeId: number;
|
||||
const maxRecurrenceCount = 3;
|
||||
|
||||
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
||||
let createdRecurringSeatedBooking: CreateRecurringSeatedBookingOutput_2024_08_13[];
|
||||
|
||||
const emailAttendeeOne = "mr_proper_seated@gmail.com";
|
||||
const nameAttendeeOne = "Mr Proper Seated";
|
||||
const emailAttendeeTwo = "mr_proper_friend_seated@gmail.com";
|
||||
const nameAttendeeTwo = "Mr Proper Friend Seated";
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const seatedEvent = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "peer coding",
|
||||
slug: seatedTventTypeSlug,
|
||||
length: 60,
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
seatedEventTypeId = seatedEvent.id;
|
||||
|
||||
const recurringSeatedEvent = await eventTypesRepositoryFixture.create(
|
||||
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
|
||||
{
|
||||
title: "peer coding recurring",
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
recurringSeatedEventTypeId = recurringSeatedEvent.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should book an event type with seats for the first time", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
const seatUid = data.seatUid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
createdSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should book an event type with seats for the second time", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeId,
|
||||
attendee: {
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "Rust",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
const seatUid = data.seatUid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(2);
|
||||
// note(Lauris): first attendee is from previous test request
|
||||
const firstAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeOne);
|
||||
expect(firstAttendee).toEqual({
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
timeZone: createdSeatedBooking.attendees[0].timeZone,
|
||||
language: createdSeatedBooking.attendees[0].language,
|
||||
absent: false,
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
const secondAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeTwo);
|
||||
expect(secondAttendee).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
createdSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should book a recurring event type with seats", async () => {
|
||||
const recurrenceCount = 2;
|
||||
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString(),
|
||||
eventTypeId: recurringSeatedEventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateRecurringSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateRecurringSeatedBooking(responseBody.data)) {
|
||||
const data: CreateRecurringSeatedBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(recurrenceCount);
|
||||
|
||||
const firstBooking = data[0];
|
||||
expect(firstBooking.seatUid).toBeDefined();
|
||||
const seatUid = firstBooking.seatUid;
|
||||
expect(firstBooking.id).toBeDefined();
|
||||
expect(firstBooking.uid).toBeDefined();
|
||||
expect(firstBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(firstBooking.status).toEqual("accepted");
|
||||
expect(firstBooking.start).toEqual(body.start);
|
||||
expect(firstBooking.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(firstBooking.duration).toEqual(60);
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(firstBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(firstBooking.attendees.length).toEqual(1);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
|
||||
const secondBooking = data[1];
|
||||
expect(secondBooking.seatUid).toBeDefined();
|
||||
const secondSeatUid = secondBooking.seatUid;
|
||||
expect(secondBooking.id).toBeDefined();
|
||||
expect(secondBooking.uid).toBeDefined();
|
||||
expect(secondBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(secondBooking.status).toEqual("accepted");
|
||||
const expectedStart = DateTime.fromISO(body.start, { zone: "utc" }).plus({ weeks: 1 }).toISO();
|
||||
expect(secondBooking.start).toEqual(expectedStart);
|
||||
expect(secondBooking.end).toEqual(
|
||||
DateTime.fromISO(expectedStart!, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(secondBooking.duration).toEqual(60);
|
||||
expect(secondBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(secondBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(secondBooking.attendees.length).toEqual(1);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid: secondSeatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
createdRecurringSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a booking for an event type with seats", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdSeatedBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
const expected = structuredClone(createdSeatedBooking);
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete expected.seatUid;
|
||||
expect(data).toEqual(expected);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a booking for a recurring event type with seats", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdRecurringSeatedBooking[1].recurringBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetRecurringSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetRecurringSeatedBooking(responseBody.data)) {
|
||||
const data: GetRecurringSeatedBookingOutput_2024_08_13[] = responseBody.data;
|
||||
const expected = structuredClone(createdRecurringSeatedBooking);
|
||||
for (const booking of expected) {
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete booking.seatUid;
|
||||
}
|
||||
expect(data).toEqual(expected);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a specific recurrence of a booking for a recurring event type with seats", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdRecurringSeatedBooking[0].uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
const expected = structuredClone(createdRecurringSeatedBooking[0]);
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete expected.seatUid;
|
||||
expect(data).toEqual(expected);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all seated bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/v2/bookings?sortCreated=asc")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(3);
|
||||
|
||||
const seatedBooking = responseBody.data[0];
|
||||
const seatedBookingExpected = structuredClone(createdSeatedBooking);
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete seatedBookingExpected.seatUid;
|
||||
expect(seatedBooking).toEqual(seatedBookingExpected);
|
||||
|
||||
const recurringSeatedBookings = [responseBody.data[1], responseBody.data[2]];
|
||||
const recurringSeatedBookingsExpected = structuredClone(createdRecurringSeatedBooking);
|
||||
for (const booking of recurringSeatedBookingsExpected) {
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete booking.seatUid;
|
||||
}
|
||||
expect(recurringSeatedBookings).toEqual(recurringSeatedBookingsExpected);
|
||||
});
|
||||
});
|
||||
|
||||
it("should reschedule seated booking", async () => {
|
||||
const body: RescheduleSeatedBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdSeatedBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
const seatUid = data.seatUid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
const attendee = createdSeatedBooking.attendees.find((a) => a.seatUid === body.seatUid);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee?.name,
|
||||
timeZone: attendee?.timeZone,
|
||||
language: attendee?.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: attendee?.name,
|
||||
...attendee?.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
createdSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel seated booking", async () => {
|
||||
const body: CancelSeatedBookingInput_2024_08_13 = {
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("cancelled");
|
||||
expect(data.start).toEqual(createdSeatedBooking.start);
|
||||
expect(data.end).toEqual(createdSeatedBooking.end);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
|
||||
return data.hasOwnProperty("seatUid");
|
||||
}
|
||||
|
||||
function responseDataIsCreateRecurringSeatedBooking(
|
||||
data: any
|
||||
): data is CreateRecurringSeatedBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
|
||||
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
|
||||
}
|
||||
|
||||
function responseDataIsGetRecurringSeatedBooking(
|
||||
data: any
|
||||
): data is GetRecurringSeatedBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
+17
-3
@@ -29,6 +29,7 @@ import {
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
@@ -369,7 +370,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
|
||||
});
|
||||
@@ -384,7 +389,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data[0].eventTypeId).toEqual(team2EventTypeId);
|
||||
});
|
||||
@@ -399,7 +408,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
|
||||
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
|
||||
@@ -428,6 +441,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(teamUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
+104
-21
@@ -28,7 +28,11 @@ import {
|
||||
VERSION_2024_08_13,
|
||||
X_CAL_CLIENT_ID,
|
||||
} from "@calcom/platform-constants";
|
||||
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
@@ -478,7 +482,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(5);
|
||||
});
|
||||
});
|
||||
@@ -492,7 +500,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -506,7 +518,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -520,7 +536,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
@@ -534,7 +554,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -548,7 +572,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(5);
|
||||
});
|
||||
});
|
||||
@@ -562,7 +590,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -576,7 +608,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -590,7 +626,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -604,7 +644,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -618,7 +662,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(5);
|
||||
});
|
||||
});
|
||||
@@ -632,7 +680,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -646,7 +698,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -660,9 +716,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
console.log("asap data", JSON.stringify(data, null, 2));
|
||||
expect(data[0].start).toEqual(createdBooking.start);
|
||||
expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
});
|
||||
@@ -677,7 +736,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
expect(data[1].start).toEqual(createdBooking.start);
|
||||
@@ -693,7 +756,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(createdBooking.start);
|
||||
expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
@@ -709,7 +776,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
expect(data[1].start).toEqual(createdBooking.start);
|
||||
@@ -725,7 +796,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(createdBooking.start);
|
||||
expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
@@ -741,7 +816,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
expect(data[1].start).toEqual(createdBooking.start);
|
||||
@@ -765,6 +844,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
@@ -793,6 +874,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.status).toEqual("cancelled");
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class CancelBookingOutput_2024_08_13 {
|
||||
@@ -17,10 +22,19 @@ export class CancelBookingOutput_2024_08_13 {
|
||||
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } },
|
||||
{ $ref: getSchemaPath(GetSeatedBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(GetRecurringSeatedBookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(GetRecurringSeatedBookingOutput_2024_08_13) } },
|
||||
],
|
||||
description:
|
||||
"Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects",
|
||||
})
|
||||
@Type(() => Object)
|
||||
data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[];
|
||||
data!:
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13[]
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13[];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class CreateBookingOutput_2024_08_13 {
|
||||
@@ -21,5 +26,9 @@ export class CreateBookingOutput_2024_08_13 {
|
||||
"Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects",
|
||||
})
|
||||
@Type(() => Object)
|
||||
data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[];
|
||||
data!:
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13[]
|
||||
| CreateSeatedBookingOutput_2024_08_13
|
||||
| CreateRecurringSeatedBookingOutput_2024_08_13[];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@ import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class RescheduleBookingOutput_2024_08_13 {
|
||||
@@ -15,11 +22,17 @@ export class RescheduleBookingOutput_2024_08_13 {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(CreateSeatedBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(CreateRecurringSeatedBookingOutput_2024_08_13) },
|
||||
],
|
||||
description:
|
||||
"Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Object)
|
||||
data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13;
|
||||
data!:
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| CreateSeatedBookingOutput_2024_08_13
|
||||
| CreateRecurringSeatedBookingOutput_2024_08_13;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
CreateBookingInput,
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
GetBookingsInput_2024_08_13,
|
||||
@@ -27,6 +26,10 @@ import {
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
CancelBookingInput,
|
||||
} from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -54,9 +57,19 @@ export class BookingsService_2024_08_13 {
|
||||
return await this.createInstantBooking(request, body);
|
||||
}
|
||||
|
||||
if (await this.isRecurring(body)) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId);
|
||||
const isRecurring = !!eventType?.recurringEvent;
|
||||
const isSeated = !!eventType?.seatsPerTimeSlot;
|
||||
|
||||
if (isRecurring && isSeated) {
|
||||
return await this.createRecurringSeatedBooking(request, body);
|
||||
}
|
||||
if (isRecurring && !isSeated) {
|
||||
return await this.createRecurringBooking(request, body);
|
||||
}
|
||||
if (isSeated) {
|
||||
return await this.createSeatedBooking(request, body);
|
||||
}
|
||||
|
||||
return await this.createRegularBooking(request, body);
|
||||
} catch (error) {
|
||||
@@ -83,47 +96,71 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
}
|
||||
|
||||
async isRecurring(body: CreateBookingInput) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId);
|
||||
return !!eventType?.recurringEvent;
|
||||
}
|
||||
|
||||
async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body);
|
||||
const bookings = await handleNewRecurringBooking(bookingRequest);
|
||||
const uid = bookings[0].recurringEventId;
|
||||
if (!uid) {
|
||||
throw new Error("Recurring booking was not created");
|
||||
}
|
||||
const ids = bookings.map((booking) => booking.id || 0);
|
||||
return this.outputService.getOutputRecurringBookings(ids);
|
||||
}
|
||||
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(uid);
|
||||
return this.outputService.getOutputRecurringBookings(recurringBooking);
|
||||
async createRecurringSeatedBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body);
|
||||
const bookings = await handleNewRecurringBooking(bookingRequest);
|
||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||
bookings.map((booking) => ({ id: booking.id || 0, seatUid: booking.seatReferenceUid || "" }))
|
||||
);
|
||||
}
|
||||
|
||||
async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body);
|
||||
const booking = await handleNewBooking(bookingRequest);
|
||||
|
||||
if (!booking.id) {
|
||||
throw new Error("Booking was not created");
|
||||
if (!booking.uid) {
|
||||
throw new Error("Booking missing uid");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
const databaseBooking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(booking.uid);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
}
|
||||
|
||||
async createSeatedBooking(request: Request, body: CreateBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body);
|
||||
const booking = await handleNewBooking(bookingRequest);
|
||||
|
||||
if (!booking.uid) {
|
||||
throw new Error("Booking missing uid");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(
|
||||
booking.uid
|
||||
);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputCreateSeatedBooking(databaseBooking, booking.seatReferenceUid || "");
|
||||
}
|
||||
|
||||
async getBooking(uid: string) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(uid);
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
|
||||
|
||||
if (booking) {
|
||||
const isRecurring = !!booking.recurringEventId;
|
||||
if (isRecurring) {
|
||||
const isSeated = !!booking.eventType?.seatsPerTimeSlot;
|
||||
|
||||
if (isRecurring && !isSeated) {
|
||||
return this.outputService.getOutputRecurringBooking(booking);
|
||||
}
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBooking(booking);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputSeatedBooking(booking);
|
||||
}
|
||||
return this.outputService.getOutputBooking(booking);
|
||||
}
|
||||
|
||||
@@ -131,8 +168,13 @@ export class BookingsService_2024_08_13 {
|
||||
if (!recurringBooking.length) {
|
||||
throw new NotFoundException(`Booking with uid=${uid} was not found in the database`);
|
||||
}
|
||||
const ids = recurringBooking.map((booking) => booking.id);
|
||||
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
|
||||
if (isRecurringSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBookings(ids);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputRecurringBookings(recurringBooking);
|
||||
return this.outputService.getOutputRecurringBookings(ids);
|
||||
}
|
||||
|
||||
async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) {
|
||||
@@ -151,12 +193,17 @@ export class BookingsService_2024_08_13 {
|
||||
// note(Lauris): fetchedBookings don't have attendees information and responses and i don't want to add them to the handler query,
|
||||
// because its used elsewhere in code that does not need that information, so i get ids, fetch bookings and then return them formatted in same order as ids.
|
||||
const ids = fetchedBookings.bookings.map((booking) => booking.id);
|
||||
const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUserAndEvent(ids);
|
||||
const bookings = await this.bookingsRepository.getByIdsWithAttendeesWithBookingSeatAndUserAndEvent(ids);
|
||||
|
||||
const bookingMap = new Map(bookings.map((booking) => [booking.id, booking]));
|
||||
const orderedBookings = ids.map((id) => bookingMap.get(id));
|
||||
|
||||
const formattedBookings: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = [];
|
||||
const formattedBookings: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13
|
||||
)[] = [];
|
||||
for (const booking of orderedBookings) {
|
||||
if (!booking) {
|
||||
continue;
|
||||
@@ -172,8 +219,13 @@ export class BookingsService_2024_08_13 {
|
||||
};
|
||||
|
||||
const isRecurring = !!formatted.recurringEventId;
|
||||
if (isRecurring) {
|
||||
const isSeated = !!formatted.eventType?.seatsPerTimeSlot;
|
||||
if (isRecurring && !isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
|
||||
} else if (isRecurring && isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
||||
} else if (isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputSeatedBooking(formatted));
|
||||
} else {
|
||||
formattedBookings.push(this.outputService.getOutputBooking(formatted));
|
||||
}
|
||||
@@ -182,7 +234,7 @@ export class BookingsService_2024_08_13 {
|
||||
return formattedBookings;
|
||||
}
|
||||
|
||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput_2024_08_13) {
|
||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
||||
try {
|
||||
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
||||
request,
|
||||
@@ -190,18 +242,34 @@ export class BookingsService_2024_08_13 {
|
||||
body
|
||||
);
|
||||
const booking = await handleNewBooking(bookingRequest);
|
||||
if (!booking.id) {
|
||||
throw new Error("Booking was not created");
|
||||
if (!booking.uid) {
|
||||
throw new Error("Booking missing uid");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
const databaseBooking =
|
||||
await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(booking.uid);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
if (databaseBooking.recurringEventId) {
|
||||
const isRecurring = !!databaseBooking.recurringEventId;
|
||||
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;
|
||||
|
||||
if (isRecurring && !isSeated) {
|
||||
return this.outputService.getOutputRecurringBooking(databaseBooking);
|
||||
}
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
booking?.seatReferenceUid || ""
|
||||
);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputCreateSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatReferenceUid || ""
|
||||
);
|
||||
}
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -213,7 +281,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput_2024_08_13) {
|
||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
||||
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
|
||||
await handleCancelBooking(bookingRequest);
|
||||
return this.getBooking(bookingUid);
|
||||
@@ -249,7 +317,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
async billBooking(booking: CreatedBooking) {
|
||||
const hostId = booking.hosts[0].id;
|
||||
const hostId = booking.hosts?.[0]?.id;
|
||||
if (!hostId) {
|
||||
this.logger.error(`Booking with uid=${booking.uid} has no host`);
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { bookingResponsesSchema } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import {
|
||||
bookingResponsesSchema,
|
||||
seatedBookingResponsesSchema,
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
@@ -16,13 +20,17 @@ import { z } from "zod";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateInstantBookingInput_2024_08_13,
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
GetBookingsInput_2024_08_13,
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
@@ -73,7 +81,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeyRepository
|
||||
private readonly apiKeyRepository: ApiKeyRepository,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository
|
||||
) {}
|
||||
|
||||
async createBookingRequest(
|
||||
@@ -239,9 +248,11 @@ export class InputBookingsService_2024_08_13 {
|
||||
async createRescheduleBookingRequest(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: RescheduleBookingInput_2024_08_13
|
||||
body: RescheduleBookingInput
|
||||
): Promise<BookingRequest> {
|
||||
const bodyTransformed = await this.transformInputRescheduleBooking(bookingUid, body);
|
||||
const bodyTransformed = this.isRescheduleSeatedBody(body)
|
||||
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
|
||||
: await this.transformInputRescheduleBooking(bookingUid, body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
|
||||
const newRequest = { ...request };
|
||||
@@ -258,6 +269,60 @@ export class InputBookingsService_2024_08_13 {
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 {
|
||||
return body.hasOwnProperty("seatUid");
|
||||
}
|
||||
|
||||
async transformInputRescheduleSeatedBooking(
|
||||
bookingUid: string,
|
||||
inputBooking: RescheduleSeatedBookingInput_2024_08_13
|
||||
) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
// todo create booking seat module, repository and fetch the seat to get info
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||
}
|
||||
if (!booking.eventTypeId) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`);
|
||||
}
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId);
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
||||
}
|
||||
|
||||
const seat = await this.bookingSeatRepository.getByReferenceUid(inputBooking.seatUid);
|
||||
if (!seat) {
|
||||
throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`);
|
||||
}
|
||||
|
||||
const { responses: bookingResponses } = seatedBookingResponsesSchema.parse(seat.data);
|
||||
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
|
||||
if (!attendee) {
|
||||
throw new NotFoundException(
|
||||
`Attendee with e-mail ${bookingResponses.email} for booking with uid=${bookingUid} and seatUid=${inputBooking.seatUid} not found`
|
||||
);
|
||||
}
|
||||
|
||||
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
return {
|
||||
start: startTime.toISO(),
|
||||
end: endTime.toISO(),
|
||||
eventTypeId: eventType.id,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// metadata: booking.metadata || {},
|
||||
metadata: {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: [],
|
||||
responses: { ...bookingResponses },
|
||||
rescheduleUid: inputBooking.seatUid,
|
||||
};
|
||||
}
|
||||
|
||||
async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
if (!booking) {
|
||||
@@ -371,9 +436,11 @@ export class InputBookingsService_2024_08_13 {
|
||||
async createCancelBookingRequest(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: CancelBookingInput_2024_08_13
|
||||
body: CancelBookingInput
|
||||
): Promise<BookingRequest> {
|
||||
const bodyTransformed = await this.transformInputCancelBooking(bookingUid, body);
|
||||
const bodyTransformed = this.isCancelSeatedBody(body)
|
||||
? await this.transformInputCancelSeatedBooking(bookingUid, body)
|
||||
: await this.transformInputCancelBooking(bookingUid, body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
|
||||
const newRequest = { ...request };
|
||||
@@ -389,6 +456,10 @@ export class InputBookingsService_2024_08_13 {
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 {
|
||||
return body.hasOwnProperty("seatUid");
|
||||
}
|
||||
|
||||
async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) {
|
||||
let allRemainingBookings = false;
|
||||
let uid = bookingUid;
|
||||
@@ -410,6 +481,31 @@ export class InputBookingsService_2024_08_13 {
|
||||
};
|
||||
}
|
||||
|
||||
async transformInputCancelSeatedBooking(
|
||||
bookingUid: string,
|
||||
inputBooking: CancelSeatedBookingInput_2024_08_13
|
||||
) {
|
||||
let allRemainingBookings = false;
|
||||
let uid = bookingUid;
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(
|
||||
bookingUid
|
||||
);
|
||||
|
||||
if (recurringBooking.length) {
|
||||
// note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event
|
||||
allRemainingBookings = true;
|
||||
// note(Lauirs): we need to set uid as one of the individual recurring ids, not the main recurring event id
|
||||
uid = recurringBooking[0].uid;
|
||||
}
|
||||
|
||||
return {
|
||||
uid,
|
||||
cancellationReason: "",
|
||||
allRemainingBookings,
|
||||
seatReferenceUid: inputBooking.seatUid,
|
||||
};
|
||||
}
|
||||
|
||||
transformInputMarkAbsentBooking(inputBooking: MarkAbsentBookingInput_2024_08_13) {
|
||||
return {
|
||||
noShowHost: inputBooking.host,
|
||||
|
||||
@@ -4,15 +4,34 @@ import { plainToClass } from "class-transformer";
|
||||
import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking } from "@calcom/prisma/client";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
SeatedAttendee,
|
||||
} from "@calcom/platform-types";
|
||||
import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
guests: z.array(z.string()).optional(),
|
||||
rescheduledReason: z.string().optional(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const seatedBookingResponsesSchema = z
|
||||
.object({
|
||||
responses: z
|
||||
.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -27,6 +46,7 @@ type DatabaseBooking = Booking & {
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
} & { user: { id: number; name: string | null; email: string } | null };
|
||||
|
||||
@@ -72,17 +92,18 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
|
||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
// note(Lauris): I don't know why plainToClass erases bookings responses so attaching manually
|
||||
bookingTransformed.bookingFieldsResponses = bookingResponses;
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
async getOutputRecurringBookings(databaseBookings: DatabaseBooking[]) {
|
||||
async getOutputRecurringBookings(bookingsIds: number[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of databaseBookings) {
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
for (const bookingId of bookingsIds) {
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(bookingId);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
throw new Error(`Booking with id=${bookingId} was not found in the database`);
|
||||
}
|
||||
|
||||
transformed.push(this.getOutputRecurringBooking(databaseBooking));
|
||||
@@ -132,4 +153,159 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
}
|
||||
|
||||
getOutputCreateSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
): CreateSeatedBookingOutput_2024_08_13 {
|
||||
const getSeatedBookingOutput = this.getOutputSeatedBooking(databaseBooking);
|
||||
return { ...getSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
duration,
|
||||
eventType: databaseBooking.eventType,
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: [],
|
||||
location: databaseBooking.location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
return attendeeParsed;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async getOutputRecurringSeatedBookings(bookingsIds: number[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const bookingId of bookingsIds) {
|
||||
const databaseBooking =
|
||||
await this.bookingsRepository.getByIdWithAttendeesWithBookingSeatAndUserAndEvent(bookingId);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${bookingId} was not found in the database`);
|
||||
}
|
||||
|
||||
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking));
|
||||
}
|
||||
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
|
||||
async getOutputCreateRecurringSeatedBookings(bookings: { id: number; seatUid: string }[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of bookings) {
|
||||
const databaseBooking =
|
||||
await this.bookingsRepository.getByIdWithAttendeesWithBookingSeatAndUserAndEvent(booking.id);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
}
|
||||
transformed.push(this.getOutputCreateRecurringSeatedBooking(databaseBooking, booking.seatUid));
|
||||
}
|
||||
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
|
||||
getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
): CreateRecurringSeatedBookingOutput_2024_08_13 {
|
||||
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(databaseBooking);
|
||||
return { ...getRecurringSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
duration,
|
||||
eventType: databaseBooking.eventType,
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: [],
|
||||
location: databaseBooking.location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
|
||||
strategy: "excludeAll",
|
||||
});
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
return attendeeParsed;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IntegrationLocation } from "@calcom/lib";
|
||||
import { OrganizerIntegrationLocation } from "@calcom/lib";
|
||||
|
||||
type BaseEventType = {
|
||||
length: number;
|
||||
@@ -7,7 +7,7 @@ type BaseEventType = {
|
||||
};
|
||||
|
||||
type EventTypeWithLocation = BaseEventType & {
|
||||
locations: IntegrationLocation[];
|
||||
locations: OrganizerIntegrationLocation[];
|
||||
};
|
||||
|
||||
const thirtyMinutes: BaseEventType = {
|
||||
|
||||
+209
-4
@@ -31,6 +31,7 @@ import {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
NameFieldInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
@@ -199,6 +200,13 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const nameBookingField: NameFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
label: "Your name sir / madam",
|
||||
placeholder: "john doe",
|
||||
disableOnPrefill: false,
|
||||
};
|
||||
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class",
|
||||
slug: "coding-class",
|
||||
@@ -209,8 +217,18 @@ describe("Event types Endpoints", () => {
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "attendeePhone",
|
||||
},
|
||||
{
|
||||
type: "attendeeAddress",
|
||||
},
|
||||
{
|
||||
type: "attendeeDefined",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
nameBookingField,
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
@@ -218,6 +236,7 @@ describe("Event types Endpoints", () => {
|
||||
required: true,
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: true,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
@@ -304,12 +323,16 @@ describe("Event types Endpoints", () => {
|
||||
);
|
||||
expect(createdEventType.color).toEqual(body.color);
|
||||
|
||||
const responseBookingFields = body.bookingFields || [];
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", type: "name" },
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
// note(Lauris): location booking field is added if multiple locations are passed
|
||||
{ isDefault: true, required: false, slug: "location", type: "radioInput" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
...responseBookingFields.map((field) => ({ isDefault: false, ...field })),
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
];
|
||||
|
||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
@@ -630,11 +653,30 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should update event type", async () => {
|
||||
const nameBookingField: NameFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
label: "Your name sir / madam",
|
||||
placeholder: "john doe",
|
||||
disableOnPrefill: true,
|
||||
};
|
||||
|
||||
const newTitle = "Coding class in Italian!";
|
||||
|
||||
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||
title: newTitle,
|
||||
scheduleId: secondSchedule.id,
|
||||
bookingFields: [
|
||||
nameBookingField,
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language you want to learn",
|
||||
slug: "select-language",
|
||||
required: true,
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
day: 4,
|
||||
week: 10,
|
||||
@@ -694,7 +736,19 @@ describe("Event types Endpoints", () => {
|
||||
expect(updatedEventType.description).toEqual(eventType.description);
|
||||
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(updatedEventType.locations).toEqual(eventType.locations);
|
||||
expect(updatedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
];
|
||||
|
||||
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
|
||||
expect(updatedEventType.ownerId).toEqual(user.id);
|
||||
expect(updatedEventType.scheduleId).toEqual(secondSchedule.id);
|
||||
expect(updatedEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
@@ -732,6 +786,7 @@ describe("Event types Endpoints", () => {
|
||||
eventType.hideCalendarEventDetails = updatedEventType.hideCalendarEventDetails;
|
||||
eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage;
|
||||
eventType.color = updatedEventType.color;
|
||||
eventType.bookingFields = updatedEventType.bookingFields;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1196,4 +1251,154 @@ describe("Event types Endpoints", () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle event-types locations", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "locations-event-types-test-e2e@api.com";
|
||||
const name = "bob-the-locations-builder";
|
||||
const username = name;
|
||||
let user: User;
|
||||
let legacyEventTypeId1: number;
|
||||
let legacyEventTypeId2: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
name,
|
||||
username,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should return integration location with link and credentialId", async () => {
|
||||
const eventTypeInput = {
|
||||
title: "event type discord",
|
||||
description: "event type description",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "discord-event-type",
|
||||
locations: [
|
||||
{
|
||||
type: "integrations:discord_video",
|
||||
link: "https://discord.com/users/100",
|
||||
credentialId: 100,
|
||||
},
|
||||
],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
legacyEventTypeId1 = legacyEventType.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${legacyEventTypeId1}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
expect(fetchedEventType.locations).toEqual([
|
||||
{
|
||||
type: "integration",
|
||||
integration: "discord-video",
|
||||
link: eventTypeInput.locations[0].link,
|
||||
credentialId: eventTypeInput.locations[0].credentialId,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return unsupported location", async () => {
|
||||
const eventTypeInput = {
|
||||
title: "event type not existing",
|
||||
description: "event type description",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "not-existing-event-type",
|
||||
locations: [
|
||||
{
|
||||
type: "this-type-does-not-exist",
|
||||
},
|
||||
],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
legacyEventTypeId1 = legacyEventType.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${legacyEventTypeId1}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
expect(fetchedEventType.locations).toEqual([
|
||||
{ type: "unknown", location: JSON.stringify(eventTypeInput.locations[0]) },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
try {
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
||||
} catch (e) {
|
||||
// Event type might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(user.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+16
-3
@@ -20,6 +20,8 @@ import {
|
||||
transformEventColorsApiToInternal,
|
||||
validateCustomEventName,
|
||||
transformSeatsApiToInternal,
|
||||
SystemField,
|
||||
CustomField,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
@@ -211,16 +213,27 @@ export class InputEventTypesService_2024_06_14 {
|
||||
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
const defaultFieldsBefore = [systemBeforeFieldName, systemBeforeFieldEmail];
|
||||
const customFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
: [];
|
||||
const customFieldsWithoutNameEmail = customFields.filter(
|
||||
(field) => field.type !== "name" && field.type !== "email"
|
||||
);
|
||||
const customNameField = customFields?.find((field) => field.type === "name");
|
||||
const customEmailField = customFields?.find((field) => field.type === "email");
|
||||
|
||||
const defaultFieldsBefore: (SystemField | CustomField)[] = [
|
||||
customNameField || systemBeforeFieldName,
|
||||
customEmailField || systemBeforeFieldEmail,
|
||||
];
|
||||
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
|
||||
if (hasMultipleLocations) {
|
||||
defaultFieldsBefore.push(systemBeforeFieldLocation);
|
||||
}
|
||||
|
||||
const customFields = transformBookingFieldsApiToInternal(inputBookingFields);
|
||||
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
|
||||
|
||||
return [...defaultFieldsBefore, ...customFields, ...defaultFieldsAfter];
|
||||
return [...defaultFieldsBefore, ...customFieldsWithoutNameEmail, ...defaultFieldsAfter];
|
||||
}
|
||||
|
||||
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
|
||||
|
||||
+17
-2
@@ -7,7 +7,7 @@ import {
|
||||
transformLocationsInternalToApi,
|
||||
transformBookingFieldsInternalToApi,
|
||||
parseRecurringEvent,
|
||||
TransformedLocationsSchema,
|
||||
InternalLocationSchema,
|
||||
BookingFieldsSchema,
|
||||
SystemField,
|
||||
CustomField,
|
||||
@@ -20,12 +20,14 @@ import {
|
||||
transformEventTypeColorsInternalToApi,
|
||||
parseEventTypeColor,
|
||||
transformSeatsInternalToApi,
|
||||
InternalLocation,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
BookerLayoutsTransformedSchema,
|
||||
NoticeThresholdTransformedSchema,
|
||||
EventTypeOutput_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
type EventTypeRelations = {
|
||||
@@ -190,7 +192,20 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
|
||||
transformLocations(locations: any) {
|
||||
if (!locations) return [];
|
||||
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
|
||||
|
||||
const knownLocations: InternalLocation[] = [];
|
||||
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
|
||||
|
||||
const parsedLocations = locations.map((location: any) => {
|
||||
const result = InternalLocationSchema.safeParse(location);
|
||||
if (result.success) {
|
||||
return knownLocations.push(result.data);
|
||||
} else {
|
||||
return unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
|
||||
}
|
||||
});
|
||||
|
||||
return [...transformLocationsInternalToApi(knownLocations), ...unknownLocations];
|
||||
}
|
||||
|
||||
transformDestinationCalendar(destinationCalendar?: DestinationCalendar | null) {
|
||||
|
||||
+34
-5
@@ -121,11 +121,13 @@ describe("Schedules Endpoints", () => {
|
||||
expect(outputSchedule?.isDefault).toEqual(expected.isDefault);
|
||||
expect(outputSchedule?.availability.length).toEqual(expectedAvailabilityLength);
|
||||
|
||||
const outputScheduleAvailability = outputSchedule?.availability[0];
|
||||
expect(outputScheduleAvailability).toBeDefined();
|
||||
expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days);
|
||||
expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime);
|
||||
expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime);
|
||||
if (expectedAvailabilityLength) {
|
||||
const outputScheduleAvailability = outputSchedule?.availability[0];
|
||||
expect(outputScheduleAvailability).toBeDefined();
|
||||
expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days);
|
||||
expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime);
|
||||
expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime);
|
||||
}
|
||||
|
||||
expect(JSON.stringify(outputSchedule?.overrides)).toEqual(JSON.stringify(expected.overrides));
|
||||
}
|
||||
@@ -222,6 +224,33 @@ describe("Schedules Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should empty availabilities and overrides", async () => {
|
||||
const body: UpdateScheduleInput_2024_06_11 = {
|
||||
availability: [],
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/schedules/${createdSchedule.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_11)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response: any) => {
|
||||
const responseData: UpdateScheduleOutput_2024_06_11 = response.body;
|
||||
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||
const responseSchedule = responseData.data;
|
||||
|
||||
const expectedSchedule = {
|
||||
...createdSchedule,
|
||||
overrides: body.overrides,
|
||||
availability: body.availability,
|
||||
};
|
||||
outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 0);
|
||||
|
||||
createdSchedule = responseSchedule;
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete schedule", async () => {
|
||||
return request(app.getHttpServer()).delete(`/api/v2/schedules/${createdSchedule.id}`).expect(200);
|
||||
});
|
||||
|
||||
@@ -119,14 +119,17 @@ export class SchedulesRepository_2024_06_11 {
|
||||
timeZone: schedule.timeZone,
|
||||
};
|
||||
|
||||
const availabilitiesAndOverrides: Prisma.AvailabilityCreateManyInput[] = [];
|
||||
const createAvailabilityStatements: Prisma.AvailabilityCreateManyInput[] = [];
|
||||
const createOverridesStatements: Prisma.AvailabilityCreateManyInput[] = [];
|
||||
|
||||
const deleteAvailabilityStatements: Prisma.AvailabilityWhereInput[] = [];
|
||||
const deleteOverridesStatements: Prisma.AvailabilityWhereInput[] = [];
|
||||
|
||||
const deleteConditions = [];
|
||||
if (availability) {
|
||||
// note(Lauris): availabilities and overrides are stored in the same "Availability" table,
|
||||
// but availabilities have "date" field as null, while overrides have it as not null, so delete
|
||||
// condition below results in deleting only rows from Availability table that are availabilities.
|
||||
deleteConditions.push({
|
||||
deleteAvailabilityStatements.push({
|
||||
scheduleId: { equals: scheduleId },
|
||||
date: null,
|
||||
});
|
||||
@@ -136,7 +139,7 @@ export class SchedulesRepository_2024_06_11 {
|
||||
// note(Lauris): availabilities and overrides are stored in the same "Availability" table,
|
||||
// but overrides have "date" field as not-null, while availabilities have it as null, so delete
|
||||
// condition below results in deleting only rows from Availability table that are overrides.
|
||||
deleteConditions.push({
|
||||
deleteOverridesStatements.push({
|
||||
scheduleId: { equals: scheduleId },
|
||||
NOT: { date: null },
|
||||
});
|
||||
@@ -144,7 +147,7 @@ export class SchedulesRepository_2024_06_11 {
|
||||
|
||||
if (availability && availability.length > 0) {
|
||||
availability.forEach((availability) => {
|
||||
availabilitiesAndOverrides.push({
|
||||
createAvailabilityStatements.push({
|
||||
days: availability.days,
|
||||
startTime: availability.startTime,
|
||||
endTime: availability.endTime,
|
||||
@@ -155,7 +158,7 @@ export class SchedulesRepository_2024_06_11 {
|
||||
|
||||
if (overrides && overrides.length > 0) {
|
||||
overrides.forEach((override) => {
|
||||
availabilitiesAndOverrides.push({
|
||||
createOverridesStatements.push({
|
||||
date: override.date,
|
||||
startTime: override.startTime,
|
||||
endTime: override.endTime,
|
||||
@@ -164,11 +167,21 @@ export class SchedulesRepository_2024_06_11 {
|
||||
});
|
||||
}
|
||||
|
||||
if (availabilitiesAndOverrides.length > 0) {
|
||||
const deleteStatements = [...deleteAvailabilityStatements, ...deleteOverridesStatements];
|
||||
const createStatements = [...createAvailabilityStatements, ...createOverridesStatements];
|
||||
|
||||
if (deleteStatements.length > 0) {
|
||||
updateScheduleData.availability = {
|
||||
deleteMany: deleteConditions,
|
||||
deleteMany: deleteStatements,
|
||||
};
|
||||
}
|
||||
|
||||
if (createStatements.length > 0) {
|
||||
updateScheduleData.availability = {
|
||||
// note(Lauris): keep deleteMany statements
|
||||
...(updateScheduleData.availability || {}),
|
||||
createMany: {
|
||||
data: availabilitiesAndOverrides,
|
||||
data: createStatements,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Logger } from "@nestjs/common";
|
||||
import { Request, Response } from "express";
|
||||
import { tap } from "rxjs/operators";
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger("ResponseInterceptor - NestInterceptor");
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler) {
|
||||
const ctx = context.switchToHttp();
|
||||
const request = ctx.getRequest<Request>();
|
||||
@@ -10,7 +13,24 @@ export class ResponseInterceptor implements NestInterceptor {
|
||||
|
||||
const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id";
|
||||
response.setHeader("X-Request-Id", requestId.toString());
|
||||
const { method, url } = request;
|
||||
const startTime = Date.now();
|
||||
|
||||
return next.handle();
|
||||
return next.handle().pipe(
|
||||
tap((data) => {
|
||||
const { statusCode } = response;
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
this.logger.log("Outgoing Response", {
|
||||
requestId,
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
responseTime,
|
||||
responseBody: data,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { Injectable, NestMiddleware } from "@nestjs/common";
|
||||
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
@Injectable()
|
||||
export class RequestIdMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger("RequestIdMiddleware - NestMiddleware");
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
req.headers["X-Request-Id"] = uuid();
|
||||
const requestId = uuid();
|
||||
req.headers["X-Request-Id"] = requestId;
|
||||
const { method, headers, body: requestBody, baseUrl } = req;
|
||||
|
||||
this.logger.log("Incoming Request", {
|
||||
requestId,
|
||||
method,
|
||||
url: baseUrl,
|
||||
headers,
|
||||
requestBody,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,20 @@ import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_0
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, EventTypesModule_2024_06_14],
|
||||
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule],
|
||||
providers: [
|
||||
EventTypesAtomService,
|
||||
MembershipsRepository,
|
||||
CredentialsRepository,
|
||||
UsersRepository,
|
||||
UsersService,
|
||||
SchedulesRepository_2024_06_11,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { EventTypesAppInput } from "@/modules/atoms/inputs/event-types-app.input";
|
||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
VERSION_NEUTRAL,
|
||||
Patch,
|
||||
Body,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
@@ -50,6 +52,26 @@ export class AtomsController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get("event-types-app/:appSlug")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async getAtomEventTypeApp(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("appSlug") appSlug: string,
|
||||
@Query() queryParams: EventTypesAppInput
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const { teamId } = queryParams;
|
||||
|
||||
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user.id, user.name, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
app,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("event-types/:eventTypeId")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@@ -64,4 +86,20 @@ export class AtomsController {
|
||||
data: eventType,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/organizations/:organizationId/teams/:teamId/event-types/:eventTypeId")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async updateAtomTeamEventType(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Body() body: UpdateEventTypeReturn
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
||||
const eventType = await this.eventTypesService.updateTeamEventType(eventTypeId, body, user, teamId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
export class EventTypesAppInput {
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
teamId?: number;
|
||||
}
|
||||
@@ -1,31 +1,54 @@
|
||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
updateEventType,
|
||||
TUpdateEventTypeInputSchema,
|
||||
systemBeforeFieldEmail,
|
||||
getEventTypeById,
|
||||
getEnabledAppsFromCredentials,
|
||||
getAppFromSlug,
|
||||
MembershipRole,
|
||||
} from "@calcom/platform-libraries";
|
||||
import type {
|
||||
App,
|
||||
CredentialDataWithTeamName,
|
||||
LocationOption,
|
||||
TeamQuery,
|
||||
CredentialOwner,
|
||||
TDependencyData,
|
||||
CredentialPayload,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { PrismaClient } from "@calcom/prisma/client";
|
||||
|
||||
type EnabledAppType = App & {
|
||||
credential: CredentialDataWithTeamName;
|
||||
credentials: CredentialDataWithTeamName[];
|
||||
locationOption: LocationOption | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesAtomService {
|
||||
constructor(
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly eventTypeService: EventTypesService_2024_06_14
|
||||
private readonly eventTypeService: EventTypesService_2024_06_14,
|
||||
private readonly teamEventTypeService: OrganizationsEventTypesService
|
||||
) {}
|
||||
|
||||
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
|
||||
this.eventTypeService.checkUserOwnsEventType(user.id, { id: eventTypeId, userId: user.id });
|
||||
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||
|
||||
const isUserOrganizationAdmin = organizationId
|
||||
@@ -45,12 +68,22 @@ export class EventTypesAtomService {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||
if (eventType?.team?.id) {
|
||||
await this.checkTeamOwnsEventType(user.id, eventType.eventType.id, eventType.team.id);
|
||||
} else {
|
||||
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||
}
|
||||
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
|
||||
this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
async updateTeamEventType(
|
||||
eventTypeId: number,
|
||||
body: TUpdateEventTypeInputSchema,
|
||||
user: UserWithProfile,
|
||||
teamId: number
|
||||
) {
|
||||
await this.checkCanUpdateTeamEventType(user.id, eventTypeId, teamId, body.scheduleId);
|
||||
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
|
||||
const bookingFields = [...(body.bookingFields || [])];
|
||||
|
||||
@@ -77,4 +110,168 @@ export class EventTypesAtomService {
|
||||
|
||||
return eventType.eventType;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
|
||||
await this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
|
||||
const bookingFields = [...(body.bookingFields || [])];
|
||||
|
||||
if (
|
||||
!bookingFields.find((field) => field.type === "email") &&
|
||||
!bookingFields.find((field) => field.type === "phone")
|
||||
) {
|
||||
bookingFields.push(systemBeforeFieldEmail);
|
||||
}
|
||||
|
||||
const eventType = await updateEventType({
|
||||
input: { id: eventTypeId, ...body, bookingFields },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbWrite.prisma,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
return eventType.eventType;
|
||||
}
|
||||
|
||||
async checkCanUpdateTeamEventType(userId: number, eventTypeId: number, teamId: number, scheduleId: number) {
|
||||
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
|
||||
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
|
||||
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
|
||||
}
|
||||
|
||||
async checkTeamOwnsEventType(userId: number, eventTypeId: number, teamId: number) {
|
||||
const membership = await this.dbRead.prisma.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
accepted: true,
|
||||
OR: [{ role: "ADMIN" }, { role: "OWNER" }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
eventTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!membership?.team?.eventTypes?.some((item) => item.id === eventTypeId)) {
|
||||
throw new ForbiddenException(
|
||||
`Access denied. Either the team with ID=${teamId} does not own the event type with ID=${eventTypeId}, or your MEMBER role does not have permission to access this resource.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTypesAppIntegration(slug: string, userId: number, userName: string | null, teamId?: number) {
|
||||
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
if (teamId) {
|
||||
const teamsQuery = await this.organizationsTeamsRepository.getUserTeamsById(userId);
|
||||
// If a team is a part of an org then include those apps
|
||||
// Don't want to iterate over these parent teams
|
||||
const filteredTeams: TeamQuery[] = [];
|
||||
const parentTeams: TeamQuery[] = [];
|
||||
// Only loop and grab parent teams if a teamId was given. If not then all teams will be queried
|
||||
if (teamId) {
|
||||
teamsQuery.forEach((team) => {
|
||||
if (team?.parent) {
|
||||
const { parent, ...filteredTeam } = team;
|
||||
filteredTeams.push(filteredTeam);
|
||||
// Only add parent team if it's not already in teamsQuery
|
||||
if (!teamsQuery.some((t) => t.id === parent.id)) {
|
||||
parentTeams.push(parent);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
userTeams = [...teamsQuery, ...parentTeams];
|
||||
const teamAppCredentials: CredentialPayload[] = userTeams.flatMap((teamApp) => {
|
||||
return teamApp.credentials ? teamApp.credentials.flat() : [];
|
||||
});
|
||||
if (teamId) {
|
||||
credentials = teamAppCredentials;
|
||||
} else {
|
||||
credentials = credentials.concat(teamAppCredentials);
|
||||
}
|
||||
}
|
||||
const enabledApps = await getEnabledAppsFromCredentials(credentials, {
|
||||
where: { slug },
|
||||
});
|
||||
const apps = await Promise.all(
|
||||
enabledApps
|
||||
.filter(({ ...app }) => app.slug === slug)
|
||||
.map(
|
||||
async ({
|
||||
credentials: _,
|
||||
credential,
|
||||
key: _2 /* don't leak to frontend */,
|
||||
...app
|
||||
}: EnabledAppType) => {
|
||||
const userCredentialIds = credentials
|
||||
.filter((c) => c.appId === app.slug && !c.teamId)
|
||||
.map((c) => c.id);
|
||||
const invalidCredentialIds = credentials
|
||||
.filter((c) => c.appId === app.slug && c.invalid)
|
||||
.map((c) => c.id);
|
||||
const teams = await Promise.all(
|
||||
credentials
|
||||
.filter((c) => c.appId === app.slug && c.teamId)
|
||||
.map(async (c) => {
|
||||
const team = userTeams.find((team) => team.id === c.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
teamId: team.id,
|
||||
name: team.name,
|
||||
logoUrl: team.logoUrl,
|
||||
credentialId: c.id,
|
||||
isAdmin:
|
||||
team.members[0].role === MembershipRole.ADMIN ||
|
||||
team.members[0].role === MembershipRole.OWNER,
|
||||
};
|
||||
})
|
||||
);
|
||||
const isSetupAlready = credential && app.categories.includes("payment") ? true : undefined;
|
||||
let dependencyData: TDependencyData = [];
|
||||
if (app.dependencies?.length) {
|
||||
dependencyData = app.dependencies.map((dependency) => {
|
||||
const dependencyInstalled = enabledApps.some(
|
||||
(dbAppIterator: EnabledAppType) =>
|
||||
dbAppIterator.credentials.length && dbAppIterator.slug === dependency
|
||||
);
|
||||
// If the app marked as dependency is simply deleted from the codebase,
|
||||
// we can have the situation where App is marked installed in DB but we couldn't get the app.
|
||||
const dependencyName = getAppFromSlug(dependency)?.name;
|
||||
return { name: dependencyName, installed: dependencyInstalled };
|
||||
});
|
||||
}
|
||||
const credentialOwner: CredentialOwner = {
|
||||
name: userName,
|
||||
teamId,
|
||||
};
|
||||
return {
|
||||
...app,
|
||||
...(teams.length && {
|
||||
credentialOwner,
|
||||
}),
|
||||
userCredentialIds,
|
||||
invalidCredentialIds,
|
||||
teams,
|
||||
isInstalled: !!userCredentialIds.length || !!teams.length || app.isGlobal,
|
||||
isSetupAlready,
|
||||
...(app.dependencies && { dependencyData }),
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
return apps[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserInOrgTeam implements CanActivate {
|
||||
constructor(private organizationsRepository: OrganizationsRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
|
||||
const teamId: string = request.params.teamId;
|
||||
const orgId: string = request.params.orgId;
|
||||
const userId: string = request.params.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ForbiddenException("No user id found in request params.");
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
throw new ForbiddenException("No org id found in request params.");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw new ForbiddenException("No team id found in request params.");
|
||||
}
|
||||
|
||||
const user = await this.organizationsRepository.findOrgTeamUser(
|
||||
Number(orgId),
|
||||
Number(teamId),
|
||||
Number(userId)
|
||||
);
|
||||
|
||||
if (user) {
|
||||
request.user = user;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [BookingSeatRepository],
|
||||
exports: [BookingSeatRepository],
|
||||
})
|
||||
export class BookingSeatModule {}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class BookingSeatRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async getByReferenceUid(referenceUid: string) {
|
||||
return this.dbRead.prisma.bookingSeat.findUnique({
|
||||
where: {
|
||||
referenceUid,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class CredentialsRepository {
|
||||
@@ -65,6 +66,18 @@ export class CredentialsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async getAllUserCredentialsById(userId: number) {
|
||||
return await this.dbRead.prisma.credential.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getUserCredentialById(userId: number, credentialId: number, type: string) {
|
||||
return await this.dbRead.prisma.credential.findUnique({
|
||||
where: {
|
||||
|
||||
+24
@@ -248,6 +248,30 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(schedule?.userId).toEqual(userId);
|
||||
}
|
||||
|
||||
it(`should fail /POST using already used managed user email`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: userEmail,
|
||||
timeZone: userTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Alice Smith",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(409);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
const error = responseBody.error;
|
||||
expect(error).toBeDefined();
|
||||
expect(error?.message).toEqual(
|
||||
`User with the provided e-mail already exists. Existing user ID=${postResponseData.user.id}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`/GET: return list of managed users`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0`)
|
||||
|
||||
+2
@@ -34,4 +34,6 @@ export class CreateManagedUserOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => CreateManagedUserData)
|
||||
data!: CreateManagedUserData;
|
||||
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
@@ -26,9 +26,11 @@ export class OAuthClientUsersService {
|
||||
isPlatformManaged: boolean,
|
||||
organizationId?: number
|
||||
) {
|
||||
const existsWithEmail = await this.managedUserExistsWithEmail(oAuthClientId, body.email);
|
||||
if (existsWithEmail) {
|
||||
throw new BadRequestException("User with the provided e-mail already exists.");
|
||||
const existingUser = await this.getExistingUserByEmail(oAuthClientId, body.email);
|
||||
if (existingUser) {
|
||||
throw new ConflictException(
|
||||
`User with the provided e-mail already exists. Existing user ID=${existingUser.id}`
|
||||
);
|
||||
}
|
||||
|
||||
let user: User;
|
||||
@@ -93,10 +95,9 @@ export class OAuthClientUsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async managedUserExistsWithEmail(oAuthClientId: string, email: string) {
|
||||
async getExistingUserByEmail(oAuthClientId: string, email: string) {
|
||||
const oAuthEmail = this.getOAuthUserEmail(oAuthClientId, email);
|
||||
const user = await this.userRepository.findByEmail(oAuthEmail);
|
||||
return !!user;
|
||||
return await this.userRepository.findByEmail(oAuthEmail);
|
||||
}
|
||||
|
||||
async updateOAuthClientUser(oAuthClientId: string, userId: number, body: UpdateManagedUserInput) {
|
||||
|
||||
+9
-6
@@ -387,15 +387,18 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
1
|
||||
);
|
||||
|
||||
const responseTeamEvent = responseBody.data[0];
|
||||
expect(responseTeamEvent?.teamId).toEqual(team.id);
|
||||
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
if (!responseTeamEvent) {
|
||||
throw new Error("Team event not found");
|
||||
}
|
||||
|
||||
const responseTeammate1Event = responseBody.data[1];
|
||||
expect(responseTeammate1Event?.ownerId).toEqual(teammate1.id);
|
||||
const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teammate1.id);
|
||||
expect(responseTeammate1Event).toBeDefined();
|
||||
expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
|
||||
|
||||
const responseTeammate2Event = responseBody.data[2];
|
||||
expect(responseTeammate2Event?.ownerId).toEqual(teammate2.id);
|
||||
const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teammate2.id);
|
||||
expect(responseTeammate2Event).toBeDefined();
|
||||
expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
|
||||
|
||||
managedEventType = responseTeamEvent;
|
||||
|
||||
+10
-7
@@ -2,6 +2,7 @@ import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/inputs/create-organization-team-membership.input";
|
||||
import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/inputs/update-organization-team-membership.input";
|
||||
import { OrgTeamMembershipOutputDto } from "@/modules/organizations/outputs/organization-teams-memberships.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -37,7 +38,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
let nonOrgTeam: Team;
|
||||
let membership: Membership;
|
||||
let membership2: Membership;
|
||||
let membershipCreatedViaApi: Membership;
|
||||
let membershipCreatedViaApi: OrgTeamMembershipOutputDto;
|
||||
|
||||
const userEmail = "org-admin-membership-teams-controller-e2e@api.com";
|
||||
const userEmail2 = "org-member-membership-teams-controller-e2e@api.com";
|
||||
@@ -200,7 +201,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership[]> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data[0].id).toEqual(membership.id);
|
||||
expect(responseBody.data[1].id).toEqual(membership2.id);
|
||||
@@ -219,7 +220,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships?skip=1&take=1`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership[]> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data[0].id).toEqual(membership2.id);
|
||||
expect(responseBody.data[0].userId).toEqual(user2.id);
|
||||
@@ -238,10 +239,11 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membership.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.id).toEqual(membership.id);
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
expect(responseBody.data.user.email).toEqual(user.email);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -272,12 +274,13 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
membershipCreatedViaApi = responseBody.data;
|
||||
expect(membershipCreatedViaApi.teamId).toEqual(orgTeam.id);
|
||||
expect(membershipCreatedViaApi.role).toEqual("MEMBER");
|
||||
expect(membershipCreatedViaApi.userId).toEqual(userToInviteViaApi.id);
|
||||
expect(membershipCreatedViaApi.user.email).toEqual(userToInviteViaApi.email);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,7 +303,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
} satisfies UpdateOrgTeamMembershipDto)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
membershipCreatedViaApi = responseBody.data;
|
||||
expect(membershipCreatedViaApi.role).toEqual("OWNER");
|
||||
@@ -312,7 +315,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.delete(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membershipCreatedViaApi.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.id).toEqual(membershipCreatedViaApi.id);
|
||||
});
|
||||
|
||||
+1
@@ -162,6 +162,7 @@ describe("Organizations Team Endpoints", () => {
|
||||
const responseBody: ApiSuccessResponse<OrgMeTeamOutputDto[]> = response.body;
|
||||
expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)).toBeDefined();
|
||||
expect(responseBody.data.some((t) => t.accepted)).toBeTruthy();
|
||||
expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)?.role).toBe("OWNER");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+7
-6
@@ -67,7 +67,7 @@ export class OrganizationsTeamsController {
|
||||
}
|
||||
|
||||
@Get("/me")
|
||||
@ApiOperation({ summary: "Get team membership for user" })
|
||||
@ApiOperation({ summary: "Get teams membership for user" })
|
||||
@Roles("ORG_MEMBER")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
async getMyTeams(
|
||||
@@ -84,13 +84,14 @@ export class OrganizationsTeamsController {
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: teams.map((team) =>
|
||||
plainToClass(
|
||||
data: teams.map((team) => {
|
||||
const me = team.members.find((member) => member.userId === user.id);
|
||||
return plainToClass(
|
||||
OrgMeTeamOutputDto,
|
||||
{ ...team, accepted: team.members.find((member) => member.userId === user.id)?.accepted ?? false },
|
||||
me ? { ...team, role: me.role, accepted: me.accepted } : team,
|
||||
{ strategy: "excludeAll" }
|
||||
)
|
||||
),
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.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";
|
||||
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 { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Orgs / Teams / Schedules")
|
||||
export class OrganizationsTeamsSchedulesController {
|
||||
constructor(private schedulesService: SchedulesService_2024_06_11) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrgTeam)
|
||||
@Get("/users/:userId/schedules")
|
||||
@DocsTags("Orgs / Teams / Users / Schedules")
|
||||
@ApiOperation({ summary: "Get schedules of a team member" })
|
||||
async getUserSchedules(
|
||||
@Param("userId", ParseIntPipe) userId: number
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const schedules = await this.schedulesService.getUserSchedules(userId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Teams Schedules Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let orgTeam: Team;
|
||||
let nonOrgTeam: Team;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = "org-admin-schedules-teams-controller-e2e@api.com";
|
||||
const userEmail2 = "org-member-schedules-teams-controller-e2e@api.com";
|
||||
const nonOrgUserEmail = "non-org-member-schedules-teams-controller-e2e@api.com";
|
||||
|
||||
const invitedUserEmail = "org-member-invited-schedules-teams-controller-e2e@api.com";
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
let nonOrgUser: User;
|
||||
|
||||
let userToInviteViaApi: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: userEmail2,
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
name: "User 2 schedule",
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
nonOrgUser = await userRepositoryFixture.create({
|
||||
email: nonOrgUserEmail,
|
||||
username: nonOrgUserEmail,
|
||||
});
|
||||
|
||||
userToInviteViaApi = await userRepositoryFixture.create({
|
||||
email: invitedUserEmail,
|
||||
username: invitedUserEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
orgTeam = await teamsRepositoryFixture.create({
|
||||
name: "Org Team",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||
name: "Non Org Team",
|
||||
isOrganization: false,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: orgTeam.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: orgTeam.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: userToInviteViaApi.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: nonOrgUser.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user2.id}`,
|
||||
username: userEmail2,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${userToInviteViaApi.id}`,
|
||||
username: invitedUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: userToInviteViaApi.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(organizationsRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
expect(org).toBeDefined();
|
||||
});
|
||||
|
||||
it("should get all the schedule of the org's team's member", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleOutput_2024_06_11[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.find((d) => d.id === user2Schedule.id)?.name).toEqual(user2Schedule.name);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||
await userRepositoryFixture.deleteByEmail(nonOrgUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/contro
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
@@ -100,6 +101,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsWebhooksService,
|
||||
WebhooksRepository,
|
||||
WebhooksService,
|
||||
OrganizationsEventTypesService,
|
||||
],
|
||||
controllers: [
|
||||
OrganizationsTeamsController,
|
||||
@@ -111,6 +113,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsAttributesController,
|
||||
OrganizationsOptionsAttributesController,
|
||||
OrganizationsWebhooksController,
|
||||
OrganizationsTeamsSchedulesController,
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
||||
@@ -103,6 +103,24 @@ export class OrganizationsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findOrgTeamUser(organizationId: number, teamId: number, userId: number) {
|
||||
return this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
profiles: {
|
||||
some: {
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
some: {
|
||||
teamId: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchOrgAdminApiStatus(organizationId: number) {
|
||||
return this.dbRead.prisma.organizationSettings.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -9,6 +10,14 @@ export class OrgMeTeamOutputDto extends OrgTeamOutputDto {
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly accepted!: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: MembershipRole.MEMBER,
|
||||
enum: [MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER],
|
||||
})
|
||||
@IsEnum([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@Expose()
|
||||
readonly role!: MembershipRole;
|
||||
}
|
||||
|
||||
export class OrgTeamsOutputResponseDto {
|
||||
|
||||
+26
@@ -5,6 +5,27 @@ import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, ValidateNested
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class MembershipUserOutputDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly avatarUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly username?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly email!: string;
|
||||
}
|
||||
|
||||
export class OrgTeamMembershipOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@@ -31,6 +52,11 @@ export class OrgTeamMembershipOutputDto {
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly disableImpersonation?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => MembershipUserOutputDto)
|
||||
@Expose()
|
||||
user!: MembershipUserOutputDto;
|
||||
}
|
||||
|
||||
export class OrgTeamMembershipsOutputResponseDto {
|
||||
|
||||
+5
-1
@@ -9,13 +9,14 @@ export class OrganizationsTeamsMembershipsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async findOrgTeamMembershipsPaginated(organizationId: number, teamId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.membership.findMany({
|
||||
return await this.dbRead.prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: teamId,
|
||||
team: {
|
||||
parentId: organizationId,
|
||||
},
|
||||
},
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
@@ -30,6 +31,7 @@ export class OrganizationsTeamsMembershipsRepository {
|
||||
parentId: organizationId,
|
||||
},
|
||||
},
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
});
|
||||
}
|
||||
async deleteOrgTeamMembershipById(organizationId: number, teamId: number, membershipId: number) {
|
||||
@@ -59,12 +61,14 @@ export class OrganizationsTeamsMembershipsRepository {
|
||||
parentId: organizationId,
|
||||
},
|
||||
},
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
});
|
||||
}
|
||||
|
||||
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.create({
|
||||
data: { ...data, teamId: teamId },
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+52
-1
@@ -4,6 +4,8 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
@@ -97,7 +99,7 @@ export class OrganizationsTeamsRepository {
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: { select: { accepted: true, userId: true } },
|
||||
members: { select: { accepted: true, userId: true, role: true } },
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
@@ -119,4 +121,53 @@ export class OrganizationsTeamsRepository {
|
||||
|
||||
return team.members.map((member) => member.userId);
|
||||
}
|
||||
|
||||
async getUserTeamsById(userId: number) {
|
||||
const userTeams = await this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return userTeams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export class CreateManagedUserInput {
|
||||
@ApiProperty({
|
||||
example: "America/New_York",
|
||||
description: `Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have
|
||||
a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.`,
|
||||
a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.
|
||||
It will default to Europe/London if not passed.`,
|
||||
})
|
||||
timeZone?: string;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -126,7 +126,7 @@ const EventTypeGroup = ({
|
||||
}: ConfigureStepCardProps & {
|
||||
groupIndex: number;
|
||||
setUpdatedEventTypesStatus: Dispatch<SetStateAction<TUpdatedEventTypesStatus>>;
|
||||
submitRefs: Array<React.RefObject<HTMLButtonElement>>;
|
||||
submitRefs: React.MutableRefObject<(HTMLButtonElement | null)[]>;
|
||||
}) => {
|
||||
const { control } = useFormContext<TEventTypesForm>();
|
||||
const { fields, update } = useFieldArray({
|
||||
@@ -174,7 +174,9 @@ const EventTypeGroup = ({
|
||||
return res;
|
||||
});
|
||||
}}
|
||||
ref={submitRefs[index]}
|
||||
ref={(el) => {
|
||||
submitRefs.current[index] = el;
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -193,17 +195,8 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
|
||||
keyName: "fieldId",
|
||||
});
|
||||
const eventTypeGroups = watch("eventTypeGroups");
|
||||
const submitRefs = useRef<Array<Array<React.RefObject<HTMLButtonElement>>>>([]);
|
||||
const submitRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
|
||||
submitRefs.current = eventTypeGroups.reduce(
|
||||
(arr: Array<Array<React.RefObject<HTMLButtonElement>>>, field) => {
|
||||
const res = field.eventTypes
|
||||
.filter((eventType) => eventType.selected)
|
||||
.map((_ref) => React.createRef<HTMLButtonElement>());
|
||||
return [...arr, res];
|
||||
},
|
||||
[]
|
||||
);
|
||||
const mainForSubmitRef = useRef<HTMLButtonElement>(null);
|
||||
const [updatedEventTypesStatus, setUpdatedEventTypesStatus] = useState<TUpdatedEventTypesStatus>(
|
||||
eventTypeGroups.reduce((arr: Array<{ id: number; updated: boolean }[]>, field) => {
|
||||
@@ -245,7 +238,7 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
|
||||
<EventTypeGroup
|
||||
groupIndex={groupIndex}
|
||||
setUpdatedEventTypesStatus={setUpdatedEventTypesStatus}
|
||||
submitRefs={submitRefs.current[groupIndex]}
|
||||
submitRefs={submitRefs}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
@@ -258,7 +251,7 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
|
||||
type="button"
|
||||
data-testid="configure-step-save"
|
||||
onClick={() => {
|
||||
submitRefs.current.map((group) => group?.map((ref) => ref.current?.click()));
|
||||
submitRefs.current.forEach((ref) => ref?.click());
|
||||
setSubmit(true);
|
||||
}}
|
||||
loading={loading}>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
import type { ActionType } from "@calcom/ui";
|
||||
import {
|
||||
Badge,
|
||||
@@ -48,6 +49,7 @@ import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
|
||||
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { ReassignDialog } from "@components/dialog/ReassignDialog";
|
||||
import { RerouteDialog } from "@components/dialog/RerouteDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||
@@ -65,9 +67,43 @@ type BookingItemProps = BookingItem & {
|
||||
};
|
||||
};
|
||||
|
||||
function BookingListItem(booking: BookingItemProps) {
|
||||
const { userId, userTimeZone, userTimeFormat, userEmail } = booking.loggedInUser;
|
||||
type ParsedBooking = ReturnType<typeof buildParsedBooking>;
|
||||
type TeamEvent = Ensure<NonNullable<ParsedBooking["eventType"]>, "team">;
|
||||
type TeamEventBooking = Omit<ParsedBooking, "eventType"> & {
|
||||
eventType: TeamEvent;
|
||||
};
|
||||
type ReroutableBooking = Ensure<TeamEventBooking, "routedFromRoutingFormReponse">;
|
||||
|
||||
function buildParsedBooking(booking: BookingItemProps) {
|
||||
// The way we fetch bookings there could be eventType object even without an eventType, but id confirms its existence
|
||||
const bookingEventType = booking.eventType.id
|
||||
? (booking.eventType as Ensure<
|
||||
typeof booking.eventType,
|
||||
// It would only ensure that the props are present, if they are optional in the original type. So, it is safe to assert here.
|
||||
"id" | "length" | "title" | "slug" | "schedulingType" | "team"
|
||||
>)
|
||||
: null;
|
||||
|
||||
const bookingMetadata = bookingMetadataSchema.parse(booking.metadata ?? null);
|
||||
return {
|
||||
...booking,
|
||||
eventType: bookingEventType,
|
||||
metadata: bookingMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
const isBookingReroutable = (booking: ParsedBooking): booking is ReroutableBooking => {
|
||||
// We support only team bookings for now for rerouting
|
||||
// Though `routedFromRoutingFormReponse` could be there for a non-team booking, we don't want to support it for now.
|
||||
// Let's not support re-routing for a booking without an event-type for now.
|
||||
// Such a booking has its event-type deleted and there might not be something to reroute to.
|
||||
return !!booking.routedFromRoutingFormReponse && !!booking.eventType?.team;
|
||||
};
|
||||
|
||||
function BookingListItem(booking: BookingItemProps) {
|
||||
const parsedBooking = buildParsedBooking(booking);
|
||||
|
||||
const { userTimeZone, userTimeFormat, userEmail } = booking.loggedInUser;
|
||||
const {
|
||||
t,
|
||||
i18n: { language },
|
||||
@@ -77,6 +113,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState<boolean>(false);
|
||||
const cardCharged = booking?.payment[0]?.success;
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
@@ -107,7 +144,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const paymentAppData = getPaymentAppData(booking.eventType);
|
||||
|
||||
const location = booking.location as ReturnType<typeof getEventLocationValue>;
|
||||
const locationVideoCallUrl = bookingMetadataSchema.parse(booking?.metadata || {})?.videoCallUrl;
|
||||
const locationVideoCallUrl = parsedBooking.metadata?.videoCallUrl;
|
||||
|
||||
const { resolvedTheme, forcedTheme } = useGetTheme();
|
||||
const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
|
||||
@@ -191,6 +228,18 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
},
|
||||
},
|
||||
...(isBookingReroutable(parsedBooking)
|
||||
? [
|
||||
{
|
||||
id: "reroute",
|
||||
label: t("reroute"),
|
||||
onClick: () => {
|
||||
setRerouteDialogIsOpen(true);
|
||||
},
|
||||
icon: "waypoints" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "change_location",
|
||||
label: t("edit_location"),
|
||||
@@ -220,6 +269,17 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isBookingInPast) {
|
||||
editBookingActions.push({
|
||||
id: "no_show",
|
||||
label: t("mark_as_no_show"),
|
||||
onClick: () => {
|
||||
setIsNoShowDialogOpen(true);
|
||||
},
|
||||
icon: "eye-off" as const,
|
||||
});
|
||||
}
|
||||
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
@@ -275,6 +335,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
|
||||
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
|
||||
const [rerouteDialogIsOpen, setRerouteDialogIsOpen] = useState(false);
|
||||
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("location_updated"), "success");
|
||||
@@ -360,6 +421,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -367,12 +429,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setIsOpenDialog={setIsOpenRescheduleDialog}
|
||||
bookingUId={booking.uid}
|
||||
/>
|
||||
<ReassignDialog
|
||||
isOpenDialog={isOpenReassignDialog}
|
||||
setIsOpenDialog={setIsOpenReassignDialog}
|
||||
bookingId={booking.id}
|
||||
teamId={booking.eventType?.team?.id || 0}
|
||||
/>
|
||||
{isOpenReassignDialog && (
|
||||
<ReassignDialog
|
||||
isOpenDialog={isOpenReassignDialog}
|
||||
setIsOpenDialog={setIsOpenReassignDialog}
|
||||
bookingId={booking.id}
|
||||
teamId={booking.eventType?.team?.id || 0}
|
||||
/>
|
||||
)}
|
||||
<EditLocationDialog
|
||||
booking={booking}
|
||||
saveLocation={saveLocation}
|
||||
@@ -402,6 +466,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
timeFormat={userTimeFormat ?? null}
|
||||
/>
|
||||
)}
|
||||
{isNoShowDialogOpen && (
|
||||
<NoShowAttendeesDialog
|
||||
bookingUid={booking.uid}
|
||||
attendees={attendeeList}
|
||||
setIsOpen={setIsNoShowDialogOpen}
|
||||
isOpen={isNoShowDialogOpen}
|
||||
/>
|
||||
)}
|
||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
|
||||
<div>
|
||||
@@ -620,6 +692,13 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{isBookingReroutable(parsedBooking) && (
|
||||
<RerouteDialog
|
||||
isOpenDialog={rerouteDialogIsOpen}
|
||||
setIsOpenDialog={setRerouteDialogIsOpen}
|
||||
booking={{ ...parsedBooking, eventType: parsedBooking.eventType }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -722,7 +801,7 @@ const FirstAttendee = ({
|
||||
className=" hover:text-blue-500"
|
||||
href={`mailto:${user.email}`}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
{user.name}
|
||||
{user.name || user.email}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -926,7 +1005,7 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
|
||||
/>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<div className=" flex justify-end p-2">
|
||||
<div className="flex justify-end p-2 ">
|
||||
<Button
|
||||
data-testid="update-no-show"
|
||||
color="secondary"
|
||||
@@ -942,6 +1021,75 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
const NoShowAttendeesDialog = ({
|
||||
attendees,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
bookingUid,
|
||||
}: {
|
||||
attendees: AttendeeProps[];
|
||||
isOpen: boolean;
|
||||
setIsOpen: (value: boolean) => void;
|
||||
bookingUid: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [noShowAttendees, setNoShowAttendees] = useState(
|
||||
attendees.map((attendee) => ({
|
||||
id: attendee.id,
|
||||
email: attendee.email,
|
||||
name: attendee.name,
|
||||
noShow: attendee.noShow || false,
|
||||
}))
|
||||
);
|
||||
|
||||
const noShowMutation = trpc.viewer.markNoShow.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
const newValue = data.attendees[0];
|
||||
setNoShowAttendees((old) =>
|
||||
old.map((attendee) =>
|
||||
attendee.email === newValue.email ? { ...attendee, noShow: newValue.noShow } : attendee
|
||||
)
|
||||
);
|
||||
showToast(t(data.message), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={() => setIsOpen(false)}>
|
||||
<DialogContent title={t("mark_as_no_show_title")} description={t("no_show_description")}>
|
||||
{noShowAttendees.map((attendee) => (
|
||||
<form
|
||||
key={attendee.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
noShowMutation.mutate({
|
||||
bookingUid,
|
||||
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
|
||||
});
|
||||
}}>
|
||||
<div className="bg-muted flex items-center justify-between rounded-md px-4 py-2">
|
||||
<span className="text-emphasis flex flex-col text-sm">
|
||||
{attendee.name}
|
||||
{attendee.email && <span className="text-muted">({attendee.email})</span>}
|
||||
</span>
|
||||
<Button color="minimal" type="submit" StartIcon={attendee.noShow ? "eye-off" : "eye"}>
|
||||
{attendee.noShow ? t("unmark_as_no_show") : t("mark_as_no_show")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
))}
|
||||
<DialogFooter>
|
||||
<DialogClose>{t("done")}</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupedGuests = ({ guests }: { guests: AttendeeProps[] }) => {
|
||||
const [openDropdown, setOpenDropdown] = useState(false);
|
||||
const { t } = useLocale();
|
||||
@@ -978,7 +1126,7 @@ const GroupedGuests = ({ guests }: { guests: AttendeeProps[] }) => {
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<div className=" flex justify-end space-x-2 p-2">
|
||||
<div className="flex justify-end space-x-2 p-2 ">
|
||||
<Link href={`mailto:${selectedEmail}`}>
|
||||
<Button
|
||||
color="secondary"
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { components } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Label,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
ConfirmationDialogContent,
|
||||
showToast,
|
||||
Select,
|
||||
RadioGroup as RadioArea,
|
||||
} from "@calcom/ui";
|
||||
|
||||
type ReassignDialog = {
|
||||
isOpenDialog: boolean;
|
||||
@@ -12,9 +29,46 @@ type ReassignDialog = {
|
||||
bookingId: number;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
reassignType: "round_robin" | "team_member";
|
||||
teamMemberId?: number;
|
||||
};
|
||||
|
||||
export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingId }: ReassignDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const [animationParentRef] = useAutoAnimate<HTMLFormElement>({
|
||||
duration: 150,
|
||||
easing: "ease-in-out",
|
||||
});
|
||||
// Were using legacy list members here because we don't currently have an easy way to paginate a select via infinite scroll
|
||||
const teamMembers = trpc.viewer.teams.getRoundRobinHostsToReassign.useQuery({
|
||||
bookingId,
|
||||
exclude: "fixedHosts",
|
||||
});
|
||||
|
||||
const teamMemberOptions = useMemo(() => {
|
||||
if (teamMembers.isLoading)
|
||||
return [
|
||||
{
|
||||
label: "Loading...",
|
||||
value: 0,
|
||||
status: "unavailable",
|
||||
},
|
||||
];
|
||||
|
||||
return teamMembers.data?.map((member) => ({
|
||||
label: member.name,
|
||||
value: member.id,
|
||||
status: member.status,
|
||||
}));
|
||||
}, [teamMembers]);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
reassignType: "round_robin",
|
||||
},
|
||||
});
|
||||
|
||||
const roundRobinReassignMutation = trpc.viewer.teams.roundRobinReassign.useMutation({
|
||||
onSuccess: async () => {
|
||||
@@ -31,26 +85,134 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
},
|
||||
});
|
||||
|
||||
const roundRobinManualReassignMutation = trpc.viewer.teams.roundRobinManualReassign.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.bookings.get.invalidate();
|
||||
setIsOpenDialog(false);
|
||||
showToast(t("booking_reassigned"), "success");
|
||||
},
|
||||
onError: async (error) => {
|
||||
if (error.message.includes(ErrorCode.NoAvailableUsersFound)) {
|
||||
showToast(t("no_available_hosts"), "error");
|
||||
} else {
|
||||
showToast(t(error.message), "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const handleSubmit = (values: FormValues) => {
|
||||
if (values.reassignType === "round_robin") {
|
||||
roundRobinReassignMutation.mutate({ teamId, bookingId });
|
||||
} else {
|
||||
if (values.teamMemberId) {
|
||||
const selectedMember = teamMemberOptions?.find((member) => member.value === values.teamMemberId);
|
||||
if (selectedMember && selectedMember.status === "unavailable") {
|
||||
setShowConfirmation(true);
|
||||
} else {
|
||||
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId: values.teamMemberId });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const watchedReassignType = form.watch("reassignType");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent title={t("reassign_round_robin_host")} description={t("reassign_to_another_rr_host")}>
|
||||
{/* TODO add team member reassignment*/}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
data-testid="rejection-confirm"
|
||||
loading={roundRobinReassignMutation.isPending}
|
||||
onClick={() => {
|
||||
roundRobinReassignMutation.mutate({ teamId, bookingId });
|
||||
}}>
|
||||
{t("reassign")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<>
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent
|
||||
title={t("reassign_round_robin_host")}
|
||||
description={t("reassign_to_another_rr_host")}
|
||||
enableOverflow>
|
||||
{/* TODO add team member reassignment*/}
|
||||
<Form form={form} handleSubmit={handleSubmit} ref={animationParentRef}>
|
||||
<RadioArea.Group
|
||||
onValueChange={(val) => {
|
||||
form.setValue("reassignType", val as "team_member" | "round_robin");
|
||||
}}
|
||||
className={classNames("mt-1 flex flex-col gap-4")}>
|
||||
<RadioArea.Item
|
||||
value="round_robin"
|
||||
className={classNames("w-full text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
value="team_member"
|
||||
className={classNames("text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("team_member_round_robin_reassign")}</strong>
|
||||
<p>{t("team_member_round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
|
||||
{watchedReassignType === "team_member" && (
|
||||
<div className="mb-2">
|
||||
<Label className="text-emphasis mt-6">{t("select_team_member")}</Label>
|
||||
<Select
|
||||
value={teamMemberOptions?.find((option) => option.value === form.getValues("teamMemberId"))}
|
||||
options={teamMemberOptions}
|
||||
onChange={(event) => {
|
||||
form.setValue("teamMemberId", event?.value);
|
||||
}}
|
||||
components={{
|
||||
Option: ({ children, ...props }) => {
|
||||
const status = props.data?.status;
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={classNames(
|
||||
"h-2 w-2 rounded-full",
|
||||
status === "available" ? "bg-green-500" : "bg-red-500"
|
||||
)}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="rejection-confirm"
|
||||
loading={roundRobinReassignMutation.isPending || roundRobinManualReassignMutation.isPending}>
|
||||
{t("reassign")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showConfirmation} onOpenChange={setShowConfirmation}>
|
||||
<ConfirmationDialogContent
|
||||
variety="warning"
|
||||
title={t("confirm_reassign_unavailable")}
|
||||
confirmBtnText={t("yes_reassign")}
|
||||
cancelBtnText={t("cancel")}
|
||||
onConfirm={() => {
|
||||
const teamMemberId = form.getValues("teamMemberId");
|
||||
if (!teamMemberId) {
|
||||
return;
|
||||
}
|
||||
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId });
|
||||
setShowConfirmation(false);
|
||||
}}>
|
||||
{t("reassign_unavailable_team_member_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,916 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useCallback } from "react";
|
||||
import type { z } from "zod";
|
||||
|
||||
import FormInputFields, {
|
||||
FormInputFieldsSkeleton,
|
||||
} from "@calcom/app-store/routing-forms/components/FormInputFields";
|
||||
import { getAbsoluteEventTypeRedirectUrl } from "@calcom/app-store/routing-forms/getEventTypeRedirectUrl";
|
||||
import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute";
|
||||
import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substituteVariables";
|
||||
import { getUrlSearchParamsToForwardForReroute } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward";
|
||||
import type { FormResponse, LocalRoute } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { createBooking } from "@calcom/features/bookings/lib/create-booking";
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { EventType, User, Team, Attendee, Booking as PrismaBooking } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui";
|
||||
import { Button, Tooltip } from "@calcom/ui";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
const enum ReroutingStatusEnum {
|
||||
REROUTING_NOT_INITIATED = "not_initiated",
|
||||
REROUTING_IN_PROGRESS = "in_progress",
|
||||
REROUTING_COMPLETE = "complete",
|
||||
REROUTING_FAILED = "failed",
|
||||
}
|
||||
|
||||
type ResponseWithForm = RouterOutputs["viewer"]["appRoutingForms"]["getResponseWithFormFields"];
|
||||
|
||||
type BookingToReroute = Pick<PrismaBooking, "metadata" | "responses" | "id" | "uid" | "title" | "status"> & {
|
||||
routedFromRoutingFormReponse: {
|
||||
id: number;
|
||||
};
|
||||
startTime: string;
|
||||
metadata: z.infer<typeof bookingMetadataSchema>;
|
||||
attendees: Pick<Attendee, "locale" | "timeZone" | "email" | "name">[];
|
||||
eventType: Pick<EventType, "length" | "schedulingType" | "title" | "id" | "slug"> & {
|
||||
team: Pick<Team, "slug"> | null;
|
||||
};
|
||||
user: Pick<User, "id" | "name" | "email"> | null;
|
||||
};
|
||||
|
||||
type TeamEventTypeBookingToReroute = Omit<BookingToReroute, "eventType"> & {
|
||||
user: NonNullable<BookingToReroute["user"]>;
|
||||
eventType: Omit<BookingToReroute["eventType"], "team"> & {
|
||||
team: Ensure<Pick<Team, "slug">, "slug">;
|
||||
};
|
||||
};
|
||||
|
||||
type RerouteDialogProps = {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: (isOpen: boolean) => void;
|
||||
booking: BookingToReroute;
|
||||
};
|
||||
|
||||
type ReroutingState =
|
||||
| {
|
||||
type: "reschedule_to_same_event_new_tab" | "reschedule_to_different_event_new_tab";
|
||||
/**
|
||||
* UID of the rescheduled booking
|
||||
*/
|
||||
newBooking: string | null;
|
||||
/**
|
||||
* window for the new tab that is opened for rerouting
|
||||
*/
|
||||
reschedulerWindow: Window;
|
||||
error?: Error;
|
||||
}
|
||||
| {
|
||||
type: "same_timeslot";
|
||||
newBooking: string | null;
|
||||
reschedulerWindow: null;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
type TeamMemberMatchingAttributeLogic = {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
};
|
||||
|
||||
// We can't let user change email identifier field as that would require contact owner information from salesforce which we don't support yet.
|
||||
// Also, this isn't a requirement as well to allow correcting the email
|
||||
const fieldsThatCannotBeCorrected = ["email"];
|
||||
|
||||
function getEventTypeUrlsForTheChosenRoute({
|
||||
chosenRoute,
|
||||
form,
|
||||
reroutingFormResponses,
|
||||
currentResponse,
|
||||
teamMemberIdsMatchingAttributeLogic,
|
||||
booking,
|
||||
searchParams,
|
||||
}: {
|
||||
form: ResponseWithForm["form"];
|
||||
currentResponse: FormResponse;
|
||||
reroutingFormResponses: Record<string, Pick<FormResponse[keyof FormResponse], "value">>;
|
||||
teamMemberIdsMatchingAttributeLogic: number[] | null;
|
||||
chosenRoute: LocalRoute | null;
|
||||
booking: TeamEventTypeBookingToReroute;
|
||||
searchParams: URLSearchParams;
|
||||
}) {
|
||||
if (!chosenRoute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formFields = form.fields || [];
|
||||
const routedFromRoutingFormReponseId = booking.routedFromRoutingFormReponse.id;
|
||||
|
||||
const allURLSearchParams = getUrlSearchParamsToForwardForReroute({
|
||||
formResponse: currentResponse,
|
||||
formResponseId: routedFromRoutingFormReponseId,
|
||||
fields: formFields,
|
||||
searchParams,
|
||||
teamMembersMatchingAttributeLogic: teamMemberIdsMatchingAttributeLogic,
|
||||
attributeRoutingConfig: chosenRoute.attributeRoutingConfig ?? null,
|
||||
rescheduleUid: booking.uid,
|
||||
reroutingFormResponses,
|
||||
});
|
||||
|
||||
const eventFullSlug = substituteVariables(chosenRoute.action.value, currentResponse, formFields);
|
||||
const eventBookingAbsoluteUrl = getAbsoluteEventTypeRedirectUrl({
|
||||
form,
|
||||
eventTypeRedirectUrl: eventFullSlug,
|
||||
allURLSearchParams,
|
||||
isEmbed: false,
|
||||
});
|
||||
|
||||
return { eventFullSlug, eventBookingAbsoluteUrl };
|
||||
}
|
||||
|
||||
function rescheduleInNewTab({ url }: { url: string }) {
|
||||
const reschedulerWindow = window.open(url, "_blank");
|
||||
|
||||
if (!reschedulerWindow) {
|
||||
throw new Error("Failed to open new tab");
|
||||
}
|
||||
|
||||
return reschedulerWindow;
|
||||
}
|
||||
|
||||
const getFullSlugForEvent = (
|
||||
eventType: Pick<TeamEventTypeBookingToReroute["eventType"], "team" | "slug">
|
||||
) => {
|
||||
return `team/${eventType.team.slug}/${eventType.slug}`;
|
||||
};
|
||||
|
||||
function isBookingTimeslotInPast(booking: BookingToReroute) {
|
||||
return dayjs(booking.startTime).isBefore(dayjs());
|
||||
}
|
||||
|
||||
const useReroutingState = ({ isOpenDialog }: Pick<RerouteDialogProps, "isOpenDialog">) => {
|
||||
const [value, setValue] = useState<ReroutingState | null>(null);
|
||||
const state = value;
|
||||
|
||||
// If dialog is closed but the rerouting window is still open, close it
|
||||
if (!isOpenDialog && state?.reschedulerWindow) {
|
||||
state.reschedulerWindow.close();
|
||||
setValue(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (state?.reschedulerWindow?.closed) {
|
||||
// Ensure that render happens again
|
||||
setValue(null);
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 1);
|
||||
|
||||
return () => clearInterval(checkInterval);
|
||||
}, [value, setValue]);
|
||||
|
||||
const status = (() => {
|
||||
if (!value) return ReroutingStatusEnum.REROUTING_NOT_INITIATED;
|
||||
if (value.error) return ReroutingStatusEnum.REROUTING_FAILED;
|
||||
if (!!value.newBooking) return ReroutingStatusEnum.REROUTING_COMPLETE;
|
||||
return ReroutingStatusEnum.REROUTING_IN_PROGRESS;
|
||||
})();
|
||||
|
||||
return { value: state, setValue, status };
|
||||
};
|
||||
|
||||
// const useUpdateIsReroutingQueryParam = ({ isOpenDialog }: { isOpenDialog: boolean }) => {
|
||||
// const { setQuery: setIsReroutingQuery } = useRouterQuery("isRerouting");
|
||||
// useEffect(() => {
|
||||
// if (isOpenDialog) {
|
||||
// setIsReroutingQuery("true");
|
||||
// } else {
|
||||
// setIsReroutingQuery(undefined);
|
||||
// }
|
||||
// }, [isOpenDialog]);
|
||||
// };
|
||||
|
||||
const gotoBookingPage = ({
|
||||
booking,
|
||||
router,
|
||||
}: {
|
||||
booking: Pick<PrismaBooking, "uid">;
|
||||
router: ReturnType<typeof useRouter>;
|
||||
}) => {
|
||||
// TODO: Do we need to send other params as well?
|
||||
router.push(`/booking/${booking.uid}?cal.rerouting=true`);
|
||||
};
|
||||
|
||||
const useEventListeners = ({ reroutingState }: { reroutingState: ReturnType<typeof useReroutingState> }) => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const messageListener = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
if (event.data.type !== "CAL:rescheduleBookingSuccessfulV2") {
|
||||
return;
|
||||
}
|
||||
const calEventData = event.data.data;
|
||||
showToast(t("rerouted_booking_successfully_redirecting_to_booking_page"), "success");
|
||||
if (!calEventData.uid) {
|
||||
console.error("Booking UID is not there");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
const newBookingId = calEventData.uid;
|
||||
reroutingState.setValue((prev) => {
|
||||
if (!prev) return null;
|
||||
return { ...prev, newBooking: newBookingId };
|
||||
});
|
||||
gotoBookingPage({ router, booking: { uid: newBookingId } });
|
||||
reroutingState.value?.reschedulerWindow?.close();
|
||||
},
|
||||
[reroutingState, reroutingState.value?.reschedulerWindow]
|
||||
);
|
||||
|
||||
// Make sure to close the rescheduler window when this tab closes/reloads
|
||||
const beforeUnloadListener = useCallback(() => {
|
||||
reroutingState.value?.reschedulerWindow?.close();
|
||||
reroutingState.setValue(null);
|
||||
}, [reroutingState.value?.reschedulerWindow]);
|
||||
|
||||
// Ensure listeners are added once the component is mounted and removed once the component is unmounted
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", messageListener);
|
||||
window.addEventListener("beforeunload", beforeUnloadListener);
|
||||
return () => {
|
||||
window.removeEventListener("message", messageListener);
|
||||
window.removeEventListener("beforeunload", beforeUnloadListener);
|
||||
};
|
||||
// messageListener and beforeUnloadListener are memoized using useCallback, so the reference remains stable
|
||||
}, [messageListener, beforeUnloadListener]);
|
||||
};
|
||||
|
||||
const NewRoutingManager = ({
|
||||
chosenRoute,
|
||||
booking,
|
||||
form,
|
||||
currentResponse,
|
||||
teamMembersMatchingAttributeLogic,
|
||||
reroutingState,
|
||||
}: {
|
||||
chosenRoute: LocalRoute;
|
||||
booking: TeamEventTypeBookingToReroute;
|
||||
form: ResponseWithForm["form"];
|
||||
currentResponse: FormResponse;
|
||||
teamMembersMatchingAttributeLogic: {
|
||||
isPending: boolean;
|
||||
data: TeamMemberMatchingAttributeLogic[] | null;
|
||||
};
|
||||
reroutingState: ReturnType<typeof useReroutingState>;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const bookerUrl = useBookerUrl();
|
||||
const teamMemberIdsMatchingAttributeLogic =
|
||||
teamMembersMatchingAttributeLogic?.data?.map((member) => member.id).filter((id) => {
|
||||
// We don't want to reroute to the same user who booked the booking
|
||||
return id !== booking.user?.id;
|
||||
}) || null;
|
||||
const routedFromRoutingFormReponseId = booking.routedFromRoutingFormReponse.id;
|
||||
|
||||
const bookingEventType = booking.eventType;
|
||||
|
||||
// Provide a reason for rescheduling that can be customized by user in the future
|
||||
// const rescheduleReason = "Rerouted";
|
||||
const isRoundRobinScheduling = bookingEventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
||||
const reroutingFormResponses = Object.fromEntries(
|
||||
Object.entries(currentResponse).map(([key, response]) => [key, { value: response.value }])
|
||||
);
|
||||
|
||||
const chosenEventUrls = getEventTypeUrlsForTheChosenRoute({
|
||||
chosenRoute,
|
||||
form,
|
||||
reroutingFormResponses,
|
||||
teamMemberIdsMatchingAttributeLogic,
|
||||
booking,
|
||||
currentResponse,
|
||||
searchParams: new URLSearchParams({
|
||||
// rescheduleReason
|
||||
}),
|
||||
});
|
||||
|
||||
const chosenEventTypeId = chosenRoute.action.eventTypeId;
|
||||
const enableChosenEventTypeQuery = !!chosenEventTypeId;
|
||||
// TODO: Get bare minimum eventType details
|
||||
const { data: chosenEventTypeData, isPending: isChosenEventTypePending } =
|
||||
trpc.viewer.eventTypes.get.useQuery(
|
||||
// enabled prop ensures that the query is not run if the chosenEventTypeId is not there
|
||||
{ id: chosenEventTypeId! },
|
||||
{
|
||||
enabled: enableChosenEventTypeQuery,
|
||||
}
|
||||
);
|
||||
|
||||
// isPending of the query is true even if enabled=false and no request was sent. So, identify if the request is actually in progress
|
||||
const isChosenEventTypeRequestInProgress = enableChosenEventTypeQuery ? isChosenEventTypePending : false;
|
||||
|
||||
const chosenEventType = chosenEventTypeData?.eventType;
|
||||
|
||||
const currentBookingEventFullSlug = getFullSlugForEvent(booking.eventType);
|
||||
const isReroutingToDifferentEvent = currentBookingEventFullSlug !== chosenEventUrls?.eventFullSlug;
|
||||
|
||||
const createBookingMutation = useMutation({
|
||||
mutationFn: createBooking,
|
||||
onSuccess: (booking) => {
|
||||
showToast(t("rerouted_booking_successfully_redirecting_to_booking_page"), "success");
|
||||
if (!booking.uid) {
|
||||
console.error("Booking UID is not there");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
const newBookingId = booking.uid;
|
||||
reroutingState.setValue((prev) => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
newBooking: newBookingId,
|
||||
};
|
||||
});
|
||||
gotoBookingPage({ router, booking: { uid: newBookingId } });
|
||||
},
|
||||
onError: (err, _, ctx) => {
|
||||
reroutingState.setValue({
|
||||
error: err,
|
||||
newBooking: null,
|
||||
type: "same_timeslot",
|
||||
reschedulerWindow: null,
|
||||
});
|
||||
showToast(
|
||||
"Looks like the timeslot is not available for the new team members. Try rescheduling with different timeslot",
|
||||
"error"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
useEventListeners({ reroutingState });
|
||||
|
||||
if (!chosenRoute) return null;
|
||||
|
||||
const isReroutingInNewTab =
|
||||
reroutingState.value?.type === "reschedule_to_same_event_new_tab" ||
|
||||
reroutingState.value?.type === "reschedule_to_different_event_new_tab";
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex flex-col space-y-3 rounded-md p-4 text-sm">
|
||||
<h2 className="text-emphasis font-medium">{t("new_routing_status")}</h2>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{reroutingState.status === ReroutingStatusEnum.REROUTING_NOT_INITIATED && reroutingPreview()}
|
||||
{(reroutingState.status === ReroutingStatusEnum.REROUTING_NOT_INITIATED || !isReroutingInNewTab) &&
|
||||
reroutingCTAs()}
|
||||
{isReroutingInNewTab && newTabReroutingStatus()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function rescheduleToSameTimeslotOfEvent({
|
||||
eventType: { id: eventTypeId, slug: eventTypeSlug, team: eventTypeTeam, length: eventTypeLength },
|
||||
}: {
|
||||
eventType: Pick<TeamEventTypeBookingToReroute["eventType"], "id" | "slug" | "length"> & {
|
||||
team: {
|
||||
slug: string | null;
|
||||
} | null;
|
||||
};
|
||||
}) {
|
||||
if (!chosenRoute) {
|
||||
console.error("Chosen route must be there for rerouting");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
if (!eventTypeTeam?.slug) {
|
||||
console.error("Event type team slug must be there for rerouting");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
const teamSlug = eventTypeTeam.slug;
|
||||
|
||||
if (isBookingTimeslotInPast(booking)) {
|
||||
showToast("You cannot reschedule to a past timeslot", "error");
|
||||
return;
|
||||
}
|
||||
const booker = booking.attendees[0];
|
||||
const getFieldsThatRemainSame = () => {
|
||||
return {
|
||||
responses: booking.responses,
|
||||
timeZone: booker.timeZone,
|
||||
language: booker.locale || "en",
|
||||
metadata: booking.metadata || {},
|
||||
...getBookingSeatFields(),
|
||||
...getHashedLinkFields(),
|
||||
};
|
||||
|
||||
function getBookingSeatFields() {
|
||||
// During rerouting, do all the seats need to be re-routed?
|
||||
// Let's not support re-routing for booking with seats for now.
|
||||
return {
|
||||
// bookingUid: booking.uid,
|
||||
// seatReferenceUid: booking.eventType,
|
||||
};
|
||||
}
|
||||
|
||||
function getHashedLinkFields() {
|
||||
// After booking hashedLink is deleted, so I don't believe we need to return anything here.
|
||||
// Also, Rerouting is for a booking that was routed and routing doesn't involve hashed link even if it is enabled for the event.
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const getFieldFromEventTypeThatRemainSame = () => {
|
||||
return {
|
||||
user: teamSlug,
|
||||
eventTypeId: eventTypeId,
|
||||
eventTypeSlug: eventTypeSlug,
|
||||
};
|
||||
};
|
||||
|
||||
const getTimeslotFields = () => {
|
||||
return {
|
||||
// It is possible that EvenType changed its duration, so we would consider that as well.
|
||||
start: dayjs(booking.startTime).format(),
|
||||
end: dayjs(booking.startTime)
|
||||
// Defaults to the default event length in case no custom duration is set.
|
||||
.add(eventTypeLength, "minute")
|
||||
.format(),
|
||||
};
|
||||
};
|
||||
|
||||
const getRoutingFormRelatedFields = () => {
|
||||
return {
|
||||
routedTeamMemberIds: teamMemberIdsMatchingAttributeLogic,
|
||||
routingFormResponseId: routedFromRoutingFormReponseId,
|
||||
skipContactOwner: chosenRoute.attributeRoutingConfig?.skipContactOwner,
|
||||
};
|
||||
};
|
||||
|
||||
const getSalesforceContactOwnerFields = () => {
|
||||
// Email field won't need to be corrected, so we don't need to support it.
|
||||
return {};
|
||||
};
|
||||
|
||||
// TODO: Long term, we should refactor handleNewBooking and use a different route specific for this purpose,
|
||||
createBookingMutation.mutate({
|
||||
rescheduleUid: booking.uid,
|
||||
// rescheduleReason,
|
||||
reroutingFormResponses: reroutingFormResponses,
|
||||
...getTimeslotFields(),
|
||||
...getFieldsThatRemainSame(),
|
||||
...getFieldFromEventTypeThatRemainSame(),
|
||||
...getRoutingFormRelatedFields(),
|
||||
...getSalesforceContactOwnerFields(),
|
||||
});
|
||||
|
||||
reroutingState.setValue({
|
||||
newBooking: null,
|
||||
type: "same_timeslot",
|
||||
reschedulerWindow: null,
|
||||
});
|
||||
}
|
||||
|
||||
function rescheduleToSameTimeslotOfSameEvent() {
|
||||
return rescheduleToSameTimeslotOfEvent({
|
||||
eventType: booking.eventType,
|
||||
});
|
||||
}
|
||||
|
||||
function rescheduleToSameTimeslotOfChosenEvent() {
|
||||
if (!chosenEventTypeId && isReroutingToDifferentEvent) {
|
||||
showToast(
|
||||
"We don't have ID for the new event. Please go to the Routing Form and save it again.",
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!chosenEventType) {
|
||||
// There are disabled/loading state on button to ensure that this function is not called if chosenEventType is not there
|
||||
console.error("Chosen event type must be there for rerouting");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
return rescheduleToSameTimeslotOfEvent({
|
||||
eventType: chosenEventType,
|
||||
});
|
||||
}
|
||||
|
||||
function rescheduleSameEventInNewTab() {
|
||||
if (!chosenEventUrls?.eventBookingAbsoluteUrl) {
|
||||
console.error("URL must be there for opening new tab");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
const reschedulerWindow = rescheduleInNewTab({
|
||||
url: chosenEventUrls.eventBookingAbsoluteUrl,
|
||||
});
|
||||
|
||||
reroutingState.setValue({
|
||||
type: "reschedule_to_same_event_new_tab",
|
||||
reschedulerWindow,
|
||||
newBooking: null,
|
||||
});
|
||||
}
|
||||
|
||||
function rescheduleDifferentEventInNewTab() {
|
||||
if (!chosenEventUrls?.eventBookingAbsoluteUrl) {
|
||||
console.error("URL must be there for opening new tab");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
const reschedulerWindow = rescheduleInNewTab({
|
||||
url: chosenEventUrls.eventBookingAbsoluteUrl,
|
||||
});
|
||||
|
||||
console.log("SETTING NEW VALUE", {
|
||||
type: "reschedule_to_different_event_new_tab",
|
||||
reschedulerWindow,
|
||||
newBooking: null,
|
||||
});
|
||||
reroutingState.setValue({
|
||||
type: "reschedule_to_different_event_new_tab",
|
||||
reschedulerWindow,
|
||||
newBooking: null,
|
||||
});
|
||||
|
||||
console.log("VALUE NOW", JSON.stringify(reroutingState));
|
||||
}
|
||||
|
||||
function reroutingPreview() {
|
||||
if (chosenRoute.action.type === RouteActionType.CustomPageMessage) {
|
||||
return <span className="text-attention">{t("reroute_preview_custom_message")}</span>;
|
||||
}
|
||||
if (chosenRoute.action.type === RouteActionType.ExternalRedirectUrl) {
|
||||
return (
|
||||
<span className="text-attention">
|
||||
{t("reroute_preview_external_redirect", { externalUrl: chosenRoute.action.value })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (chosenRoute.action.type === RouteActionType.EventTypeRedirectUrl) {
|
||||
// Computed value for eventTypeSlugToRedirect
|
||||
const eventTypeSlugToRedirect =
|
||||
chosenRoute.action.type === RouteActionType.EventTypeRedirectUrl ? chosenRoute.action.value : null;
|
||||
|
||||
if (!eventTypeSlugToRedirect) {
|
||||
console.error("Event type slug to redirect must be there if action is EventTypeRedirectUrl");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<span className="text-default" data-testid="reroute-preview-event-type">
|
||||
<span className="font-semibold">{t("event_type")}:</span>{" "}
|
||||
<a
|
||||
className="underline"
|
||||
target="_blank"
|
||||
href={`${bookerUrl}/${eventTypeSlugToRedirect}`}
|
||||
rel="noreferrer"
|
||||
data-testid="test-routing-result">
|
||||
{eventTypeSlugToRedirect}
|
||||
</a>
|
||||
</span>
|
||||
<span className="text-default" data-testid="reroute-preview-hosts">
|
||||
<Tooltip content="Includes fixed hosts always and assumes that all the team members mentioned here are assigned to the event">
|
||||
<span className="font-semibold">
|
||||
{isRoundRobinScheduling ? t("reroute_preview_possible_host") : t("hosts")}:
|
||||
</span>
|
||||
</Tooltip>{" "}
|
||||
{bookingHosts()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function bookingHosts() {
|
||||
if (teamMembersMatchingAttributeLogic.isPending) return t("loading");
|
||||
|
||||
const hostEmails = teamMembersMatchingAttributeLogic.data?.map((member) => member.email).join(", ");
|
||||
return hostEmails || t("no_matching_members");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function reroutingCTAs() {
|
||||
if (chosenRoute.action.type !== RouteActionType.EventTypeRedirectUrl) {
|
||||
// There are no actions to perform if the new chosen route isn't an event type redirect
|
||||
return null;
|
||||
}
|
||||
const shouldDisableCTAs = teamMembersMatchingAttributeLogic.isPending || createBookingMutation.isPending;
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{!isReroutingToDifferentEvent ? (
|
||||
<Button onClick={rescheduleSameEventInNewTab} disabled={shouldDisableCTAs}>
|
||||
{t("reschedule_with_different_timeslot")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={rescheduleDifferentEventInNewTab} disabled={shouldDisableCTAs}>
|
||||
{t("reschedule_to_the_new_event_with_different_timeslot")}
|
||||
</Button>
|
||||
)}
|
||||
{!isReroutingToDifferentEvent ? (
|
||||
<Button
|
||||
disabled={shouldDisableCTAs}
|
||||
loading={createBookingMutation.isPending}
|
||||
onClick={rescheduleToSameTimeslotOfSameEvent}>
|
||||
{t("reschedule_with_same_timeslot")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={shouldDisableCTAs}
|
||||
loading={createBookingMutation.isPending || isChosenEventTypeRequestInProgress}
|
||||
onClick={rescheduleToSameTimeslotOfChosenEvent}>
|
||||
{t("reschedule_with_same_timeslot_of_new_event")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function newTabReroutingStatus() {
|
||||
if (reroutingState.status !== ReroutingStatusEnum.REROUTING_IN_PROGRESS) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span className="text-attention">
|
||||
<span>Continue with rerouting in the new</span>{" "}
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
className="text-attention underline"
|
||||
onClick={() => reroutingState.value?.reschedulerWindow?.focus()}>
|
||||
tab
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const CurrentRoutingStatus = ({
|
||||
booking,
|
||||
}: {
|
||||
booking: Pick<TeamEventTypeBookingToReroute, "eventType" | "user" | "attendees">;
|
||||
}) => {
|
||||
const fullSlug = getFullSlugForEvent(booking.eventType);
|
||||
const { t } = useLocale();
|
||||
const bookerUrl = useBookerUrl();
|
||||
if (!fullSlug) return null;
|
||||
return (
|
||||
<div className="bg-muted flex flex-col space-y-3 rounded-md p-4 text-sm">
|
||||
<h2 className="text-emphasis font-medium">{t("current_routing_status")}</h2>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<span className="text-default" data-testid="current-routing-status-event-type">
|
||||
<span className="font-semibold">{t("event_type")}:</span>{" "}
|
||||
<a
|
||||
className="underline"
|
||||
target="_blank"
|
||||
href={`${bookerUrl}/${fullSlug}`}
|
||||
rel="noreferrer"
|
||||
data-testid="test-routing-result">
|
||||
{fullSlug}
|
||||
</a>
|
||||
</span>
|
||||
<span className="text-default" data-testid="current-routing-status-organizer">
|
||||
<span className="font-semibold">{t("organizer")}:</span> <span>{booking.user.email}</span>
|
||||
</span>
|
||||
<span className="text-default" data-testid="current-routing-status-attendees">
|
||||
<span className="font-semibold">{t("attendees")}:</span>{" "}
|
||||
<span>{booking.attendees.map((attendee) => attendee.email).join(", ")}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RerouteDialogContentAndFooter = ({
|
||||
booking,
|
||||
isOpenDialog,
|
||||
setIsOpenDialog,
|
||||
}: Pick<RerouteDialogProps, "isOpenDialog" | "setIsOpenDialog"> & {
|
||||
booking: TeamEventTypeBookingToReroute;
|
||||
}) => {
|
||||
const {
|
||||
data: responseWithForm,
|
||||
isPending: isRoutingFormLoading,
|
||||
error: formResponseFetchError,
|
||||
} = trpc.viewer.appRoutingForms.getResponseWithFormFields.useQuery({
|
||||
formResponseId: booking.routedFromRoutingFormReponse.id,
|
||||
});
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
if (isRoutingFormLoading)
|
||||
return (
|
||||
<>
|
||||
<FormInputFieldsSkeleton />
|
||||
<DialogFooter>
|
||||
<Button color="secondary" onClick={() => setIsOpenDialog(false)}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button disabled data-testid="verify-new-route-button">
|
||||
{t("verify_new_route")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
|
||||
if (formResponseFetchError) {
|
||||
return <div className="mb-8">{formResponseFetchError.message}</div>;
|
||||
}
|
||||
|
||||
if (!responseWithForm) return <div className="mb-8">{t("something_went_wrong")}</div>;
|
||||
|
||||
return (
|
||||
<RerouteDialogContentAndFooterWithFormResponse
|
||||
isOpenDialog={isOpenDialog}
|
||||
setIsOpenDialog={setIsOpenDialog}
|
||||
responseWithForm={responseWithForm}
|
||||
booking={booking}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RerouteDialogContentAndFooterWithFormResponse = ({
|
||||
responseWithForm,
|
||||
booking,
|
||||
isOpenDialog,
|
||||
setIsOpenDialog,
|
||||
}: Pick<RerouteDialogProps, "isOpenDialog" | "setIsOpenDialog"> & {
|
||||
responseWithForm: ResponseWithForm;
|
||||
booking: TeamEventTypeBookingToReroute;
|
||||
}) => {
|
||||
const form = responseWithForm.form;
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [responseFromOrganizer, setResponseFromOrganizer] = useState<FormResponse>({});
|
||||
const isResponseFromOrganizerUnpopulated = Object.keys(responseFromOrganizer).length === 0;
|
||||
const currentResponse = isResponseFromOrganizerUnpopulated
|
||||
? responseWithForm.response
|
||||
: responseFromOrganizer;
|
||||
const [chosenRoute, setChosenRoute] = useState<LocalRoute | null>(null);
|
||||
|
||||
const reroutingState = useReroutingState({
|
||||
isOpenDialog,
|
||||
});
|
||||
|
||||
const [teamMembersMatchingAttributeLogic, setTeamMembersMatchingAttributeLogic] = useState<
|
||||
| {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
}[]
|
||||
| null
|
||||
>([]);
|
||||
|
||||
const findTeamMembersMatchingAttributeLogicMutation =
|
||||
trpc.viewer.appRoutingForms.findTeamMembersMatchingAttributeLogic.useMutation({
|
||||
onSuccess(data) {
|
||||
setTeamMembersMatchingAttributeLogic(data.result);
|
||||
},
|
||||
});
|
||||
|
||||
function verifyRoute() {
|
||||
// Reset all states
|
||||
reroutingState.setValue(null);
|
||||
setTeamMembersMatchingAttributeLogic(null);
|
||||
|
||||
const route = findMatchingRoute({
|
||||
form,
|
||||
response: currentResponse,
|
||||
});
|
||||
|
||||
setChosenRoute(route || null);
|
||||
|
||||
if (!route) return;
|
||||
|
||||
findTeamMembersMatchingAttributeLogicMutation.mutate({
|
||||
formId: form.id,
|
||||
response: currentResponse,
|
||||
routeId: route.id,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Link
|
||||
className="text-emphasis text-semibold font-cal mb-4 flex underline"
|
||||
href={`/apps/routing-forms/form-edit/${form.id}`}
|
||||
target="_blank">
|
||||
{form.name}
|
||||
</Link>
|
||||
<FormInputFields
|
||||
response={currentResponse}
|
||||
disabledFields={fieldsThatCannotBeCorrected}
|
||||
form={form}
|
||||
setResponse={setResponseFromOrganizer}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<CurrentRoutingStatus booking={booking} />
|
||||
{chosenRoute && (
|
||||
<NewRoutingManager
|
||||
chosenRoute={chosenRoute}
|
||||
booking={booking}
|
||||
teamMembersMatchingAttributeLogic={{
|
||||
isPending: findTeamMembersMatchingAttributeLogicMutation.isPending,
|
||||
data: teamMembersMatchingAttributeLogic,
|
||||
}}
|
||||
reroutingState={reroutingState}
|
||||
form={form}
|
||||
currentResponse={currentResponse}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderFooter()}
|
||||
</div>
|
||||
);
|
||||
|
||||
function renderFooter() {
|
||||
if (reroutingState.status !== ReroutingStatusEnum.REROUTING_COMPLETE) {
|
||||
return (
|
||||
<DialogFooter>
|
||||
<Button color="secondary" onClick={() => setIsOpenDialog(false)}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={verifyRoute}
|
||||
data-testid="verify-new-route-button"
|
||||
disabled={reroutingState.status === ReroutingStatusEnum.REROUTING_IN_PROGRESS}>
|
||||
{t("verify_new_route")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
);
|
||||
}
|
||||
|
||||
if (reroutingState.status === ReroutingStatusEnum.REROUTING_COMPLETE) {
|
||||
return (
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsOpenDialog(false);
|
||||
}}>
|
||||
{t("close")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: RerouteDialogProps) => {
|
||||
const { t } = useLocale();
|
||||
const bookingEventType = booking.eventType;
|
||||
|
||||
if (!bookingEventType.team) {
|
||||
console.error("Only team event can be re-routed");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
if (!bookingEventType.team.slug) {
|
||||
console.error("Team slug must be there for rerouting");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
if (!booking.user) {
|
||||
// Not a hard requirement, but better to be strict with data.
|
||||
console.error("Booking must have a user");
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
|
||||
const teamEventTypeBooking = {
|
||||
...booking,
|
||||
user: booking.user,
|
||||
eventType: {
|
||||
...bookingEventType,
|
||||
team: {
|
||||
...bookingEventType.team,
|
||||
slug: bookingEventType.team.slug,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent enableOverflow preventCloseOnOutsideClick>
|
||||
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
|
||||
<RerouteDialogContentAndFooter
|
||||
booking={teamEventTypeBooking}
|
||||
isOpenDialog={isOpenDialog}
|
||||
setIsOpenDialog={setIsOpenDialog}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,497 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { RerouteDialog } from "../RerouteDialog";
|
||||
|
||||
const mockRouter = {
|
||||
push: vi.fn((path: string) => {
|
||||
return;
|
||||
}),
|
||||
};
|
||||
|
||||
vi.mock("next/navigation", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("next/navigation")>();
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => mockRouter),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/app-store/routing-forms/lib/processRoute", () => ({
|
||||
findMatchingRoute: vi.fn(({ form, response }) => {
|
||||
return form.routes.find((route: any) => route.__testMatching);
|
||||
}),
|
||||
}));
|
||||
|
||||
const fakeNewTabWindow = {
|
||||
close: vi.fn(),
|
||||
};
|
||||
|
||||
const mockOpen = vi.fn((_url: string) => {
|
||||
return fakeNewTabWindow;
|
||||
});
|
||||
|
||||
vi.stubGlobal("open", mockOpen);
|
||||
|
||||
vi.mock("@calcom/app-store/routing-forms/components/FormInputFields", () => ({
|
||||
default: vi.fn(({ response, form, setResponse, disabledFields }) => {
|
||||
return (
|
||||
<div data-testid="mock-form-input-fields">
|
||||
{form.fields?.map((field: any) => (
|
||||
<div key={field.id}>
|
||||
<label data-testid={`mock-form-field-${field.id}-label`}>{field.label}</label>
|
||||
<div data-testid={`mock-form-field-${field.id}-value`}>{response[field.id]?.value}</div>
|
||||
</div>
|
||||
))}
|
||||
<div data-testid="mock-form-field-disabled-fields-identifiers">{disabledFields.join(", ")}</div>
|
||||
<button
|
||||
data-testid="mock-form-update-response-button"
|
||||
onClick={() =>
|
||||
setResponse({
|
||||
"company-size": {
|
||||
label: "Company Size",
|
||||
value: "small",
|
||||
},
|
||||
country: {
|
||||
label: "Country",
|
||||
value: "usa",
|
||||
},
|
||||
})
|
||||
}>
|
||||
Test Update Response
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
FormInputFieldsSkeleton: vi.fn(() => <div data-testid="mock-form-input-fields-skeleton" />),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useLocale", () => ({
|
||||
useLocale: vi.fn(() => ({ t: (key: string) => key })),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/web/lib/hooks/useRouterQuery", () => ({
|
||||
default: vi.fn(() => {
|
||||
return {
|
||||
setQuery: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/ui", async (importOriginal) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const actual = await importOriginal<any>("@calcom/ui");
|
||||
return {
|
||||
...actual,
|
||||
Tooltip: vi.fn(({ children }) => <div data-testid="mock-tooltip">{children}</div>),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/trpc/react", () => ({
|
||||
trpc: {
|
||||
viewer: {
|
||||
appRoutingForms: {
|
||||
getResponseWithFormFields: {
|
||||
useQuery: vi.fn(() => ({
|
||||
data: {
|
||||
form: {
|
||||
id: "form-id",
|
||||
name: "Test Form",
|
||||
fields: [
|
||||
{
|
||||
id: "company-size",
|
||||
label: "Company Size",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Small", value: "small" },
|
||||
{ label: "Medium", value: "medium" },
|
||||
{ label: "Large", value: "large" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "country",
|
||||
label: "Country",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "USA", value: "usa" },
|
||||
{ label: "Canada", value: "canada" },
|
||||
{ label: "UK", value: "uk" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
label: "Email",
|
||||
type: "email",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
id: "mock-route-id",
|
||||
action: {
|
||||
eventTypeId: 123,
|
||||
type: RouteActionType.EventTypeRedirectUrl,
|
||||
value: "team/test-team/new-test-event",
|
||||
},
|
||||
__testMatching: true,
|
||||
},
|
||||
{
|
||||
id: "fallback-route",
|
||||
isFallback: true,
|
||||
action: {
|
||||
type: RouteActionType.CustomPageMessage,
|
||||
value: "456",
|
||||
},
|
||||
__testMatching: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
response: {
|
||||
"company-size": {
|
||||
label: "Company Size",
|
||||
value: "small",
|
||||
},
|
||||
country: {
|
||||
label: "Country",
|
||||
value: "usa",
|
||||
},
|
||||
},
|
||||
},
|
||||
isPending: false,
|
||||
})),
|
||||
},
|
||||
findTeamMembersMatchingAttributeLogic: {
|
||||
useMutation: vi.fn(({ onSuccess }) => {
|
||||
return {
|
||||
mutate: vi.fn(() => {
|
||||
onSuccess({
|
||||
result: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Matching User 1",
|
||||
email: "matching-user-1@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Matching User 2",
|
||||
email: "matching-user-2@example.com",
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
eventTypes: {
|
||||
get: {
|
||||
useQuery: vi.fn(() => ({
|
||||
data: {
|
||||
eventType: {
|
||||
id: "123",
|
||||
title: "Mocked Event Type",
|
||||
slug: "mocked-event-type",
|
||||
length: 30,
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: { slug: "mocked-team" },
|
||||
},
|
||||
},
|
||||
isPending: false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let mockMutateFn = vi.fn(({ __testOnSuccess }) => {
|
||||
__testOnSuccess({
|
||||
uid: "RESCHEDULED_BOOKING_UID_SAME_TIMESLOT",
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useMutation: vi.fn(({ onSuccess }) => {
|
||||
return {
|
||||
mutate: vi.fn((payload) => {
|
||||
mockMutateFn({
|
||||
...payload,
|
||||
__testOnSuccess: onSuccess,
|
||||
});
|
||||
}),
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
onSuccess: onSuccess,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
async function mockMessageFromOpenedTab({ type, data }: { type: string; data: any }) {
|
||||
const messageReceivedPromise = new Promise<boolean>((resolve) => {
|
||||
window.addEventListener("message", () => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
window.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
|
||||
return messageReceivedPromise;
|
||||
}
|
||||
|
||||
function expectEventTypeInfoInCurrentRouting({
|
||||
eventTypeText,
|
||||
eventTypeHref,
|
||||
}: {
|
||||
eventTypeText: string;
|
||||
eventTypeHref: string;
|
||||
}) {
|
||||
const eventTypeEl = screen.getByTestId("current-routing-status-event-type");
|
||||
expect(eventTypeEl).toHaveTextContent(eventTypeText);
|
||||
expect(eventTypeEl.querySelector("a")).toHaveAttribute("href", eventTypeHref);
|
||||
}
|
||||
|
||||
function expectEventTypeInfoInReroutePreview({
|
||||
eventTypeText,
|
||||
eventTypeHref,
|
||||
}: {
|
||||
eventTypeText: string;
|
||||
eventTypeHref: string;
|
||||
}) {
|
||||
const eventTypeEl = screen.getByTestId("reroute-preview-event-type");
|
||||
expect(eventTypeEl).toHaveTextContent(eventTypeText);
|
||||
expect(eventTypeEl.querySelector("a")).toHaveAttribute("href", eventTypeHref);
|
||||
}
|
||||
|
||||
function expectOrganizerInfoInCurrentRouting({ organizerText }: { organizerText: string }) {
|
||||
const organizerEl = screen.getByTestId("current-routing-status-organizer");
|
||||
expect(organizerEl).toHaveTextContent(organizerText);
|
||||
}
|
||||
|
||||
function expectAttendeesInfoInCurrentRouting({ attendeesText }: { attendeesText: string }) {
|
||||
const attendeesEl = screen.getByTestId("current-routing-status-attendees");
|
||||
expect(attendeesEl).toHaveTextContent(attendeesText);
|
||||
}
|
||||
const userWhoBooked = { id: 1, name: "Test User", email: "user@example.com" };
|
||||
const mockBooking = {
|
||||
id: 1,
|
||||
uid: "original-booking-uid",
|
||||
title: "Test Booking",
|
||||
startTime: new Date(new Date().setDate(new Date().getDate() + 1)).toISOString(),
|
||||
metadata: {},
|
||||
responses: {},
|
||||
attendees: [{ email: "attendee@example.com", name: "Attendee", timeZone: "UTC", locale: "en" }],
|
||||
eventType: {
|
||||
id: 1,
|
||||
slug: "test-event",
|
||||
team: { slug: "test-team" },
|
||||
length: 60,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
title: "Test Event",
|
||||
},
|
||||
user: userWhoBooked,
|
||||
routedFromRoutingFormReponse: { id: 1 },
|
||||
status: BookingStatus.ACCEPTED, // Add this line
|
||||
};
|
||||
|
||||
const buildBooking = () => {
|
||||
return {
|
||||
...mockBooking,
|
||||
};
|
||||
};
|
||||
|
||||
describe("RerouteDialog", () => {
|
||||
const mockSetIsOpenDialog = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the dialog when open", () => {
|
||||
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
||||
|
||||
expect(screen.getByText("reroute_booking")).toBeInTheDocument();
|
||||
expect(screen.getByText("reroute_booking_description")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("doesn't render the dialog when closed", () => {
|
||||
render(
|
||||
<RerouteDialog isOpenDialog={false} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
||||
);
|
||||
|
||||
expect(screen.queryByText("reroute_booking")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays current routing status", async () => {
|
||||
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
||||
expect(screen.getByText("current_routing_status")).toBeInTheDocument();
|
||||
|
||||
expectEventTypeInfoInCurrentRouting({
|
||||
eventTypeText: "team/test-team/test-event",
|
||||
eventTypeHref: "https://cal.com/team/test-team/test-event",
|
||||
});
|
||||
expectOrganizerInfoInCurrentRouting({
|
||||
organizerText: "user@example.com",
|
||||
});
|
||||
expectAttendeesInfoInCurrentRouting({
|
||||
attendeesText: "attendee@example.com",
|
||||
});
|
||||
// screen.logTestingPlaygroundURL()
|
||||
});
|
||||
|
||||
test("verify_new_route button is enabled even when form fields are not filled", async () => {
|
||||
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
||||
expect(screen.getByText("verify_new_route")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("disabledFields are passed to FormInputFields with value ['email'] - email field is disabled", async () => {
|
||||
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
||||
expect(screen.getByTestId("mock-form-field-disabled-fields-identifiers")).toHaveTextContent(/^email$/);
|
||||
});
|
||||
|
||||
test("Expect form fields and name to be rendered", async () => {
|
||||
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
||||
expect(screen.getByText("Test Form")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-form-field-company-size-label")).toHaveTextContent("Company Size");
|
||||
expect(screen.getByTestId("mock-form-field-company-size-value")).toHaveTextContent("small");
|
||||
expect(screen.getByTestId("mock-form-field-country-label")).toHaveTextContent("Country");
|
||||
expect(screen.getByTestId("mock-form-field-country-value")).toHaveTextContent("usa");
|
||||
});
|
||||
|
||||
test("cancel button closes the dialog", async () => {
|
||||
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
||||
|
||||
expect(screen.getByText("cancel")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("cancel"));
|
||||
expect(mockSetIsOpenDialog).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
describe("New Routing tests", () => {
|
||||
test("when verify_new_route is clicked, the form is submitted", async () => {
|
||||
render(
|
||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
||||
);
|
||||
fireEvent.click(screen.getByText("verify_new_route"));
|
||||
|
||||
expectEventTypeInfoInReroutePreview({
|
||||
eventTypeText: "team/test-team/new-test-event",
|
||||
eventTypeHref: "https://cal.com/team/test-team/new-test-event",
|
||||
});
|
||||
expect(screen.getByText("verify_new_route")).toBeEnabled();
|
||||
expect(screen.getByTestId("reroute-preview-hosts")).toHaveTextContent("reroute_preview_possible_host");
|
||||
expect(screen.getByTestId("reroute-preview-hosts")).toHaveTextContent("matching-user-1@example.com");
|
||||
|
||||
expect(screen.getByText("reschedule_to_the_new_event_with_different_timeslot")).toBeInTheDocument();
|
||||
expect(screen.getByText("reschedule_with_same_timeslot_of_new_event")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("New tab rescheduling", () => {
|
||||
test("new tab is closed when new booking is rerouted", async () => {
|
||||
render(
|
||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
||||
);
|
||||
clickVerifyNewRouteButton();
|
||||
clickRescheduleToTheNewEventWithDifferentTimeslotButton();
|
||||
await mockMessageFromOpenedTab({
|
||||
type: "CAL:rescheduleBookingSuccessfulV2",
|
||||
data: {
|
||||
uid: "RESCHEDULED_BOOKING_UID_NEW_TAB",
|
||||
},
|
||||
});
|
||||
|
||||
const rescheduleTabUrl = mockOpen.mock.calls[0][0] as unknown as string;
|
||||
const rescheduleTabUrlObject = new URL(rescheduleTabUrl, "http://mockcal.com");
|
||||
expect(Object.fromEntries(rescheduleTabUrlObject.searchParams.entries())).toEqual(
|
||||
expect.objectContaining({
|
||||
rescheduleUid: mockBooking.uid,
|
||||
"cal.rerouting": "true",
|
||||
// Shouldn't include the user who booked the booking
|
||||
"cal.routedTeamMemberIds": "2",
|
||||
"cal.reroutingFormResponses": JSON.stringify({
|
||||
"company-size": {
|
||||
value: "small",
|
||||
},
|
||||
country: {
|
||||
value: "usa",
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(fakeNewTabWindow.close).toHaveBeenCalled();
|
||||
expectToBeNavigatedToBookingPage({
|
||||
booking: {
|
||||
uid: "RESCHEDULED_BOOKING_UID_NEW_TAB",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Rescheduling with same timeslot works", async () => {
|
||||
render(
|
||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
||||
);
|
||||
clickVerifyNewRouteButton();
|
||||
clickRescheduleWithSameTimeslotOfChosenEventButton();
|
||||
expect(mockMutateFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rescheduleUid: mockBooking.uid,
|
||||
// Shouldn't include the user who booked the booking
|
||||
routedTeamMemberIds: [2],
|
||||
reroutingFormResponses: {
|
||||
"company-size": {
|
||||
value: "small",
|
||||
},
|
||||
country: {
|
||||
value: "usa",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expectToBeNavigatedToBookingPage({
|
||||
booking: {
|
||||
uid: "RESCHEDULED_BOOKING_UID_SAME_TIMESLOT",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function clickVerifyNewRouteButton() {
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText("verify_new_route"));
|
||||
});
|
||||
}
|
||||
|
||||
function clickRescheduleToTheNewEventWithDifferentTimeslotButton() {
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText("reschedule_to_the_new_event_with_different_timeslot"));
|
||||
});
|
||||
}
|
||||
|
||||
function clickRescheduleWithSameTimeslotOfChosenEventButton() {
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText("reschedule_with_same_timeslot_of_new_event"));
|
||||
});
|
||||
}
|
||||
|
||||
function expectToBeNavigatedToBookingPage({
|
||||
booking,
|
||||
}: {
|
||||
booking: {
|
||||
uid: string;
|
||||
};
|
||||
}) {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith(`/booking/${booking.uid}?cal.rerouting=true`);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const CustomEmailTextField = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`border-default mt-2 flex items-center rounded-md border ${
|
||||
className={`border-default mt-2 flex w-full items-center rounded-md border ${
|
||||
inputFocus ? "ring-brand-default border-neutral-300 ring-2" : ""
|
||||
}`}>
|
||||
<TextField
|
||||
|
||||
@@ -57,6 +57,7 @@ const SecondaryEmailModal = ({
|
||||
type="creation"
|
||||
data-testid="secondary-email-add-dialog">
|
||||
<Form form={formMethods} handleSubmit={handleAddEmail}>
|
||||
<div className="text-subtle mb-4 text-sm">{t("change_email_hint")}</div>
|
||||
<TextField
|
||||
label={t("email_address")}
|
||||
data-testid="secondary-email-input"
|
||||
|
||||
@@ -159,14 +159,14 @@ export const OAuthClientCard = ({
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => router.push(`/settings/platform/oauth-clients/${id}/edit/webhooks`)}>
|
||||
Webhooks
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
color="secondary"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => router.push(`/settings/platform/oauth-clients/${id}/edit`)}>
|
||||
|
||||
@@ -212,7 +212,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
autoCorrect="none"
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
|
||||
"border-l-1 my-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
|
||||
isInputUsernamePremium
|
||||
? "border border-orange-400 focus:border focus:border-orange-400"
|
||||
: "border focus:border",
|
||||
@@ -262,7 +262,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
)}
|
||||
</div>
|
||||
{paymentMsg}
|
||||
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
|
||||
{markAsError && <p className="mt-1 text-xs text-red-500">{t("username_already_taken")}</p>}
|
||||
|
||||
<Dialog open={openDialogSaveUsername}>
|
||||
<DialogContent
|
||||
|
||||
@@ -56,6 +56,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return {
|
||||
props: {
|
||||
entity: eventData.entity,
|
||||
eventTypeId: eventData.id,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -10,7 +11,7 @@ import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
|
||||
@@ -29,7 +30,6 @@ 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 ssr = await ssrInit(context);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug);
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
|
||||
@@ -54,43 +54,54 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
select: {
|
||||
id: true,
|
||||
hideBranding: true,
|
||||
parent: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
eventTypes: {
|
||||
where: {
|
||||
slug: meetingSlug,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isInstantEvent: true,
|
||||
schedulingType: true,
|
||||
metadata: true,
|
||||
length: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
if (!team || !team.eventTypes?.[0]) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const eventData = team.eventTypes[0];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
}
|
||||
|
||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username: teamSlug,
|
||||
eventSlug: meetingSlug,
|
||||
isTeamEvent: true,
|
||||
org,
|
||||
fromRedirectOfNonOrgLink: context.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
const ssr = await ssrInit(context);
|
||||
const fromRedirectOfNonOrgLink = context.query.orgRedirection === "true";
|
||||
const isUnpublished = team.parent ? !team.parent.slug : !team.slug;
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
eventTypeId: eventData.id,
|
||||
entity: {
|
||||
fromRedirectOfNonOrgLink,
|
||||
considerUnpublished: isUnpublished && !fromRedirectOfNonOrgLink,
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
teamSlug: team.slug ?? null,
|
||||
name: team.parent?.name ?? team.name ?? null,
|
||||
},
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventData.metadata),
|
||||
},
|
||||
booking,
|
||||
user: teamSlug,
|
||||
@@ -98,20 +109,27 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
slug: meetingSlug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: team?.hideBranding,
|
||||
isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false,
|
||||
isInstantMeeting: eventData && queryIsInstantMeeting ? true : false,
|
||||
themeBasis: null,
|
||||
orgBannerUrl: eventData?.team?.parent?.bannerUrl ?? "",
|
||||
orgBannerUrl: team.parent?.bannerUrl ?? "",
|
||||
teamMemberEmail: await getTeamMemberEmail(eventData, email as string),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type EventData = RouterOutputs["viewer"]["public"]["event"];
|
||||
|
||||
async function getTeamMemberEmail(eventData: EventData, email?: string): Promise<string | null> {
|
||||
async function getTeamMemberEmail(
|
||||
eventData: {
|
||||
id: number;
|
||||
isInstantEvent: boolean;
|
||||
schedulingType: SchedulingType | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
length: number;
|
||||
},
|
||||
email?: string
|
||||
): Promise<string | null> {
|
||||
// Pre-requisites
|
||||
if (!eventData || !email || eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
|
||||
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.id);
|
||||
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
||||
if (!crmContactOwnerEmail) return null;
|
||||
// Determine if the contactOwner is a part of the event type
|
||||
const contactOwnerQuery = await prisma.user.findFirst({
|
||||
|
||||
@@ -201,7 +201,7 @@ export default function AvailabilityPage({ currentOrg }: PageProps) {
|
||||
(data && (data.user.role === MembershipRole.OWNER || data.user.role === MembershipRole.ADMIN)) ?? false;
|
||||
const isOrgAndPrivate = data?.isOrganization && data.isPrivate;
|
||||
|
||||
const canViewTeamAvailability = isOrg && (isOrgAdminOrOwner || !isOrgAndPrivate);
|
||||
const canViewTeamAvailability = isOrgAdminOrOwner || !isOrgAndPrivate;
|
||||
|
||||
const toggleGroupOptions = [{ value: "mine", label: t("my_availability") }];
|
||||
|
||||
@@ -233,7 +233,7 @@ export default function AvailabilityPage({ currentOrg }: PageProps) {
|
||||
</div>
|
||||
}>
|
||||
{searchParams?.get("type") === "team" && canViewTeamAvailability ? (
|
||||
<AvailabilitySliderTable userTimeFormat={me?.data?.timeFormat ?? null} />
|
||||
<AvailabilitySliderTable userTimeFormat={me?.data?.timeFormat ?? null} isOrg={isOrg} />
|
||||
) : (
|
||||
<AvailabilityListWithQuery />
|
||||
)}
|
||||
|
||||
@@ -78,7 +78,6 @@ export default function Bookings() {
|
||||
const { t } = useLocale();
|
||||
const user = useMeQuery().data;
|
||||
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
|
||||
|
||||
const query = trpc.viewer.bookings.get.useInfiniteQuery(
|
||||
{
|
||||
limit: 10,
|
||||
@@ -88,7 +87,6 @@ export default function Bookings() {
|
||||
},
|
||||
},
|
||||
{
|
||||
// first render has status `undefined`
|
||||
enabled: true,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
|
||||
@@ -365,16 +365,26 @@ export default function Success(props: PageProps) {
|
||||
const isNotAttendingSeatedEvent = isCancelled && seatReferenceUid;
|
||||
const isEventCancelled = isCancelled && !seatReferenceUid;
|
||||
const isPastBooking = isBookingInPast;
|
||||
const isRerouting = searchParams?.get("cal.rerouting") === "true";
|
||||
const isRescheduled = bookingInfo?.rescheduled;
|
||||
|
||||
const successPageHeadline = (() => {
|
||||
if (needsConfirmationAndReschedulable) {
|
||||
return isRecurringBooking ? t("booking_submitted_recurring") : t("booking_submitted");
|
||||
}
|
||||
|
||||
if (isRerouting) {
|
||||
return t("This meeting has been rerouted");
|
||||
}
|
||||
|
||||
if (isNotAttendingSeatedEvent) {
|
||||
return t("no_longer_attending");
|
||||
}
|
||||
|
||||
if (isRescheduled) {
|
||||
return t("your_event_has_been_rescheduled");
|
||||
}
|
||||
|
||||
if (isEventCancelled) {
|
||||
return t("event_cancelled");
|
||||
}
|
||||
@@ -476,6 +486,7 @@ export default function Success(props: PageProps) {
|
||||
id="modal-headline">
|
||||
{successPageHeadline}
|
||||
</h3>
|
||||
|
||||
<div className="mt-3">
|
||||
<p className="text-default">{getTitle()}</p>
|
||||
</div>
|
||||
@@ -691,6 +702,7 @@ export default function Success(props: PageProps) {
|
||||
{!requiresLoginToUpdate &&
|
||||
(!needsConfirmation || !userIsOwner) &&
|
||||
isReschedulable &&
|
||||
!isRerouting &&
|
||||
(!isCancellationMode ? (
|
||||
<>
|
||||
<hr className="border-subtle mb-8" />
|
||||
@@ -750,6 +762,18 @@ export default function Success(props: PageProps) {
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
{isRerouting && typeof window !== "undefined" && window.opener && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.opener.focus();
|
||||
window.close();
|
||||
}}>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{userIsOwner &&
|
||||
!needsConfirmation &&
|
||||
!isCancellationMode &&
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
function Type({ slug, user, booking, isEmbed, isBrandingHidden, entity, duration }: PageProps) {
|
||||
function Type({ slug, user, booking, isEmbed, isBrandingHidden, entity, eventTypeId, duration }: PageProps) {
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
@@ -29,7 +29,7 @@ function Type({ slug, user, booking, isEmbed, isBrandingHidden, entity, duration
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
isInstantMeeting
|
||||
entity={entity}
|
||||
entity={{ ...entity, eventTypeId: eventTypeId }}
|
||||
duration={duration}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -116,6 +116,7 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
|
||||
if (res.locale) {
|
||||
window.calNewLocale = res.locale;
|
||||
document.cookie = `calNewLocale=${res.locale}; path=/`;
|
||||
}
|
||||
await revalidatePage();
|
||||
},
|
||||
@@ -235,7 +236,11 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
)}
|
||||
/>
|
||||
{!watchedTzSchedules.length ? (
|
||||
<Button color="minimal" className="mt-2" onClick={() => setIsTZScheduleOpen(true)}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="mt-2"
|
||||
StartIcon="calendar"
|
||||
onClick={() => setIsTZScheduleOpen(true)}>
|
||||
{t("schedule_timezone_change")}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -310,7 +315,7 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<div className="text-gray text-default mt-2 flex items-center text-sm">
|
||||
<div className="text-gray text-subtle mt-2 flex items-center text-xs">
|
||||
{t("timeformat_profile_hint")}
|
||||
</div>
|
||||
<Controller
|
||||
|
||||
@@ -335,7 +335,7 @@ const ProfileView = () => {
|
||||
Icon="triangle-alert">
|
||||
<>
|
||||
<div className="mb-10">
|
||||
<p className="text-default mb-4">{t("delete_account_confirmation_message")}</p>
|
||||
<p className="text-subtle mb-4 text-sm">{t("delete_account_confirmation_message")}</p>
|
||||
{isCALIdentityProvider && (
|
||||
<PasswordField
|
||||
data-testid="password"
|
||||
@@ -626,7 +626,7 @@ const ProfileForm = ({
|
||||
onChange(newAvatar);
|
||||
}}
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: value })}
|
||||
triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"}
|
||||
triggerButtonColor={showRemoveAvatarButton ? "secondary" : "secondary"}
|
||||
/>
|
||||
|
||||
{showRemoveAvatarButton && (
|
||||
@@ -651,35 +651,36 @@ const ProfileForm = ({
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Label>{t("email")}</Label>
|
||||
{secondaryEmailFields.map((field, index) => (
|
||||
<CustomEmailTextField
|
||||
key={field.itemId}
|
||||
formMethods={formMethods}
|
||||
formMethodFieldName={`secondaryEmails.${index}.email` as keyof FormValues}
|
||||
errorMessage={get(formMethods.formState.errors, `secondaryEmails.${index}.email.message`)}
|
||||
emailVerified={Boolean(field.emailVerified)}
|
||||
emailPrimary={field.emailPrimary}
|
||||
dataTestId={`profile-form-email-${index}`}
|
||||
handleChangePrimary={() => {
|
||||
const fields = secondaryEmailFields.map((secondaryField, cIndex) => ({
|
||||
...secondaryField,
|
||||
emailPrimary: cIndex === index,
|
||||
}));
|
||||
updateAllSecondaryEmailFields(fields);
|
||||
}}
|
||||
handleVerifyEmail={() => handleResendVerifyEmail(field.email)}
|
||||
handleItemDelete={() => deleteSecondaryEmail(index)}
|
||||
/>
|
||||
))}
|
||||
<div className="text-default mt-2 flex items-center text-sm">{t("change_email_hint")}</div>
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon="plus"
|
||||
className="mt-2"
|
||||
onClick={() => handleAddSecondaryEmail()}
|
||||
data-testid="add-secondary-email">
|
||||
{t("add_email")}
|
||||
</Button>
|
||||
<div className="-mt-2 flex gap-2">
|
||||
{secondaryEmailFields.map((field, index) => (
|
||||
<CustomEmailTextField
|
||||
key={field.itemId}
|
||||
formMethods={formMethods}
|
||||
formMethodFieldName={`secondaryEmails.${index}.email` as keyof FormValues}
|
||||
errorMessage={get(formMethods.formState.errors, `secondaryEmails.${index}.email.message`)}
|
||||
emailVerified={Boolean(field.emailVerified)}
|
||||
emailPrimary={field.emailPrimary}
|
||||
dataTestId={`profile-form-email-${index}`}
|
||||
handleChangePrimary={() => {
|
||||
const fields = secondaryEmailFields.map((secondaryField, cIndex) => ({
|
||||
...secondaryField,
|
||||
emailPrimary: cIndex === index,
|
||||
}));
|
||||
updateAllSecondaryEmailFields(fields);
|
||||
}}
|
||||
handleVerifyEmail={() => handleResendVerifyEmail(field.email)}
|
||||
handleItemDelete={() => deleteSecondaryEmail(index)}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
color="secondary"
|
||||
StartIcon="plus"
|
||||
className="mt-2 h-full"
|
||||
onClick={() => handleAddSecondaryEmail()}
|
||||
data-testid="add-secondary-email">
|
||||
{t("add_email")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Label>{t("about")}</Label>
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { WizardLayout } from "@calcom/ui";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
|
||||
@@ -35,13 +38,29 @@ export const CreateTeamEventType = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const { data: team } = trpc.viewer.teams.get.useQuery(
|
||||
{ teamId: teamId ?? -1, isOrg: false },
|
||||
{ enabled: !!teamId }
|
||||
);
|
||||
|
||||
const { form, createMutation, isManagedEventType } = useCreateEventType(onSuccessMutation, onErrorMutation);
|
||||
|
||||
const orgBranding = useOrgBranding();
|
||||
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
|
||||
|
||||
return (
|
||||
<TeamEventTypeForm
|
||||
isTeamAdminOrOwner={true}
|
||||
teamSlug={team?.slug}
|
||||
teamId={teamId}
|
||||
isTeamAdminOrOwner={true}
|
||||
urlPrefix={urlPrefix}
|
||||
isPending={createMutation.isPending}
|
||||
form={form}
|
||||
isManagedEventType={isManagedEventType}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}
|
||||
SubmitButton={SubmitButton}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ function Type({
|
||||
isInstantMeeting={isInstantMeeting}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
entity={eventData.entity}
|
||||
entity={{ ...eventData.entity, eventTypeId: eventData?.eventTypeId }}
|
||||
durationConfig={eventData.metadata?.multipleDuration}
|
||||
/* TODO: Currently unused, evaluate it is needed-
|
||||
* Possible alternative approach is to have onDurationChange.
|
||||
|
||||
@@ -52,7 +52,7 @@ function Type({
|
||||
eventSlug={slug}
|
||||
bookingData={booking}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={eventData.entity}
|
||||
entity={{ ...eventData.entity, eventTypeId: eventData?.id }}
|
||||
durationConfig={eventData.metadata?.multipleDuration}
|
||||
orgBannerUrl={orgBannerUrl}
|
||||
/* TODO: Currently unused, evaluate it is needed-
|
||||
|
||||
@@ -182,6 +182,8 @@ const nextConfig = {
|
||||
},
|
||||
i18n: {
|
||||
...i18n,
|
||||
defaultLocale: "en",
|
||||
locales: ["en"],
|
||||
localeDetection: false,
|
||||
},
|
||||
productionBrowserSourceMaps: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.6.1",
|
||||
"version": "4.6.10",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
import { highPerfRouter } from "@calcom/trpc/server/routers/viewer/highPerf/_router";
|
||||
|
||||
export default createNextApiHandler(highPerfRouter);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 221 KiB |
@@ -659,6 +659,15 @@
|
||||
<path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" ></path>
|
||||
<rect x="2" y="6" width="14" height="12" rx="2" ></rect>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-waypoints" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="waypoints">
|
||||
<circle cx="12" cy="4.5" r="2.5" ></circle>
|
||||
<path d="m10.2 6.3-3.9 3.9" ></path>
|
||||
<circle cx="4.5" cy="12" r="2.5" ></circle>
|
||||
<path d="M7 12h10" ></path>
|
||||
<circle cx="19.5" cy="12" r="2.5" ></circle>
|
||||
<path d="m13.8 17.7 3.9-3.9" ></path>
|
||||
<circle cx="12" cy="19.5" r="2.5" ></circle>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-webhook" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="webhook">
|
||||
<path d="M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2" ></path>
|
||||
<path d="m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06" ></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "السماح للحضور بالحجز لك عن طريق حجز المجموعات الديناميكي",
|
||||
"dynamic_booking": "روابط المجموعة الديناميكية",
|
||||
"allow_seo_indexing": "السماح لمحركات البحث بالوصول إلى المحتوى العام لديك",
|
||||
"seo_indexing": "السماح بالفهرسة وفق SEO",
|
||||
"seo_indexing": "السماح بفهرسة محركات البحث",
|
||||
"allow_browser_notifications": "السماح بإشعارات المتصفح",
|
||||
"you_have_denied_notifications": "لقد رفضت الإشعارات. أعد ضبط الإذن في إعدادات المتصفح لتمكينها.",
|
||||
"disable_browser_notifications": "تعطيل إشعارات المتصفح",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "إخفاء الزر \"حجز عضو فريق\"",
|
||||
"hide_book_a_team_member_description": "إخفاء الزر \"حجز عضو فريق\" من صفحاتك العامة.",
|
||||
"danger_zone": "منطقة خطر",
|
||||
"account_deletion_cannot_be_undone": "احترس. لا يمكن التراجع عن حذف الحساب.",
|
||||
"team_deletion_cannot_be_undone": "كن حذرا. لا يمكن التراجع عن حذف الفريق.",
|
||||
"account_deletion_cannot_be_undone": "كن حذرًا. لا يمكن التراجع عن حذف الحساب.",
|
||||
"team_deletion_cannot_be_undone": "كن حذرًا. لا يمكن التراجع عن حذف الفريق.",
|
||||
"back": "عودة",
|
||||
"cancel": "إلغاء",
|
||||
"cancel_all_remaining": "إلغاء كل ما تبقى",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "ما تواتر مرات الحجز",
|
||||
"event_advanced_tab_description": "إعدادات الرزنامة والمزيد...",
|
||||
"event_advanced_tab_title": "متقدم",
|
||||
"event_payments_tab_title": "المدفوعات",
|
||||
"event_payments_tab_description": "إعداد المدفوعات للفعالية",
|
||||
"event_setup_multiple_duration_error": "إعداد الحدث: تستلزم فترات متعددة خيارًا واحدًا على الأقل.",
|
||||
"event_setup_multiple_duration_default_error": "إعداد الحدث: الرجاء تحديد مدة افتراضية صالحة.",
|
||||
"event_setup_booking_limits_error": "يجب أن تكون حدود الحجز بترتيب تصاعدي. [يوم، أسبوع، شهر، سنة]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "التوجيه إلى الشخص المناسب بناءً على إجابات النموذج الخاص بك",
|
||||
"form_description": "قم بإنشاء النموذج الخاص بك لتوجيه صاحب حجز",
|
||||
"copy_link_to_form": "نسخ الرابط إلى النموذج",
|
||||
"theme": "السمة",
|
||||
"theme": "سمة صفحة الحجز",
|
||||
"theme_applies_note": "ينطبق هذا فقط على صفحات الحجز العامة",
|
||||
"app_theme": "مظهر لوحة التحكم",
|
||||
"app_theme": "سمة لوحة التحكم",
|
||||
"app_theme_applies_note": "هذا ينطبق فقط على لوحة التحكم المسجلة دخولك",
|
||||
"theme_system": "الافتراضي للنظام",
|
||||
"add_a_team": "إضافة فريق",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "لا يوجد تقويم مثبت",
|
||||
"no_calendar_installed_description": "لم تقم بربط أي من التقويمات الخاصة بك",
|
||||
"add_a_calendar": "إضافة تقويم",
|
||||
"change_email_hint": "قد تحتاج إلى تسجيل الخروج والعودة لرؤية التغييرات التي تم تنفيذها",
|
||||
"change_email_hint": "قد تحتاج إلى تسجيل الخروج وإعادة تسجيل الدخول لرؤية أي تغيير يتم تطبيقه.",
|
||||
"confirm_password_change_email": "يرجى تأكيد كلمة المرور قبل تغيير عنوان بريدك الإلكتروني",
|
||||
"seats": "مقاعد",
|
||||
"every_app_published": "كل تطبيق منشور على App Store {{appName}} مفتوح المصدر ويتم اختباره بدقة من خلال مراجعات الأقران. ومع ذلك، فإن شركة {{companyName}} لا تدعم هذه التطبيقات أو تصرح بها ما لم يتم نشرها بواسطة {{appName}}. إذا واجهت محتوى أو سلوكًا غير لائق، فيرجى الإبلاغ عنه.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "إعادة تثبيت التطبيق",
|
||||
"reschedule_reason": "سبب إعادة الجدولة",
|
||||
"choose_common_schedule_team_event": "اختر جدولًا زمنيًا مشتركًا",
|
||||
"choose_common_schedule_team_event_description": "قم بتمكين هذا الخيار إذا كنت ترغب في استخدام جدول زمني مشترك بين المضيفين. عند تعطيل هذا الخيار، سيتم حجز كل مضيف على أساس جدوله الافتراضي.",
|
||||
"choose_common_schedule_team_event_description": "قم بتمكين هذا إذا كنت ترغب في استخدام جدول مشترك بين المضيفين. عند التعطيل، سيتم حجز كل مضيف بناءً على جدوله الافتراضي أو الجدول الذي تم اختياره.",
|
||||
"reason": "السبب",
|
||||
"sender_id": "معرّف المرسل",
|
||||
"sender_id_error_message": "يسمح بالحروف والأرقام والمسافات فقط (بحد أقصى 11 حرفًا)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "رمز التحقق غير صحيح",
|
||||
"not_verified": "لم يتم التحقق منه بعد",
|
||||
"no_availability_in_month": "غير متاح في شهر {{month}}",
|
||||
"view_previous_month": "عرض الشهر السابق",
|
||||
"view_next_month": "عرض الشهر التالي",
|
||||
"send_code": "إرسال الرمز",
|
||||
"number_verified": "تم التحقق من الرقم",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "الإعداد",
|
||||
"setup_description": "إعداد مثال Cal.com",
|
||||
"configure": "تهيئة",
|
||||
"sso_configuration": "تسجيل الدخول لمرة واحدة",
|
||||
"sso_configuration": "تسجيل الدخول الموحد",
|
||||
"sso_configuration_description": "قم بتهيئة SAML/OIDC SSO والسماح لأعضاء الفريق بتسجيل الدخول باستخدام موفر الهوية",
|
||||
"sso_configuration_description_orgs": "قم بتكوين SAML/OIDC SSO واسمح لأعضاء المؤسسة بتسجيل الدخول باستخدام موفر الهوية",
|
||||
"sso_oidc_heading": "SSO مع OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "الأحداث الأكثر شعبية",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "إليك ملخصك للأحداث الشعبية لفريقك {{teamName}} خلال آخر 30 يومًا",
|
||||
"me": "أنا",
|
||||
"monthly_digest_email": "الرسالة الإلكترونية للموجز الشهري",
|
||||
"monthly_digest_email": "البريد الإلكتروني الشهري الموجز",
|
||||
"monthly_digest_email_for_teams": "رسالة إلكترونية بالموجز الشهري للفرق",
|
||||
"verify_team_tooltip": "تأكيد فريقك لتمكين إرسال الرسائل إلى الحاضرين",
|
||||
"member_removed": "تمت إزالة العضو",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "خيار جديد",
|
||||
"update_profile": "تحديث العضو",
|
||||
"attribute_updated_successfully": "تم تحديث السمة بنجاح",
|
||||
"attribute_deleted_successfully": "تم حذف السمة بنجاح",
|
||||
"attributes_edited_successfully": "تم تعديل السمات بنجاح",
|
||||
"attribute_meta_description": "إدارة السمات لأعضاء فريقك",
|
||||
"attributes_edit_description": "تعديل السمات لأعضاء فريقك",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "منذ بداية الشهر",
|
||||
"year_to_date": "منذ بداية السنة",
|
||||
"custom_range": "نطاق مخصص",
|
||||
"show_all_columns": "عرض جميع الأعمدة",
|
||||
"toggle_columns": "تبديل الأعمدة",
|
||||
"no_columns_found": "لم يتم العثور على أعمدة",
|
||||
"salesforce_create_record_as": "عند الحجز، أضف الأحداث والمشاركين الجدد كالتالي:",
|
||||
"salesforce_lead": "عميل محتمل",
|
||||
"salesforce_contact_under_account": "جهة اتصال تحت حساب",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "إذا لم تكن جهة الاتصال موجودة تحت حساب، قم بإنشاء عميل محتمل جديد من المشارك",
|
||||
"salesforce_create_new_contact_under_account": "إنشاء جهة اتصال جديدة تحت حساب بناءً على نطاق البريد الإلكتروني للمشارك وجهات الاتصال الموجودة",
|
||||
"mass_assign_attributes": "تعيين السمات بشكل جماعي",
|
||||
"reroute_preview_custom_message": "ينتج عنه عرض رسالة مخصصة. حاول تغيير الاستجابة لتوجيه إلى حدث",
|
||||
"reroute_preview_external_redirect": "ينتج عنه إعادة التوجيه إلى {{externalUrl}}. حاول تغيير الاستجابة لتوجيه إلى حدث",
|
||||
"reroute_preview_possible_host": "المضيف المحتمل",
|
||||
"current_routing_status": "حالة التوجيه الحالية",
|
||||
"reroute_booking": "إعادة توجيه الحجز",
|
||||
"reroute_booking_description": "إعادة توجيه الحجز إلى أعضاء فريق مختلفين",
|
||||
"verify_new_route": "التحقق من المسار الجديد",
|
||||
"reroute": "إعادة التوجيه",
|
||||
"new_routing_status": "حالة التوجيه الجديدة",
|
||||
"salesforce_book_directly_with_attendee_owner": "إذا كان المشارك موجودًا في Salesforce، احجز مباشرة مع المالك",
|
||||
"salesforce_check_owner_of": "الكيان الذي يجب التحقق من مالكه للحجز مباشرة",
|
||||
"account": "حساب",
|
||||
"choose_hosts_schedule": "اختر جداول المضيفين",
|
||||
"hosts_schedule_description": "يمكنك اختيار جدول مختلف لكل مضيف. يتم اختيار جدول المضيف الافتراضي لكل مضيف بشكل افتراضي.",
|
||||
"no_hosts_description": "لا يوجد مضيفون معينون، أضف مضيفين في علامة التبويب التعيين.",
|
||||
"reschedule_with_same_timeslot": "إعادة الجدولة بنفس الفترة الزمنية",
|
||||
"reschedule_with_same_timeslot_of_new_event": "إعادة الجدولة بنفس الفترة الزمنية للحدث الجديد",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "إعادة الجدولة للحدث الجديد بفترة زمنية مختلفة",
|
||||
"reschedule_with_different_timeslot": "إعادة الجدولة بفترة زمنية مختلفة",
|
||||
"we_dont_have_id_for_the_new_event": "ليس لدينا معرف للحدث الجديد. يرجى الانتقال إلى نموذج التوجيه وحفظه مرة أخرى.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "تمت إعادة توجيه الحجز بنجاح. جاري إعادة التوجيه إلى صفحة الحجز",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "İştirakçılara dinamik qrup rezervasiyaları vasitəsilə sizi rezervasiya etməyə icazə verin",
|
||||
"dynamic_booking": "Dinamik qrup bağlantıları",
|
||||
"allow_seo_indexing": "Axtarış motorlarının ictimai məzmununuza daxil olmasına icazə verin",
|
||||
"seo_indexing": "SEO İndeksləməsinə İcazə Verin",
|
||||
"seo_indexing": "Axtarış sisteminin indeksləməsinə icazə verin",
|
||||
"allow_browser_notifications": "Brauzer bildirişlərinə icazə ver",
|
||||
"you_have_denied_notifications": "Bildirişlərə icazə verməmisiniz. Aktivləşdirmək üçün brauzer parametrlərində icazəni yenidən qurun.",
|
||||
"disable_browser_notifications": "Brauzer bildirişlərini söndür",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Nə qədər tez-tez rezervasiya edilə bilərsiniz",
|
||||
"event_advanced_tab_description": "Təqvim parametrləri və daha çox...",
|
||||
"event_advanced_tab_title": "İrəli",
|
||||
"event_payments_tab_title": "Ödənişlər",
|
||||
"event_payments_tab_description": "Tədbir üçün ödənişləri təyin edin",
|
||||
"event_setup_multiple_duration_error": "Tədbir Quraşdırması: Bir neçə müddət üçün ən azı 1 seçim tələb olunur.",
|
||||
"event_setup_multiple_duration_default_error": "Tədbir Quraşdırması: Zəhmət olmasa, etibarlı bir standart müddət seçin.",
|
||||
"event_setup_booking_limits_error": "Rezervasiya limitləri artan qaydada olmalıdır. [gün, həftə, ay, il]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Formunuza verdiyiniz cavablara əsasən düzgün şəxsə yönləndirin",
|
||||
"form_description": "Rezervasiya edən şəxsi yönləndirmək üçün formanızı yaradın",
|
||||
"copy_link_to_form": "Formanın linkini kopyalayın",
|
||||
"theme": "Rezervasiya Səhifəsinin Mövzusu",
|
||||
"theme": "Rezervasiya səhifəsinin teması",
|
||||
"theme_applies_note": "Bu yalnız sizin ictimai rezervasiya səhifələrinizə aiddir",
|
||||
"app_theme": "İdarə Panelinin Mövzusu",
|
||||
"app_theme": "İdarəetmə panelinin teması",
|
||||
"app_theme_applies_note": "Bu yalnız sizin daxil olduğunuz idarə panelinə aiddir",
|
||||
"theme_system": "Sistem defoltu",
|
||||
"add_a_team": "Komanda əlavə edin",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Quraşdırılmış təqvim yoxdur",
|
||||
"no_calendar_installed_description": "Hələ heç bir təqviminizi qoşmamısınız",
|
||||
"add_a_calendar": "Təqvim əlavə edin",
|
||||
"change_email_hint": "Dəyişikliklərin qüvvəyə minməsi üçün çıxış edib yenidən daxil olmanız lazım ola bilər",
|
||||
"change_email_hint": "Dəyişikliyin qüvvəyə minməsi üçün sistemdən çıxıb yenidən daxil olmağınız lazım ola bilər.",
|
||||
"confirm_password_change_email": "E-poçt ünvanınızı dəyişdirmədən əvvəl parolunuzu təsdiqləyin",
|
||||
"seats": "oturacaqlar",
|
||||
"every_app_published": "{{appName}} App Store-da yayımlanan hər bir tətbiq açıq mənbəlidir və həmkarların rəyləri vasitəsilə hərtərəfli sınaqdan keçirilir. Buna baxmayaraq, {{companyName}} bu tətbiqləri təsdiqləmir və ya sertifikatlaşdırmır, əgər onlar {{appName}} tərəfindən yayımlanmayıbsa. Uyğunsuz məzmun və ya davranışla qarşılaşsanız, xahiş edirik bildirin.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Tətbiqi yenidən quraşdırın",
|
||||
"reschedule_reason": "Yenidən planlaşdırma səbəbi",
|
||||
"choose_common_schedule_team_event": "Ümumi cədvəli seçin",
|
||||
"choose_common_schedule_team_event_description": "Əgər ev sahibləri arasında ümumi cədvəldən istifadə etmək istəyirsinizsə, bunu aktiv edin. Deaktiv edildikdə, hər bir ev sahibi öz standart cədvəlinə əsasən rezervasiya ediləcək.",
|
||||
"choose_common_schedule_team_event_description": "Əgər hostlar arasında ümumi cədvəldən istifadə etmək istəyirsinizsə, bunu aktivləşdirin. Deaktiv edildikdə, hər host öz standart cədvəlinə və ya seçilmiş cədvəlinə əsasən rezervasiya ediləcək.",
|
||||
"reason": "Səbəb",
|
||||
"sender_id": "Göndərən ID",
|
||||
"sender_id_error_message": "Yalnız hərflər, rəqəmlər və boşluqlar icazəlidir (maks. 11 simvol)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Yanlış təsdiq kodu",
|
||||
"not_verified": "Hələ təsdiqlənməyib",
|
||||
"no_availability_in_month": "{{month}} ayında əlçatanlıq yoxdur",
|
||||
"view_previous_month": "Əvvəlki aya baxın",
|
||||
"view_next_month": "Növbəti aya bax",
|
||||
"send_code": "Kodu göndər",
|
||||
"number_verified": "Nömrə təsdiqləndi",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Quraşdırma",
|
||||
"setup_description": "Cal.com instansiyasını quraşdırın",
|
||||
"configure": "Konfiqurasiya",
|
||||
"sso_configuration": "Tək Giriş (SSO) Konfiqurasiyası",
|
||||
"sso_configuration": "Vahid giriş",
|
||||
"sso_configuration_description": "SAML/OIDC SSO-nu konfiqurasiya edin və komanda üzvlərinin Kimlik Provayderi istifadə edərək daxil olmasına icazə verin",
|
||||
"sso_configuration_description_orgs": "SAML/OIDC SSO-nu konfiqurasiya edin və təşkilat üzvlərinin Kimlik Provayderi istifadə edərək daxil olmasına icazə verin",
|
||||
"sso_oidc_heading": "OIDC ilə SSO",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Ən Populyar Tədbirlər",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Son 30 gün ərzində komandanız üçün populyar tədbirlərin xülasəsi {{teamName}}",
|
||||
"me": "Mən",
|
||||
"monthly_digest_email": "Aylıq Xülasə E-poçtu",
|
||||
"monthly_digest_email": "Aylıq icmal e-poçtu",
|
||||
"monthly_digest_email_for_teams": "Komandalar üçün aylıq xülasə e-poçtu",
|
||||
"verify_team_tooltip": "İştirakçılara mesaj göndərməyi aktivləşdirmək üçün komandanızı təsdiqləyin",
|
||||
"member_removed": "Üzv silindi",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Yeni seçim",
|
||||
"update_profile": "Üzvü yenilə",
|
||||
"attribute_updated_successfully": "Atribut uğurla yeniləndi",
|
||||
"attribute_deleted_successfully": "Atribut uğurla silindi",
|
||||
"attributes_edited_successfully": "Atributlar uğurla redaktə edildi",
|
||||
"attribute_meta_description": "Komanda üzvləriniz üçün atributları idarə edin",
|
||||
"attributes_edit_description": "Komanda üzvləriniz üçün atributları redaktə edin",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "bu ayın tarixinə qədər",
|
||||
"year_to_date": "bu ilin tarixinə qədər",
|
||||
"custom_range": "xüsusi aralıq",
|
||||
"show_all_columns": "Bütün sütunları göstər",
|
||||
"toggle_columns": "Sütunları dəyiş",
|
||||
"no_columns_found": "Sütun tapılmadı",
|
||||
"salesforce_create_record_as": "Rezervasiya zamanı tədbirləri əlavə edin və yeni iştirakçıları belə qeyd edin:",
|
||||
"salesforce_lead": "Potensial müştəri",
|
||||
"salesforce_contact_under_account": "Hesab altında əlaqə",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Əgər əlaqə hesab altında mövcud deyilsə, iştirakçıdan yeni potensial müştəri yaradın",
|
||||
"salesforce_create_new_contact_under_account": "İştirakçının email domeni və mövcud əlaqələr əsasında hesab altında yeni əlaqə yaradın",
|
||||
"mass_assign_attributes": "Atributları kütləvi təyin et",
|
||||
"reroute_preview_custom_message": "Bu, xüsusi mesajın göstərilməsi ilə nəticələnir. Cavabı bir tədbir üçün marşruta dəyişdirməyə çalışın",
|
||||
"reroute_preview_external_redirect": "Bu, {{externalUrl}} ünvanına yönləndirmə ilə nəticələnir. Cavabı bir tədbir üçün marşruta dəyişdirməyə çalışın",
|
||||
"reroute_preview_possible_host": "Mümkün ev sahibi",
|
||||
"current_routing_status": "Cari marşrutlaşdırma statusu",
|
||||
"reroute_booking": "Rezervasiyanı yenidən yönləndirin",
|
||||
"reroute_booking_description": "Rezervasiyanı fərqli komanda üzvlərinə yönləndirin",
|
||||
"verify_new_route": "Yeni marşrutu təsdiqləyin",
|
||||
"reroute": "Yenidən yönləndirmə",
|
||||
"new_routing_status": "Yeni marşrutlaşdırma statusu",
|
||||
"salesforce_book_directly_with_attendee_owner": "Əgər iştirakçı Salesforce-da mövcuddursa, birbaşa sahib ilə rezervasiya edin",
|
||||
"salesforce_check_owner_of": "Birbaşa rezervasiya etmək üçün sahibini yoxlayacaq obyekt",
|
||||
"account": "Hesab",
|
||||
"choose_hosts_schedule": "Hostların cədvəlini seçin",
|
||||
"hosts_schedule_description": "Hər bir host üçün fərqli cədvəl seçə bilərsiniz. Hər hostun standart cədvəli avtomatik olaraq seçilir.",
|
||||
"no_hosts_description": "Host təyin edilməyib, təyinat sekmesinde host əlavə edin.",
|
||||
"reschedule_with_same_timeslot": "Eyni vaxt aralığı ilə yenidən planlaşdırın",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Yeni tədbirin eyni vaxt aralığı ilə yenidən planlaşdırın",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Yeni tədbiri fərqli vaxt aralığı ilə yenidən planlaşdırın",
|
||||
"reschedule_with_different_timeslot": "Fərqli vaxt aralığı ilə yenidən planlaşdırın",
|
||||
"we_dont_have_id_for_the_new_event": "Yeni tədbir üçün ID-miz yoxdur. Zəhmət olmasa, Marşrutlaşdırma Formasına keçin və yenidən saxlayın.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Rezervasiya uğurla yenidən yönləndirildi. Rezervasiya səhifəsinə yönləndirilir",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Povolit účastníkům rezervovat vás prostřednictvím dynamických skupinových rezervací",
|
||||
"dynamic_booking": "Dynamické odkazy na skupiny",
|
||||
"allow_seo_indexing": "Povolte vyhledávačům přístup k vašemu veřejnému obsahu",
|
||||
"seo_indexing": "Zapnout indexování SEO",
|
||||
"seo_indexing": "Povolit indexování vyhledávači",
|
||||
"allow_browser_notifications": "Povolit oznámení v prohlížeči",
|
||||
"you_have_denied_notifications": "Odmítli jste oznámení. Obnovte oprávnění v nastavení prohlížeče, abyste je mohli povolit.",
|
||||
"disable_browser_notifications": "Zakázat oznámení v prohlížeči",
|
||||
@@ -673,7 +673,7 @@
|
||||
"hide_book_a_team_member": "Skrýt tlačítko Rezervovat člena týmu",
|
||||
"hide_book_a_team_member_description": "Skrýt tlačítko Rezervovat člena týmu na veřejných stránkách.",
|
||||
"danger_zone": "Nebezpečná zóna",
|
||||
"account_deletion_cannot_be_undone": "Pozor. Odstranění účtu nelze vrátit.",
|
||||
"account_deletion_cannot_be_undone": "Buďte opatrní. Smazání účtu nelze vrátit zpět.",
|
||||
"team_deletion_cannot_be_undone": "Buďte opatrní. Smazání týmu nelze vrátit zpět.",
|
||||
"back": "Zpět",
|
||||
"cancel": "Zrušit",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Jak často lze u vás provést rezervaci",
|
||||
"event_advanced_tab_description": "Nastavení kalendáře a další...",
|
||||
"event_advanced_tab_title": "Upřesnit",
|
||||
"event_payments_tab_title": "Platby",
|
||||
"event_payments_tab_description": "Nastavit platby pro událost",
|
||||
"event_setup_multiple_duration_error": "Nastavení události: více dob trvání vyžaduje alespoň 1 možnost.",
|
||||
"event_setup_multiple_duration_default_error": "Nastavení události: vyberte prosím platnou výchozí dobu trvání.",
|
||||
"event_setup_booking_limits_error": "Limity rezervací musí být seřazeny vzestupně. [den,týden,měsíc,rok]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Nasměrovat té správné osobě na základě odpovědí ve vašem formuláři",
|
||||
"form_description": "Vytvořte vlastní formulář, který adresujete rezervující osobě",
|
||||
"copy_link_to_form": "Kopírovat odkaz na formulář",
|
||||
"theme": "Motiv",
|
||||
"theme": "Motiv stránky pro rezervace",
|
||||
"theme_applies_note": "Toto se týká pouze vašich veřejných stránek s rezervacemi",
|
||||
"app_theme": "Téma dashboardu",
|
||||
"app_theme": "Motiv dashboardu",
|
||||
"app_theme_applies_note": "Tento se týká pouze vaše přihlášené dashboardu",
|
||||
"theme_system": "Výchozí nastavení systému",
|
||||
"add_a_team": "Přidat tým",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Není nainstalován kalendář",
|
||||
"no_calendar_installed_description": "Ještě jste nepropojili žádný ze svých kalendářů",
|
||||
"add_a_calendar": "Přidat kalendář",
|
||||
"change_email_hint": "Možná se budete muset odhlásit a znovu přihlásit, aby se změny projevily",
|
||||
"change_email_hint": "Možná se budete muset odhlásit a znovu přihlásit, aby se změna projevila.",
|
||||
"confirm_password_change_email": "Před změnou e-mailové adresy potvrďte své heslo",
|
||||
"seats": "míst",
|
||||
"every_app_published": "Každá aplikace zveřejněná v obchodě s aplikacemi na {{appName}} má otevřený zdrojový kód a je důkladně testována prostřednictvím vzájemného hodnocení. Společnost {{companyName}} nicméně tyto aplikace neschvaluje ani necertifikuje, pokud nejsou zveřejněny přímo společností {{appName}}. Pokud se setkáte s nevhodným obsahem nebo chováním, nahlaste to.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Přeinstalovat aplikaci",
|
||||
"reschedule_reason": "Důvod změny termínu",
|
||||
"choose_common_schedule_team_event": "Vyberte společný plán",
|
||||
"choose_common_schedule_team_event_description": "Tuto možnost povolte, pokud chcete používat společný plán mezi hostiteli. Při zakázání této možnosti bude každý hostitel rezervován na základě svého výchozího plánu.",
|
||||
"choose_common_schedule_team_event_description": "Povolte tuto možnost, pokud chcete použít společný rozvrh mezi hostiteli. Pokud je zakázána, každý hostitel bude rezervován podle svého výchozího rozvrhu nebo zvoleného rozvrhu.",
|
||||
"reason": "Důvod",
|
||||
"sender_id": "ID odesílatele",
|
||||
"sender_id_error_message": "Povolena jsou pouze písmena, číslice a mezery (max. 11 znaků)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Nesprávný ověřovací kód",
|
||||
"not_verified": "Zatím neověřeno",
|
||||
"no_availability_in_month": "Žádná dostupnost pro {{month}}",
|
||||
"view_previous_month": "Zobrazit předchozí měsíc",
|
||||
"view_next_month": "Zobrazit další měsíc",
|
||||
"send_code": "Odeslat kód",
|
||||
"number_verified": "Číslo ověřeno",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Nastavení",
|
||||
"setup_description": "Nastavte instanci Cal.com",
|
||||
"configure": "Konfigurovat",
|
||||
"sso_configuration": "Single Sign-On",
|
||||
"sso_configuration": "Jednotné přihlášení",
|
||||
"sso_configuration_description": "Konfigurace SAML/OIDC SSO a umožnění členům týmu přihlašování prostřednictvím poskytovatele identity",
|
||||
"sso_configuration_description_orgs": "Nastavte SAML/OIDC SSO a umožněte členům organizace přihlásit se pomocí poskytovatele identity",
|
||||
"sso_oidc_heading": "SSO s OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Nejoblíbenější události",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Zde je přehled oblíbených událostí vašeho týmu {{teamName}} za posledních 30 dní",
|
||||
"me": "Já",
|
||||
"monthly_digest_email": "E-mail s měsíčním přehledem",
|
||||
"monthly_digest_email": "Měsíční souhrnný e-mail",
|
||||
"monthly_digest_email_for_teams": "Měsíční e-mail s přehledem pro týmy",
|
||||
"verify_team_tooltip": "Proveďte ověření svého týmu a zapněte odesílání zpráv účastníkům",
|
||||
"member_removed": "Člen odstraněn",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nová možnost",
|
||||
"update_profile": "Aktualizovat člena",
|
||||
"attribute_updated_successfully": "Atribut úspěšně aktualizován",
|
||||
"attribute_deleted_successfully": "Atribut byl úspěšně smazán",
|
||||
"attributes_edited_successfully": "Atributy úspěšně upraveny",
|
||||
"attribute_meta_description": "Spravujte atributy pro členy svého týmu",
|
||||
"attributes_edit_description": "Upravte atributy pro členy svého týmu",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "od začátku měsíce",
|
||||
"year_to_date": "od začátku roku",
|
||||
"custom_range": "vlastní rozsah",
|
||||
"show_all_columns": "Zobrazit všechny sloupce",
|
||||
"toggle_columns": "Přepnout sloupce",
|
||||
"no_columns_found": "Žádné sloupce nebyly nalezeny",
|
||||
"salesforce_create_record_as": "Při rezervaci přidejte události a nové účastníky jako:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Kontakt pod účtem",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Pokud kontakt neexistuje pod účtem, vytvořte nový lead z účastníka",
|
||||
"salesforce_create_new_contact_under_account": "Vytvořte nový kontakt pod účtem na základě e-mailové domény účastníka a existujících kontaktů",
|
||||
"mass_assign_attributes": "Hromadné přiřazení atributů",
|
||||
"reroute_preview_custom_message": "Výsledkem je zobrazení vlastní zprávy. Zkuste změnit odpověď na přesměrování na událost",
|
||||
"reroute_preview_external_redirect": "Výsledkem je přesměrování na {{externalUrl}}. Zkuste změnit odpověď na přesměrování na událost",
|
||||
"reroute_preview_possible_host": "Možný hostitel",
|
||||
"current_routing_status": "Aktuální stav směrování",
|
||||
"reroute_booking": "Přesměrovat rezervaci",
|
||||
"reroute_booking_description": "Přesměrovat rezervaci na jiné členy týmu",
|
||||
"verify_new_route": "Ověřit novou trasu",
|
||||
"reroute": "Přesměrovat",
|
||||
"new_routing_status": "Nový stav směrování",
|
||||
"salesforce_book_directly_with_attendee_owner": "Pokud účastník existuje v Salesforce, rezervujte přímo s vlastníkem",
|
||||
"salesforce_check_owner_of": "Entita ke kontrole vlastníka pro přímou rezervaci",
|
||||
"account": "Účet",
|
||||
"choose_hosts_schedule": "Vyberte rozvrhy hostitelů",
|
||||
"hosts_schedule_description": "Můžete vybrat jiný rozvrh pro každého hostitele. Výchozí rozvrh každého hostitele je vybrán automaticky.",
|
||||
"no_hosts_description": "Žádní hostitelé nejsou přiřazeni, přidejte hostitele na kartě přiřazení.",
|
||||
"reschedule_with_same_timeslot": "Přeplánovat se stejným časovým úsekem",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Přeplánovat se stejným časovým úsekem nové události",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Přeplánovat na novou událost s jiným časovým úsekem",
|
||||
"reschedule_with_different_timeslot": "Přeplánovat s jiným časovým úsekem",
|
||||
"we_dont_have_id_for_the_new_event": "Nemáme ID pro novou událost. Prosím, přejděte do formuláře směrování a uložte jej znovu.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Rezervace byla úspěšně přesměrována. Přesměrování na stránku rezervace",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Tillad deltagere at booke dig gennem dynamiske gruppebookinger",
|
||||
"dynamic_booking": "Dynamiske gruppelinks",
|
||||
"allow_seo_indexing": "Tillad søgemaskiner at få adgang til dit offentlige indhold",
|
||||
"seo_indexing": "Tillad SEO-indeksering",
|
||||
"seo_indexing": "Tillad indeksering af søgemaskiner",
|
||||
"allow_browser_notifications": "Tillad browsermeddelelser",
|
||||
"you_have_denied_notifications": "Du har nægtet notifikationer. Nulstil tilladelsen i browserindstillingerne for at aktivere.",
|
||||
"disable_browser_notifications": "Deaktiver browsernotifikationer",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Hvor ofte du kan bookes",
|
||||
"event_advanced_tab_description": "Kalenderindstillinger & mere...",
|
||||
"event_advanced_tab_title": "Avanceret",
|
||||
"event_payments_tab_title": "Betalinger",
|
||||
"event_payments_tab_description": "Opsæt betalinger for arrangement",
|
||||
"event_setup_multiple_duration_error": "Opsætning af begivenheder: Flere varigheder kræver mindst 1 mulighed.",
|
||||
"event_setup_multiple_duration_default_error": "Opsætning af begivenheder: Vælg venligst en gyldig standardvarighed.",
|
||||
"event_setup_booking_limits_error": "Bookinggrænser skal være i stigende rækkefølge. [dag,uge,måned,år]",
|
||||
@@ -1566,7 +1568,7 @@
|
||||
"route_to_the_right_person": "Rute til den rette person baseret på svarene på din formular",
|
||||
"form_description": "Opret din formular for at rute en booker",
|
||||
"copy_link_to_form": "Kopiér link til formular",
|
||||
"theme": "Tema",
|
||||
"theme": "Tema for bookingside",
|
||||
"theme_applies_note": "Dette gælder kun for dine offentlige bookingsider",
|
||||
"app_theme": "Dashboard-tema",
|
||||
"app_theme_applies_note": "Dette gælder kun for din loggede dashboard",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Ingen kalender installeret",
|
||||
"no_calendar_installed_description": "Du har endnu ikke tilsluttet nogen af dine kalendere",
|
||||
"add_a_calendar": "Tilføj en kalender",
|
||||
"change_email_hint": "Det kan være nødvendigt at logge ud og ind igen for at se ændringerne træde i kraft",
|
||||
"change_email_hint": "Du skal muligvis logge ud og ind igen for at se ændringen træde i kraft.",
|
||||
"confirm_password_change_email": "Bekræft venligst din adgangskode, før du ændrer din e-mailadresse",
|
||||
"seats": "pladser",
|
||||
"every_app_published": "Hver app, der offentliggøres i {{appName}} App Store, er open source og grundigt testet via peer reviews. Ikke desto mindre godkender eller bekræfter {{companyName}} ikke disse apps, medmindre de offentliggøres af {{appName}}. Hvis du støder på upassende indhold eller adfærd, bedes du rapportere det.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Geninstaller app",
|
||||
"reschedule_reason": "Årsag til omlægning",
|
||||
"choose_common_schedule_team_event": "Vælg en fælles tidsplan",
|
||||
"choose_common_schedule_team_event_description": "Aktivér dette hvis du ønsker at bruge en fælles tidsplan mellem værter. Når deaktiveret, vil hver vært blive booket baseret på deres standardtidsplan.",
|
||||
"choose_common_schedule_team_event_description": "Aktivér dette, hvis du vil bruge en fælles tidsplan mellem værter. Når det er deaktiveret, vil hver vært blive booket baseret på deres standardtidsplan eller deres valgte tidsplan.",
|
||||
"reason": "Årsag",
|
||||
"sender_id": "Afsender ID",
|
||||
"sender_id_error_message": "Kun bogstaver, tal og mellemrum tilladt (maks. 11 tegn)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Forkert bekræftelseskode",
|
||||
"not_verified": "Endnu ikke bekræftet",
|
||||
"no_availability_in_month": "Ingen tilgængelighed i {{month}}",
|
||||
"view_previous_month": "Se forrige måned",
|
||||
"view_next_month": "Vis næste måned",
|
||||
"send_code": "Send kode",
|
||||
"number_verified": "Nummer Bekræftet",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Opsætning",
|
||||
"setup_description": "Opsæt Cal.com instans",
|
||||
"configure": "Indstil",
|
||||
"sso_configuration": "Enkelt Log På",
|
||||
"sso_configuration": "Single sign-on",
|
||||
"sso_configuration_description": "Konfigurer SAML/OIDC SSO og tillad teammedlemmer at logge ind ved hjælp af en Identitetsudbyder",
|
||||
"sso_configuration_description_orgs": "Konfigurer SAML/OIDC SSO og tillad organisationsmedlemmer at logge ind ved hjælp af en identitetsudbyder",
|
||||
"sso_oidc_heading": "SSO med OIDC",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Ny mulighed",
|
||||
"update_profile": "Opdater medlem",
|
||||
"attribute_updated_successfully": "Egenskab opdateret med succes",
|
||||
"attribute_deleted_successfully": "Attribut slettet med succes",
|
||||
"attributes_edited_successfully": "Egenskaber redigeret med succes",
|
||||
"attribute_meta_description": "Administrer egenskaber for dine teammedlemmer",
|
||||
"attributes_edit_description": "Rediger egenskaber for dine teammedlemmer",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "måned til dato",
|
||||
"year_to_date": "år til dato",
|
||||
"custom_range": "tilpasset interval",
|
||||
"show_all_columns": "Vis alle kolonner",
|
||||
"toggle_columns": "Skift kolonner",
|
||||
"no_columns_found": "Ingen kolonner fundet",
|
||||
"salesforce_create_record_as": "Ved booking, tilføj begivenheder og nye deltagere som:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Kontakt under en konto",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Hvis kontakten ikke findes under en konto, opret nyt lead fra deltager",
|
||||
"salesforce_create_new_contact_under_account": "Opret en ny kontakt under en konto baseret på deltagerens e-maildomæne og eksisterende kontakter",
|
||||
"mass_assign_attributes": "Masse tildel attributter",
|
||||
"reroute_preview_custom_message": "Det resulterer i at vise en tilpasset besked. Prøv at ændre svaret til at omdirigere til en begivenhed",
|
||||
"reroute_preview_external_redirect": "Det resulterer i en omdirigering til {{externalUrl}}. Prøv at ændre svaret til at omdirigere til en begivenhed",
|
||||
"reroute_preview_possible_host": "Mulig vært",
|
||||
"current_routing_status": "Nuværende omdirigeringsstatus",
|
||||
"reroute_booking": "Omdiriger booking",
|
||||
"reroute_booking_description": "Omdiriger bookingen til forskellige teammedlemmer",
|
||||
"verify_new_route": "Bekræft ny rute",
|
||||
"reroute": "Omdiriger",
|
||||
"new_routing_status": "Ny omdirigeringsstatus",
|
||||
"salesforce_book_directly_with_attendee_owner": "Hvis deltageren findes i Salesforce, book direkte med ejeren",
|
||||
"salesforce_check_owner_of": "Enhed til at tjekke ejeren af for at booke direkte",
|
||||
"account": "Konto",
|
||||
"choose_hosts_schedule": "Vælg værternes tidsplaner",
|
||||
"hosts_schedule_description": "Du kan vælge en anden tidsplan for hver vært. Hver værts standardtidsplan er valgt som standard.",
|
||||
"no_hosts_description": "Ingen værter tildelt, tilføj værter i tildelingsfanen.",
|
||||
"reschedule_with_same_timeslot": "Ombook med samme tidspunkt",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Ombook med samme tidspunkt for den nye begivenhed",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Ombook til den nye begivenhed med et andet tidspunkt",
|
||||
"reschedule_with_different_timeslot": "Ombook med et andet tidspunkt",
|
||||
"we_dont_have_id_for_the_new_event": "Vi har ikke et ID for den nye begivenhed. Gå venligst til Ruteskemaet og gem det igen.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Booking omdirigeret med succes. Omdirigerer til bookingsiden",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Ermöglichen Sie es den Teilnehmenden, Sie über dynamische Gruppenbuchungen zu buchen",
|
||||
"dynamic_booking": "Dynamische Gruppenlinks",
|
||||
"allow_seo_indexing": "Suchmaschinen erlauben, auf Ihre öffentlichen Inhalte zuzugreifen",
|
||||
"seo_indexing": "Suchmaschinen-Indexierung erlauben",
|
||||
"seo_indexing": "Suchmaschinenindexierung erlauben",
|
||||
"allow_browser_notifications": "Browser-Benachrichtigungen zulassen",
|
||||
"you_have_denied_notifications": "Sie haben Benachrichtigungen abgelehnt. Setzen Sie die Berechtigung in den Browser-Einstellungen zurück, um sie zu aktivieren.",
|
||||
"disable_browser_notifications": "Browser-Benachrichtigungen deaktivieren",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "„Ein Teammitglied buchen“-Schaltfläche ausblenden",
|
||||
"hide_book_a_team_member_description": "Blendet die „Ein Teammitglied buchen“-Schaltflächen auf Ihren öffentlichen Seiten aus.",
|
||||
"danger_zone": "Achtung",
|
||||
"account_deletion_cannot_be_undone": "Vorsicht. Löschen eines Kontos kann nicht rückgängig gemacht werden.",
|
||||
"team_deletion_cannot_be_undone": "Seien Sie vorsichtig. Das Löschen eines Teams kann nicht rückgängig gemacht werden",
|
||||
"account_deletion_cannot_be_undone": "Seien Sie vorsichtig. Die Kontolöschung kann nicht rückgängig gemacht werden.",
|
||||
"team_deletion_cannot_be_undone": "Seien Sie vorsichtig. Die Teamlöschung kann nicht rückgängig gemacht werden.",
|
||||
"back": "Zurück",
|
||||
"cancel": "Abbrechen",
|
||||
"cancel_all_remaining": "Alle verbleibenden absagen",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Wie oft können Sie gebucht werden",
|
||||
"event_advanced_tab_description": "Kalendereinstellungen & mehr...",
|
||||
"event_advanced_tab_title": "Erweitert",
|
||||
"event_payments_tab_title": "Zahlungen",
|
||||
"event_payments_tab_description": "Zahlungen für Veranstaltung einrichten",
|
||||
"event_setup_multiple_duration_error": "Terminkonfiguration: Mehrere Laufzeiten erfordern mindestens eine Option.",
|
||||
"event_setup_multiple_duration_default_error": "Termineinrichtung: Bitte wählen Sie eine gültige Standarddauer.",
|
||||
"event_setup_booking_limits_error": "Die Buchungslimits müssen in aufsteigender Reihenfolge sein. [Tag, Woche, Monat, Jahr]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Anhand der Formularantworten an die richtige Person weiterleiten",
|
||||
"form_description": "Erstellen Sie Ihr Formular, um einen Bucher zu anzuleiten",
|
||||
"copy_link_to_form": "Link in Formular kopieren",
|
||||
"theme": "Theme",
|
||||
"theme": "Design der Buchungsseite",
|
||||
"theme_applies_note": "Dies gilt nur für Ihre öffentlichen Buchungsseiten",
|
||||
"app_theme": "Dashboard-Thema",
|
||||
"app_theme": "Dashboard-Design",
|
||||
"app_theme_applies_note": "Dies gilt nur für Ihr eingeloggtes Dashboard",
|
||||
"theme_system": "Systemstandard",
|
||||
"add_a_team": "Team hinzufügen",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Kein Kalender installiert",
|
||||
"no_calendar_installed_description": "Sie haben noch keinen Ihrer Kalender verbunden",
|
||||
"add_a_calendar": "Kalender hinzufügen",
|
||||
"change_email_hint": "Möglicherweise müssen Sie sich aus- und einloggen, um jegliche Änderung wirksam zu machen",
|
||||
"change_email_hint": "Möglicherweise müssen Sie sich ab- und wieder anmelden, um Änderungen wirksam zu sehen.",
|
||||
"confirm_password_change_email": "Bitte bestätigen Sie Ihr Passwort, bevor Sie Ihre E-Mail-Adresse ändern",
|
||||
"seats": "Plätze",
|
||||
"every_app_published": "Jede App, die im {{appName}} App Store veröffentlicht wird, ist Open Source und gründlich durch Peer Reviews getestet. {{companyName}} unterstützt oder zertifiziert diese Apps jedoch nicht, es sei denn, sie werden von {{appName}} selbst veröffentlicht. Wenn Sie auf unangemessene Inhalte oder Verhaltensweisen stoßen, melden Sie diese bitte.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "App neu installieren",
|
||||
"reschedule_reason": "Verlegungsgrund",
|
||||
"choose_common_schedule_team_event": "Gemeinsamen Zeitplan auswählen",
|
||||
"choose_common_schedule_team_event_description": "Aktivieren Sie dies, wenn Sie einen gemeinsamen Zeitplan zwischen Hosts verwenden möchten. Wenn deaktiviert, ist jeder Host basierend auf seinen Standardplan buchbar.",
|
||||
"choose_common_schedule_team_event_description": "Aktivieren Sie dies, wenn Sie einen gemeinsamen Zeitplan für alle Gastgeber verwenden möchten. Wenn deaktiviert, wird jeder Gastgeber basierend auf seinem Standardzeitplan oder seinem gewählten Zeitplan gebucht.",
|
||||
"reason": "Grund",
|
||||
"sender_id": "Absender-ID",
|
||||
"sender_id_error_message": "Nur Buchstaben, Zahlen und Leerzeichen erlaubt (max. 11 Zeichen)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Bestätigungscode falsch",
|
||||
"not_verified": "Noch nicht bestätigt",
|
||||
"no_availability_in_month": "Keine Verfügbarkeit im {{month}}",
|
||||
"view_previous_month": "Vormonat anzeigen",
|
||||
"view_next_month": "Nächsten Monat anzeigen",
|
||||
"send_code": "Code senden",
|
||||
"number_verified": "Nummer bestätigt",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Einrichtung",
|
||||
"setup_description": "Cal.com Instanz einrichten",
|
||||
"configure": "Konfigurieren",
|
||||
"sso_configuration": "SAML-Konfiguration",
|
||||
"sso_configuration": "Single Sign-On",
|
||||
"sso_configuration_description": "SAML/OIDC SSO konfigurieren und Teammitgliedern erlauben, sich mit einem Identitätsanbieter anzumelden",
|
||||
"sso_configuration_description_orgs": "SAML/OIDC SSO konfigurieren und Organisationsmitgliedern erlauben, sich mit einem Identitätsanbieter anzumelden",
|
||||
"sso_oidc_heading": "SSO mit OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Beliebteste Termine",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Hier ist Ihre Zusammenfassung der beliebten Termine für Ihr Team {{teamName}} der letzten 30 Tage",
|
||||
"me": "Ich",
|
||||
"monthly_digest_email": "Monatliche Statistik E-Mail",
|
||||
"monthly_digest_email": "Monatliche Zusammenfassungs-E-Mail",
|
||||
"monthly_digest_email_for_teams": "Monatliche Statistik E-Mail für Teams",
|
||||
"verify_team_tooltip": "Verifizieren Sie Ihr Team, um das Senden von Nachrichten an Teilnehmer zu aktivieren",
|
||||
"member_removed": "Mitglied entfernt",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Neue Option",
|
||||
"update_profile": "Mitglied aktualisieren",
|
||||
"attribute_updated_successfully": "Attribut erfolgreich aktualisiert",
|
||||
"attribute_deleted_successfully": "Attribut erfolgreich gelöscht",
|
||||
"attributes_edited_successfully": "Attribute erfolgreich bearbeitet",
|
||||
"attribute_meta_description": "Verwalten Sie Attribute für Ihre Teammitglieder",
|
||||
"attributes_edit_description": "Bearbeiten Sie Attribute für Ihre Teammitglieder",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "aktueller Monat",
|
||||
"year_to_date": "aktuelles Jahr",
|
||||
"custom_range": "benutzerdefinierter Zeitraum",
|
||||
"show_all_columns": "Alle Spalten anzeigen",
|
||||
"toggle_columns": "Spalten ein-/ausblenden",
|
||||
"no_columns_found": "Keine Spalten gefunden",
|
||||
"salesforce_create_record_as": "Bei der Buchung Ereignisse und neue Teilnehmer hinzufügen als:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Kontakt unter einem Account",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Wenn der Kontakt nicht unter einem Account existiert, erstellen Sie einen neuen Lead aus dem Teilnehmer",
|
||||
"salesforce_create_new_contact_under_account": "Erstellen Sie einen neuen Kontakt unter einem Account basierend auf der E-Mail-Domain des Teilnehmers und bestehenden Kontakten",
|
||||
"mass_assign_attributes": "Attribute massenweise zuweisen",
|
||||
"reroute_preview_custom_message": "Es führt zur Anzeige einer benutzerdefinierten Nachricht. Versuchen Sie, die Antwort zu ändern, um zu einem Ereignis weiterzuleiten",
|
||||
"reroute_preview_external_redirect": "Es führt zur Weiterleitung auf {{externalUrl}}. Versuchen Sie, die Antwort zu ändern, um zu einem Ereignis weiterzuleiten",
|
||||
"reroute_preview_possible_host": "Möglicher Gastgeber",
|
||||
"current_routing_status": "Aktueller Weiterleitungsstatus",
|
||||
"reroute_booking": "Buchung umleiten",
|
||||
"reroute_booking_description": "Leiten Sie die Buchung an andere Teammitglieder um",
|
||||
"verify_new_route": "Neue Route überprüfen",
|
||||
"reroute": "Umleiten",
|
||||
"new_routing_status": "Neuer Weiterleitungsstatus",
|
||||
"salesforce_book_directly_with_attendee_owner": "Wenn der Teilnehmer in Salesforce existiert, buchen Sie direkt beim Besitzer",
|
||||
"salesforce_check_owner_of": "Entität, deren Besitzer für die direkte Buchung überprüft werden soll",
|
||||
"account": "Account",
|
||||
"choose_hosts_schedule": "Zeitpläne der Gastgeber auswählen",
|
||||
"hosts_schedule_description": "Sie können für jeden Gastgeber einen anderen Zeitplan auswählen. Standardmäßig wird der Standardzeitplan jedes Gastgebers ausgewählt.",
|
||||
"no_hosts_description": "Keine Gastgeber zugewiesen. Fügen Sie Gastgeber im Zuweisungs-Tab hinzu.",
|
||||
"reschedule_with_same_timeslot": "Mit gleichem Zeitfenster neu planen",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Mit gleichem Zeitfenster des neuen Ereignisses neu planen",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Zum neuen Ereignis mit anderem Zeitfenster neu planen",
|
||||
"reschedule_with_different_timeslot": "Mit anderem Zeitfenster neu planen",
|
||||
"we_dont_have_id_for_the_new_event": "Wir haben keine ID für das neue Ereignis. Bitte gehen Sie zum Routing-Formular und speichern Sie es erneut.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Buchung erfolgreich umgeleitet. Weiterleitung zur Buchungsseite",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -39,10 +39,14 @@
|
||||
"email_not_sent": "Error occurred while sending email",
|
||||
"event_declined_subject": "Declined: {{title}} at {{date}}",
|
||||
"event_cancelled_subject": "Canceled: {{title}} at {{date}}",
|
||||
"event_reassigned_subject": "Reassigned: {{title}} at {{date}}",
|
||||
"event_request_declined": "Your event request has been declined",
|
||||
"event_request_declined_recurring": "Your recurring event request has been declined",
|
||||
"event_request_cancelled": "Your scheduled event was canceled",
|
||||
"event_request_reassigned": "Your scheduled event was reassigned",
|
||||
"event_reassigned_subtitle": "You will no longer have the event on your calendar and your round robin likelihood will not be negatively impacted",
|
||||
"organizer": "Organizer",
|
||||
"reassigned_to": "Reassigned to",
|
||||
"need_to_reschedule_or_cancel": "Need to reschedule or cancel?",
|
||||
"you_can_view_booking_details_with_this_url": "You can view the booking details from this url {{url}} and add the event to your calendar",
|
||||
"no_options_available": "No options available",
|
||||
@@ -127,6 +131,7 @@
|
||||
"your_meeting_has_been_booked": "Your meeting has been booked",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Your {{title}} has been rescheduled to {{date}}.",
|
||||
"event_has_been_rescheduled": "Updated - Your event has been rescheduled",
|
||||
"your_event_has_been_rescheduled": "Your event has been rescheduled",
|
||||
"request_reschedule_subtitle": "{{organizer}} has canceled the booking and requested you to pick another time.",
|
||||
"request_reschedule_title_organizer": "You have requested {{attendee}} to reschedule",
|
||||
"request_reschedule_subtitle_organizer": "You have canceled the booking and {{attendee}} should pick a new booking time with you.",
|
||||
@@ -462,7 +467,7 @@
|
||||
"allow_dynamic_booking": "Allow attendees to book you through dynamic group bookings",
|
||||
"dynamic_booking": "Dynamic group links",
|
||||
"allow_seo_indexing": "Allow search engines to access your public content",
|
||||
"seo_indexing": "Allow SEO Indexing",
|
||||
"seo_indexing": "Allow search engine indexing",
|
||||
"allow_browser_notifications": "Allow Browser Notifications",
|
||||
"you_have_denied_notifications": "You have denied notifications. Reset permission in browser settings to enable.",
|
||||
"disable_browser_notifications": "Disable Browser Notifications",
|
||||
@@ -488,6 +493,7 @@
|
||||
"booking_payment_initiated": "Booking Payment Initiated",
|
||||
"meeting_ended": "Meeting Ended",
|
||||
"form_submitted": "Form Submitted",
|
||||
"form_submitted_no_event": "Form submitted, no event booked",
|
||||
"booking_paid": "Booking Paid",
|
||||
"ooo_created": "Out of Office Created",
|
||||
"booking_no_show_updated": "Booking No-Show Updated",
|
||||
@@ -673,8 +679,8 @@
|
||||
"hide_book_a_team_member": "Hide Book a Team Member Button",
|
||||
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
|
||||
"danger_zone": "Danger zone",
|
||||
"account_deletion_cannot_be_undone": "Be Careful. Account deletion cannot be undone.",
|
||||
"team_deletion_cannot_be_undone": "Be Careful. Team deletion cannot be undone.",
|
||||
"account_deletion_cannot_be_undone": "Be careful. Account deletion cannot be undone.",
|
||||
"team_deletion_cannot_be_undone": "Be careful. Team deletion cannot be undone.",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"cancel_all_remaining": "Cancel all remaining",
|
||||
@@ -1387,6 +1393,8 @@
|
||||
"event_limit_tab_description": "How often you can be booked",
|
||||
"event_advanced_tab_description": "Calendar settings & more...",
|
||||
"event_advanced_tab_title": "Advanced",
|
||||
"event_payments_tab_title": "Payments",
|
||||
"event_payments_tab_description": "Set up payments for event",
|
||||
"event_setup_multiple_duration_error": "Event Setup: Multiple durations requires at least 1 option.",
|
||||
"event_setup_multiple_duration_default_error": "Event Setup: Please select a valid default duration.",
|
||||
"event_setup_booking_limits_error": "Booking limits must be in ascending order. [day, week, month, year]",
|
||||
@@ -1566,9 +1574,9 @@
|
||||
"route_to_the_right_person": "Route to the right person based on the answers to your form",
|
||||
"form_description": "Create your form to route a booker",
|
||||
"copy_link_to_form": "Copy link to form",
|
||||
"theme": "Booking Page Theme",
|
||||
"theme": "Booking page theme",
|
||||
"theme_applies_note": "This only applies to your public booking pages",
|
||||
"app_theme": "Dashboard Theme",
|
||||
"app_theme": "Dashboard theme",
|
||||
"app_theme_applies_note": "This only applies to your logged in dashboard",
|
||||
"theme_system": "System default",
|
||||
"add_a_team": "Add a team",
|
||||
@@ -1599,7 +1607,7 @@
|
||||
"no_calendar_installed": "No calendar installed",
|
||||
"no_calendar_installed_description": "You have not yet connected any of your calendars",
|
||||
"add_a_calendar": "Add a calendar",
|
||||
"change_email_hint": "You may need to log out and back in to see any change take effect",
|
||||
"change_email_hint": "You may need to log out and back in to see any change take effect.",
|
||||
"confirm_password_change_email": "Please confirm your password before changing your email address",
|
||||
"seats": "seats",
|
||||
"every_app_published": "Every app published on the {{appName}} App Store is open source and thoroughly tested via peer reviews. Nevertheless, {{companyName}} does not endorse or certify these apps unless they are published by {{appName}}. If you encounter inappropriate content or behaviour please report it.",
|
||||
@@ -1797,6 +1805,7 @@
|
||||
"wrong_code": "Wrong verification code",
|
||||
"not_verified": "Not yet verified",
|
||||
"no_availability_in_month": "No availability in {{month}}",
|
||||
"view_previous_month": "View previous month",
|
||||
"view_next_month": "View next month",
|
||||
"send_code": "Send code",
|
||||
"number_verified": "Number Verified",
|
||||
@@ -1848,7 +1857,7 @@
|
||||
"setup": "Setup",
|
||||
"setup_description": "Setup Cal.com instance",
|
||||
"configure": "Configure",
|
||||
"sso_configuration": "Single Sign-On",
|
||||
"sso_configuration": "Single sign-on",
|
||||
"sso_configuration_description": "Configure SAML/OIDC SSO and allow team members to login using an Identity Provider",
|
||||
"sso_configuration_description_orgs": "Configure SAML/OIDC SSO and allow organization members to login using an Identity Provider",
|
||||
"sso_oidc_heading": "SSO with OIDC",
|
||||
@@ -2222,7 +2231,7 @@
|
||||
"most_popular_events": "Most Popular Events",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Here's your summary of popular events for your team {{teamName}} for the last 30 days",
|
||||
"me": "Me",
|
||||
"monthly_digest_email": "Monthly Digest Email",
|
||||
"monthly_digest_email": "Monthly digest email",
|
||||
"monthly_digest_email_for_teams": "Monthly digest email for teams",
|
||||
"verify_team_tooltip": "Verify your team to enable sending messages to attendees",
|
||||
"member_removed": "Member removed",
|
||||
@@ -2549,7 +2558,8 @@
|
||||
"no_support_needed": "No Support Needed?",
|
||||
"hide_support": "Hide Support",
|
||||
"event_ratings": "Average Ratings",
|
||||
"event_no_show": "Host No Show",
|
||||
"event_no_show": "No-Show (Host)",
|
||||
"event_no_show_guest": "No-Show (Guest)",
|
||||
"recent_ratings": "Recent ratings",
|
||||
"no_ratings": "No ratings submitted",
|
||||
"no_ratings_description": "Add a workflow with 'Rating' to collect ratings after meetings",
|
||||
@@ -2650,16 +2660,45 @@
|
||||
"no_columns_found": "No columns found",
|
||||
"salesforce_create_record_as": "On booking, add events on and new attendees as:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Contact under an account",
|
||||
"salesforce_contact_under_account": "Contact under an account",
|
||||
"salesforce_skip_entry_creation": "Skip creating {{entry}} record if they do not exist in Salesforce",
|
||||
"salesforce_if_account_does_not_exist": "If the contact does not exist under an account, create new lead from attendee",
|
||||
"salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
|
||||
"salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
|
||||
"mass_assign_attributes": "Mass assign attributes",
|
||||
"reroute_preview_custom_message": "It results in showing custom message. Try changing the response to route to an event",
|
||||
"reroute_preview_external_redirect": "It results in redirecting to {{externalUrl}}. Try changing the response to route to an event",
|
||||
"reroute_preview_possible_host": "Possible host",
|
||||
"current_routing_status": "Current routing status",
|
||||
"reroute_booking": "Reroute booking",
|
||||
"reroute_booking_description": "Reroute the booking to different team members",
|
||||
"verify_new_route": "Verify new route",
|
||||
"reroute": "Reroute",
|
||||
"new_routing_status": "New routing status",
|
||||
"salesforce_book_directly_with_attendee_owner": "If attendee exists in Salesforce, book directly with the owner",
|
||||
"salesforce_check_owner_of": "Entity to check owner of to book directly",
|
||||
"account": "Account",
|
||||
"choose_hosts_schedule": "Choose host schedules",
|
||||
"hosts_schedule_description": "You can select a different schedule for each host. Each host's default schedule is chosen by default.",
|
||||
"no_hosts_description": "No hosts assigned, add hosts in assignment tab.",
|
||||
"team_member_round_robin_reassign": "Manually reassign round robin host",
|
||||
"team_member_round_robin_reassign_description": "Manually reassign round robin host to another available host",
|
||||
"round_robin_reassign_description": "Reassign round robin host to another available host",
|
||||
"invalid_round_robin_host": "New user is not a valid round-robin host for this event type",
|
||||
"user_is_round_robin_fixed": "New user is a fixed host for this event type",
|
||||
"reschedule_with_same_timeslot": "Reschedule with same timeslot",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Reschedule with same timeslot of the new event",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Reschedule to the new event with different timeslot",
|
||||
"reschedule_with_different_timeslot": "Reschedule with different timeslot",
|
||||
"we_dont_have_id_for_the_new_event": "We don't have ID for the new event. Please go to the Routing Form and save it again.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Rerouted booking successfully. Redirecting to booking page",
|
||||
"on_booking_write_to_event_object": "On booking, write to event object",
|
||||
"field_name": "Field name",
|
||||
"add_new_field": "Add new field",
|
||||
"you_dont_have_access_to_reroute_this_booking": "You don't have access to reroute this booking",
|
||||
"form_response_not_found": "Form response not found",
|
||||
"confirm_reassign_unavailable": "Manual Reassign to unavailable host",
|
||||
"reassign_unavailable_team_member_description": "Are you sure you want to reassign this booking to an unavailable host?",
|
||||
"yes_reassign": "Yes, reassign",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Permitir a los asistentes reservar a través de reservas dinámicas de grupo",
|
||||
"dynamic_booking": "Enlaces de grupo dinámicos",
|
||||
"allow_seo_indexing": "Permitir que los motores de búsqueda accedan a tu contenido público",
|
||||
"seo_indexing": "Permitir Indexación SEO",
|
||||
"seo_indexing": "Permitir indexación en motores de búsqueda",
|
||||
"allow_browser_notifications": "Permitir notificaciones del navegador",
|
||||
"you_have_denied_notifications": "Has denegado las notificaciones. Restablece el permiso en la configuración del navegador para habilitarlas.",
|
||||
"disable_browser_notifications": "Desactivar notificaciones del navegador",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Con qué frecuencia puedes ser reservado",
|
||||
"event_advanced_tab_description": "Configuración del calendario y más...",
|
||||
"event_advanced_tab_title": "Avanzado",
|
||||
"event_payments_tab_title": "Pagos",
|
||||
"event_payments_tab_description": "Configurar pagos para el evento",
|
||||
"event_setup_multiple_duration_error": "Configuración del evento: Duraciones múltiples requieren al menos 1 opción.",
|
||||
"event_setup_multiple_duration_default_error": "Configuración del evento: Por favor selecciona una duración predeterminada válida.",
|
||||
"event_setup_booking_limits_error": "Los límites de reserva deben estar en orden ascendente. [día, semana, mes, año]",
|
||||
@@ -1568,7 +1570,7 @@
|
||||
"copy_link_to_form": "Copiar enlace al formulario",
|
||||
"theme": "Tema de la página de reservas",
|
||||
"theme_applies_note": "Esto solo se aplica a tus páginas públicas de reservas",
|
||||
"app_theme": "Tema del panel",
|
||||
"app_theme": "Tema del panel de control",
|
||||
"app_theme_applies_note": "Esto solo se aplica a tu panel de control cuando has iniciado sesión",
|
||||
"theme_system": "Predeterminado del sistema",
|
||||
"add_a_team": "Agregar un equipo",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "No hay calendario instalado",
|
||||
"no_calendar_installed_description": "Aún no has conectado ninguno de tus calendarios",
|
||||
"add_a_calendar": "Agregar un calendario",
|
||||
"change_email_hint": "Es posible que necesites cerrar sesión y volver a iniciarla para que los cambios surtan efecto",
|
||||
"change_email_hint": "Es posible que necesites cerrar sesión y volver a iniciarla para ver los cambios aplicados.",
|
||||
"confirm_password_change_email": "Por favor confirma tu contraseña antes de cambiar tu dirección de correo electrónico",
|
||||
"seats": "asientos",
|
||||
"every_app_published": "Cada aplicación publicada en la {{appName}} App Store es de código abierto y ha sido rigurosamente probada mediante revisiones por pares. Sin embargo, {{companyName}} no respalda ni certifica estas aplicaciones a menos que sean publicadas por {{appName}}. Si encuentras contenido o comportamiento inapropiado, por favor repórtalo.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Reinstalar la aplicación",
|
||||
"reschedule_reason": "Razón para reprogramar",
|
||||
"choose_common_schedule_team_event": "Elige un horario común",
|
||||
"choose_common_schedule_team_event_description": "Habilita esto si deseas usar un horario común entre los anfitriones. Cuando está deshabilitado, cada anfitrión será reservado según su horario predeterminado.",
|
||||
"choose_common_schedule_team_event_description": "Activa esto si deseas usar un horario común entre los anfitriones. Cuando está desactivado, cada anfitrión será reservado según su horario predeterminado o el horario que hayan elegido.",
|
||||
"reason": "Razón",
|
||||
"sender_id": "ID del remitente",
|
||||
"sender_id_error_message": "Solo se permiten letras, números y espacios (máx. 11 caracteres)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Código de verificación incorrecto",
|
||||
"not_verified": "Aún no verificado",
|
||||
"no_availability_in_month": "No hay disponibilidad en {{month}}",
|
||||
"view_previous_month": "Ver mes anterior",
|
||||
"view_next_month": "Ver el próximo mes",
|
||||
"send_code": "Enviar código",
|
||||
"number_verified": "Número verificado",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Configuración",
|
||||
"setup_description": "Configurar instancia de Cal.com",
|
||||
"configure": "Configurar",
|
||||
"sso_configuration": "Inicio de Sesión Único (SSO)",
|
||||
"sso_configuration": "Inicio de sesión único",
|
||||
"sso_configuration_description": "Configurar SSO SAML/OIDC y permitir que los miembros del equipo inicien sesión usando un Proveedor de Identidad",
|
||||
"sso_configuration_description_orgs": "Configurar SSO SAML/OIDC y permitir que los miembros de la organización inicien sesión usando un Proveedor de Identidad",
|
||||
"sso_oidc_heading": "SSO con OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Eventos Más Populares",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Aquí tienes un resumen de los eventos populares para tu equipo {{teamName}} en los últimos 30 días",
|
||||
"me": "Yo",
|
||||
"monthly_digest_email": "Correo Electrónico del Resumen Mensual",
|
||||
"monthly_digest_email": "Correo electrónico de resumen mensual",
|
||||
"monthly_digest_email_for_teams": "Correo electrónico del resumen mensual para equipos",
|
||||
"verify_team_tooltip": "Verifica tu equipo para habilitar el envío de mensajes a los asistentes",
|
||||
"member_removed": "Miembro eliminado",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nueva opción",
|
||||
"update_profile": "Actualizar miembro",
|
||||
"attribute_updated_successfully": "Atributo actualizado con éxito",
|
||||
"attribute_deleted_successfully": "Atributo eliminado exitosamente",
|
||||
"attributes_edited_successfully": "Atributos editados con éxito",
|
||||
"attribute_meta_description": "Gestiona los atributos de los miembros de tu equipo",
|
||||
"attributes_edit_description": "Edita los atributos de los miembros de tu equipo",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "mes hasta la fecha",
|
||||
"year_to_date": "año hasta la fecha",
|
||||
"custom_range": "rango personalizado",
|
||||
"show_all_columns": "Mostrar todas las columnas",
|
||||
"toggle_columns": "Alternar columnas",
|
||||
"no_columns_found": "No se encontraron columnas",
|
||||
"salesforce_create_record_as": "Al reservar, agregar eventos y nuevos asistentes como:",
|
||||
"salesforce_lead": "Cliente potencial",
|
||||
"salesforce_contact_under_account": "Contacto bajo una cuenta",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Si el contacto no existe bajo una cuenta, crear un nuevo cliente potencial a partir del asistente",
|
||||
"salesforce_create_new_contact_under_account": "Crear un nuevo contacto bajo una cuenta basado en el dominio de correo electrónico del asistente y los contactos existentes",
|
||||
"mass_assign_attributes": "Asignar atributos en masa",
|
||||
"reroute_preview_custom_message": "Resulta en mostrar un mensaje personalizado. Intenta cambiar la respuesta para redirigir a un evento",
|
||||
"reroute_preview_external_redirect": "Resulta en redirigir a {{externalUrl}}. Intenta cambiar la respuesta para redirigir a un evento",
|
||||
"reroute_preview_possible_host": "Posible anfitrión",
|
||||
"current_routing_status": "Estado actual de enrutamiento",
|
||||
"reroute_booking": "Redirigir reserva",
|
||||
"reroute_booking_description": "Redirige la reserva a diferentes miembros del equipo",
|
||||
"verify_new_route": "Verificar nueva ruta",
|
||||
"reroute": "Redirigir",
|
||||
"new_routing_status": "Nuevo estado de enrutamiento",
|
||||
"salesforce_book_directly_with_attendee_owner": "Si el asistente existe en Salesforce, reservar directamente con el propietario",
|
||||
"salesforce_check_owner_of": "Entidad de la cual verificar el propietario para reservar directamente",
|
||||
"account": "Cuenta",
|
||||
"choose_hosts_schedule": "Elegir horarios de los anfitriones",
|
||||
"hosts_schedule_description": "Puedes seleccionar un horario diferente para cada anfitrión. Por defecto, se elige el horario predeterminado de cada anfitrión.",
|
||||
"no_hosts_description": "No hay anfitriones asignados, agrega anfitriones en la pestaña de asignación.",
|
||||
"reschedule_with_same_timeslot": "Reprogramar con el mismo horario",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Reprogramar con el mismo horario del nuevo evento",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Reprogramar al nuevo evento con un horario diferente",
|
||||
"reschedule_with_different_timeslot": "Reprogramar con un horario diferente",
|
||||
"we_dont_have_id_for_the_new_event": "No tenemos el ID para el nuevo evento. Por favor, ve al Formulario de Enrutamiento y guárdalo nuevamente.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Reserva redirigida exitosamente. Redirigiendo a la página de reserva",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Permitir que los asistentes lo reserven a través de reservas dinámicas de grupo",
|
||||
"dynamic_booking": "Enlaces dinámicos de grupo",
|
||||
"allow_seo_indexing": "Permita que los motores de búsqueda accedan a su contenido público",
|
||||
"seo_indexing": "Permitir indexación SEO",
|
||||
"seo_indexing": "Permitir indexación en motores de búsqueda",
|
||||
"allow_browser_notifications": "Permitir notificaciones del navegador",
|
||||
"you_have_denied_notifications": "Has denegado las notificaciones. Restablece el permiso en la configuración del navegador para habilitarlas.",
|
||||
"disable_browser_notifications": "Desactivar notificaciones del navegador",
|
||||
@@ -673,7 +673,7 @@
|
||||
"hide_book_a_team_member": "Ocultar el botón Reservar un miembro del equipo",
|
||||
"hide_book_a_team_member_description": "Ocultar el botón Reservar un miembro del equipo de sus páginas públicas.",
|
||||
"danger_zone": "Paso definitivo",
|
||||
"account_deletion_cannot_be_undone": "Tenga cuidado. La eliminación de la cuenta no se puede deshacer.",
|
||||
"account_deletion_cannot_be_undone": "Ten cuidado. La eliminación de la cuenta no se puede deshacer.",
|
||||
"team_deletion_cannot_be_undone": "Ten cuidado. La eliminación del equipo no se puede deshacer.",
|
||||
"back": "Atrás",
|
||||
"cancel": "Cancelar",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Con qué frecuencia se puede reservar",
|
||||
"event_advanced_tab_description": "Ajustes del calendario y más...",
|
||||
"event_advanced_tab_title": "Avanzado",
|
||||
"event_payments_tab_title": "Pagos",
|
||||
"event_payments_tab_description": "Configurar pagos para el evento",
|
||||
"event_setup_multiple_duration_error": "Configuración de eventos: varias duraciones requieren al menos una opción.",
|
||||
"event_setup_multiple_duration_default_error": "Configuración de eventos: seleccione una duración predeterminada válida.",
|
||||
"event_setup_booking_limits_error": "Los límites de las reservas deben estar en orden ascendente. [día, semana, mes, año]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Ruta a la persona adecuada en función de las respuestas a su formulario",
|
||||
"form_description": "Crea tu formulario para dirigirlo a un agente de reservas",
|
||||
"copy_link_to_form": "Copiar enlace al formulario",
|
||||
"theme": "Tema",
|
||||
"theme": "Tema de la página de reserva",
|
||||
"theme_applies_note": "Esto sólo se aplica a tus páginas públicas de reservas",
|
||||
"app_theme": "Tema del tablero",
|
||||
"app_theme": "Tema del panel de control",
|
||||
"app_theme_applies_note": "Esto solo se aplica a su tabla de control conectada",
|
||||
"theme_system": "Predeterminado del sistema",
|
||||
"add_a_team": "Añadir un equipo",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "No hay ningún calendario instalado",
|
||||
"no_calendar_installed_description": "Aún no has conectado ninguno de tus calendarios",
|
||||
"add_a_calendar": "Añadir un calendario",
|
||||
"change_email_hint": "Es posible que tengas que cerrar la sesión y volver a entrar para que los cambios surtan efecto",
|
||||
"change_email_hint": "Es posible que necesites cerrar sesión y volver a iniciarla para ver los cambios aplicados.",
|
||||
"confirm_password_change_email": "Confirma tu contraseña antes de cambiar la dirección de correo electrónico",
|
||||
"seats": "plazas",
|
||||
"every_app_published": "Todas las aplicaciones publicadas en la tienda de aplicaciones de {{appName}} son de código abierto y han sido probadas exhaustivamente mediante revisiones por parte de los usuarios. Sin embargo, {{companyName}} no respalda ni certifica estas aplicaciones a menos que sean publicadas por {{appName}}. Si encuentras contenidos o comportamientos inapropiados, notifícalos.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Reinstalar aplicación",
|
||||
"reschedule_reason": "Razón de la reprogramación",
|
||||
"choose_common_schedule_team_event": "Elige un horario común para todos",
|
||||
"choose_common_schedule_team_event_description": "Habilite esto si desea utilizar un cronograma común entre anfitriones. Cuando esté desactivado, cada anfitrión se reservará en relación a su horario predeterminado.",
|
||||
"choose_common_schedule_team_event_description": "Activa esta opción si deseas utilizar un horario común entre los anfitriones. Cuando está desactivada, cada anfitrión será reservado según su horario predeterminado o el horario que haya elegido.",
|
||||
"reason": "Motivo",
|
||||
"sender_id": "ID del remitente",
|
||||
"sender_id_error_message": "Sólo se permiten letras, números y espacios (máx. 11 caracteres)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Código de verificación incorrecto",
|
||||
"not_verified": "Aún no se ha verificado",
|
||||
"no_availability_in_month": "Sin disponibilidad en {{month}}",
|
||||
"view_previous_month": "Ver mes anterior",
|
||||
"view_next_month": "Ver el próximo mes",
|
||||
"send_code": "Enviar código",
|
||||
"number_verified": "Número verificado",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Configuración",
|
||||
"setup_description": "Configurar la instancia de Cal.com",
|
||||
"configure": "Configurar",
|
||||
"sso_configuration": "Configuración de SAML",
|
||||
"sso_configuration": "Inicio de sesión único",
|
||||
"sso_configuration_description": "Configure SAML/OIDC SSO y permita que los miembros del equipo inicien sesión con un proveedor de identidad",
|
||||
"sso_configuration_description_orgs": "Configure SSO SAML/OIDC y permita que los miembros de la organización inicien sesión utilizando un proveedor de identidad",
|
||||
"sso_oidc_heading": "SSO con OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Eventos más populares",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Este es el resumen de los eventos populares de su equipo {{teamName}} durante los últimos 30 días",
|
||||
"me": "Yo",
|
||||
"monthly_digest_email": "Correo electrónico del resumen mensual",
|
||||
"monthly_digest_email": "Correo electrónico de resumen mensual",
|
||||
"monthly_digest_email_for_teams": "Correo electrónico de resumen mensual para equipos",
|
||||
"verify_team_tooltip": "Verifique su equipo para activar el envío de mensajes a los asistentes",
|
||||
"member_removed": "Miembro eliminado",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nueva opción",
|
||||
"update_profile": "Actualizar miembro",
|
||||
"attribute_updated_successfully": "Atributo actualizado con éxito",
|
||||
"attribute_deleted_successfully": "Atributo eliminado con éxito",
|
||||
"attributes_edited_successfully": "Atributos editados con éxito",
|
||||
"attribute_meta_description": "Gestionar atributos para los miembros de tu equipo",
|
||||
"attributes_edit_description": "Editar atributos para los miembros de tu equipo",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "mes hasta la fecha",
|
||||
"year_to_date": "año hasta la fecha",
|
||||
"custom_range": "rango personalizado",
|
||||
"show_all_columns": "Mostrar todas las columnas",
|
||||
"toggle_columns": "Alternar columnas",
|
||||
"no_columns_found": "No se encontraron columnas",
|
||||
"salesforce_create_record_as": "Al reservar, agregar eventos y nuevos asistentes como:",
|
||||
"salesforce_lead": "Cliente potencial",
|
||||
"salesforce_contact_under_account": "Contacto bajo una cuenta",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Si el contacto no existe bajo una cuenta, crear un nuevo cliente potencial a partir del asistente",
|
||||
"salesforce_create_new_contact_under_account": "Crear un nuevo contacto bajo una cuenta basado en el dominio de correo electrónico del asistente y los contactos existentes",
|
||||
"mass_assign_attributes": "Asignar atributos en masa",
|
||||
"reroute_preview_custom_message": "Resulta en mostrar un mensaje personalizado. Intenta cambiar la respuesta para redirigir a un evento",
|
||||
"reroute_preview_external_redirect": "Resulta en redirigir a {{externalUrl}}. Intenta cambiar la respuesta para redirigir a un evento",
|
||||
"reroute_preview_possible_host": "Posible anfitrión",
|
||||
"current_routing_status": "Estado actual de enrutamiento",
|
||||
"reroute_booking": "Redirigir reserva",
|
||||
"reroute_booking_description": "Redirige la reserva a diferentes miembros del equipo",
|
||||
"verify_new_route": "Verificar nueva ruta",
|
||||
"reroute": "Redirigir",
|
||||
"new_routing_status": "Nuevo estado de enrutamiento",
|
||||
"salesforce_book_directly_with_attendee_owner": "Si el asistente existe en Salesforce, reservar directamente con el propietario",
|
||||
"salesforce_check_owner_of": "Entidad de la que verificar el propietario para reservar directamente",
|
||||
"account": "Cuenta",
|
||||
"choose_hosts_schedule": "Elegir horarios de los anfitriones",
|
||||
"hosts_schedule_description": "Puedes seleccionar un horario diferente para cada anfitrión. Por defecto, se elige el horario predeterminado de cada anfitrión.",
|
||||
"no_hosts_description": "No hay anfitriones asignados, añade anfitriones en la pestaña de asignación.",
|
||||
"reschedule_with_same_timeslot": "Reprogramar con el mismo horario",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Reprogramar con el mismo horario del nuevo evento",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Reprogramar al nuevo evento con un horario diferente",
|
||||
"reschedule_with_different_timeslot": "Reprogramar con un horario diferente",
|
||||
"we_dont_have_id_for_the_new_event": "No tenemos ID para el nuevo evento. Por favor, ve al Formulario de Enrutamiento y guárdalo de nuevo.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Reserva redirigida con éxito. Redirigiendo a la página de reserva",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Luba osalejatel teid dünaamiliste grupibroneeringute kaudu broneerida",
|
||||
"dynamic_booking": "Dünaamilised grupilingid",
|
||||
"allow_seo_indexing": "Luba otsingumootoritel juurdepääs teie avalikule sisule",
|
||||
"seo_indexing": "Luba SEO indekseerimine",
|
||||
"seo_indexing": "Luba otsingumootorite indekseerimine",
|
||||
"allow_browser_notifications": "Luba brauseri teavitused",
|
||||
"you_have_denied_notifications": "Oled teavitused keelanud. Luba need brauseri seadetes uuesti.",
|
||||
"disable_browser_notifications": "Keela brauseri teavitused",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Kui sageli saate broneerida",
|
||||
"event_advanced_tab_description": "Kalendri seaded ja muu...",
|
||||
"event_advanced_tab_title": "Täpsem",
|
||||
"event_payments_tab_title": "Maksed",
|
||||
"event_payments_tab_description": "Seadista sündmuse maksed",
|
||||
"event_setup_multiple_duration_error": "Sündmuse seadistamine: mitu kestust nõuab vähemalt ühte valikut.",
|
||||
"event_setup_multiple_duration_default_error": "Sündmuse seadistamine: valige kehtiv vaikekestus.",
|
||||
"event_setup_booking_limits_error": "Broneerimislimiidid peavad olema kasvavas järjekorras. [päev, nädal, kuu, aasta]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Tee vormi vastuste põhjal marsruut õigele inimesele",
|
||||
"form_description": "Loo oma vorm broneerija suunamiseks",
|
||||
"copy_link_to_form": "Kopeeri link vormile",
|
||||
"theme": "Broneerimislehe teema",
|
||||
"theme": "Broneerimislehe kujundus",
|
||||
"theme_applies_note": "See kehtib ainult teie avalike broneerimislehtede kohta",
|
||||
"app_theme": "Armatuurlaua teema",
|
||||
"app_theme": "Juhtpaneeli kujundus",
|
||||
"app_theme_applies_note": "See kehtib ainult teie sisselogitud juhtpaneeli kohta",
|
||||
"theme_system": "Süsteemi vaikeseade",
|
||||
"add_a_team": "Lisa meeskond",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Kalendrit pole installitud",
|
||||
"no_calendar_installed_description": "Te pole veel ühtegi oma kalendrit ühendanud",
|
||||
"add_a_calendar": "Lisa kalender",
|
||||
"change_email_hint": "Muudatuste jõustumiseks peate võib-olla välja logima ja uuesti sisse logima",
|
||||
"change_email_hint": "Muudatuse jõustumiseks võib olla vaja välja logida ja uuesti sisse logida.",
|
||||
"confirm_password_change_email": "Palun kinnitage oma parool enne e-posti aadressi muutmist",
|
||||
"seats": "istmed",
|
||||
"every_app_published": "Kõik rakenduses {{appName}} App Store avaldatud rakendused on avatud lähtekoodiga ja neid testitakse põhjalikult kolleegide arvustuste kaudu. Sellegipoolest ei toeta ega sertifitseeri {{companyName}} neid rakendusi, välja arvatud juhul, kui {{appName}} on need avaldanud. Kui te Kui märkate sobimatut sisu või käitumist, teavitage sellest.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "installige rakendus uuesti",
|
||||
"reschedule_reason": "Ajastamise põhjus",
|
||||
"choose_common_schedule_team_event": "Valige ühine ajakava",
|
||||
"choose_common_schedule_team_event_description": "Lubage see, kui soovite kasutada hostide vahel ühist ajakava. Kui see on keelatud, broneeritakse iga host nende vaikegraafiku alusel.",
|
||||
"choose_common_schedule_team_event_description": "Luba see, kui soovid võõrustajate vahel kasutada ühist ajakava. Kui see on keelatud, broneeritakse iga võõrustaja aeg vastavalt nende vaikimisi ajakavale või nende valitud ajakavale.",
|
||||
"reason": "Põhjus",
|
||||
"sender_id": "Saatja ID",
|
||||
"sender_id_error_message": "Lubatud on ainult tähed, numbrid ja tühikud (max 11 tähemärki)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Vale kinnituskood",
|
||||
"not_verified": "Pole veel kinnitatud",
|
||||
"no_availability_in_month": "Pole saadaval {{kuus}}",
|
||||
"view_previous_month": "Vaata eelmist kuud",
|
||||
"view_next_month": "Vaata järgmist kuud",
|
||||
"send_code": "Saada kood",
|
||||
"number_verified": "Number kinnitatud",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Kõige populaarsemad sündmused",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Siin on teie meeskonna {{teamName}} populaarsete sündmuste kokkuvõte viimase 30 päeva jooksul",
|
||||
"me": "Mina",
|
||||
"monthly_digest_email": "Monthly Digest Email",
|
||||
"monthly_digest_email": "Igakuine kokkuvõttev e-kiri",
|
||||
"monthly_digest_email_for_teams": "Igakuine kokkuvõtte meilimeeskondadele",
|
||||
"verify_team_tooltip": "Kinnitage oma meeskond, et võimaldada osalejatele sõnumite saatmine",
|
||||
"member_removed": "Liige eemaldatud",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Uus valik",
|
||||
"update_profile": "Uuenda liiget",
|
||||
"attribute_updated_successfully": "Atribuut edukalt uuendatud",
|
||||
"attribute_deleted_successfully": "Atribuut on edukalt kustutatud",
|
||||
"attributes_edited_successfully": "Atribuudid edukalt muudetud",
|
||||
"attribute_meta_description": "Halda atribuute oma meeskonnaliikmete jaoks",
|
||||
"attributes_edit_description": "Muuda atribuute oma meeskonnaliikmete jaoks",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "kuu algusest tänaseni",
|
||||
"year_to_date": "aasta algusest tänaseni",
|
||||
"custom_range": "kohandatud vahemik",
|
||||
"show_all_columns": "Näita kõiki veerge",
|
||||
"toggle_columns": "Lülita veerud sisse/välja",
|
||||
"no_columns_found": "Veerge ei leitud",
|
||||
"salesforce_create_record_as": "Broneeringu korral lisa sündmused ja uued osalejad kui:",
|
||||
"salesforce_lead": "Müügivihje",
|
||||
"salesforce_contact_under_account": "Kontakt konto all",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Kui kontakti ei eksisteeri konto all, loo osaleja andmetest uus müügivihje",
|
||||
"salesforce_create_new_contact_under_account": "Loo uus kontakt konto alla, põhinedes osaleja e-posti domeenile ja olemasolevatele kontaktidele",
|
||||
"mass_assign_attributes": "Massiline atribuutide määramine",
|
||||
"reroute_preview_custom_message": "See toob kaasa kohandatud sõnumi kuvamise. Proovi muuta vastust, et suunata sündmusele",
|
||||
"reroute_preview_external_redirect": "See toob kaasa ümbersuunamise aadressile {{externalUrl}}. Proovi muuta vastust, et suunata sündmusele",
|
||||
"reroute_preview_possible_host": "Võimalik võõrustaja",
|
||||
"current_routing_status": "Praegune marsruutimise olek",
|
||||
"reroute_booking": "Suuna broneering ümber",
|
||||
"reroute_booking_description": "Suuna broneering erinevatele meeskonnaliikmetele",
|
||||
"verify_new_route": "Kontrolli uut marsruuti",
|
||||
"reroute": "Suuna ümber",
|
||||
"new_routing_status": "Uus marsruutimise olek",
|
||||
"salesforce_book_directly_with_attendee_owner": "Kui osaleja eksisteerib Salesforce'is, broneeri otse omanikuga",
|
||||
"salesforce_check_owner_of": "Üksus, mille omanikku kontrollida otsebroneerimiseks",
|
||||
"account": "Konto",
|
||||
"choose_hosts_schedule": "Vali võõrustajate ajakavad",
|
||||
"hosts_schedule_description": "Saad valida igale võõrustajale erineva ajakava. Vaikimisi on valitud iga võõrustaja tavaline ajakava.",
|
||||
"no_hosts_description": "Võõrustajaid pole määratud, lisa need määramise vahekaardil.",
|
||||
"reschedule_with_same_timeslot": "Ajasta ümber sama ajavahemikuga",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Ajasta ümber uue sündmuse sama ajavahemikuga",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Ajasta ümber uuele sündmusele erineva ajavahemikuga",
|
||||
"reschedule_with_different_timeslot": "Ajasta ümber erineva ajavahemikuga",
|
||||
"we_dont_have_id_for_the_new_event": "Meil pole uue sündmuse ID-d. Palun mine marsruutimise vormile ja salvesta see uuesti.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Broneering edukalt ümber suunatud. Suunatakse broneerimislehele",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Autorisez les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques.",
|
||||
"dynamic_booking": "Liens de groupe dynamiques",
|
||||
"allow_seo_indexing": "Autorisez les moteurs de recherche à accéder à votre contenu public.",
|
||||
"seo_indexing": "Autoriser l'indexation SEO",
|
||||
"seo_indexing": "Autoriser l'indexation par les moteurs de recherche",
|
||||
"allow_browser_notifications": "Autoriser les notifications du navigateur",
|
||||
"you_have_denied_notifications": "Vous avez refusé les notifications. Réinitialisez l'autorisation dans les paramètres du navigateur pour les activer.",
|
||||
"disable_browser_notifications": "Désactiver les notifications du navigateur",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "Masquer le bouton Réserver un membre d'équipe",
|
||||
"hide_book_a_team_member_description": "Masquez le bouton Réserver un membre d'équipe de vos pages publiques.",
|
||||
"danger_zone": "Zone de danger",
|
||||
"account_deletion_cannot_be_undone": "Attention, la suppression de compte est irréversible.",
|
||||
"team_deletion_cannot_be_undone": "Attention. La suppression de l'équipe est irréversible",
|
||||
"account_deletion_cannot_be_undone": "Attention. La suppression du compte ne peut pas être annulée.",
|
||||
"team_deletion_cannot_be_undone": "Attention. La suppression de l'équipe ne peut pas être annulée.",
|
||||
"back": "Retour",
|
||||
"cancel": "Annuler",
|
||||
"cancel_all_remaining": "Annuler tous les événements restants",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Fréquence de réservation",
|
||||
"event_advanced_tab_description": "Paramètres du calendrier, etc.",
|
||||
"event_advanced_tab_title": "Avancé",
|
||||
"event_payments_tab_title": "Paiements",
|
||||
"event_payments_tab_description": "Configurer les paiements pour l'événement",
|
||||
"event_setup_multiple_duration_error": "Configuration de l'événement : plusieurs durées requièrent au moins une option.",
|
||||
"event_setup_multiple_duration_default_error": "Configuration de l'événement : veuillez sélectionner une durée par défaut valide.",
|
||||
"event_setup_booking_limits_error": "Les limites de réservation doivent être dans l'ordre croissant. [jour, semaine, mois, année]",
|
||||
@@ -1566,7 +1568,7 @@
|
||||
"route_to_the_right_person": "Acheminez vers la bonne personne en fonction des réponses à votre formulaire.",
|
||||
"form_description": "Créez votre formulaire pour rediriger un utilisateur.",
|
||||
"copy_link_to_form": "Copier le lien vers le formulaire",
|
||||
"theme": "Apparence",
|
||||
"theme": "Thème de la page de réservation",
|
||||
"theme_applies_note": "Ceci s'applique uniquement à vos pages publiques de réservation.",
|
||||
"app_theme": "Thème du tableau de bord",
|
||||
"app_theme_applies_note": "Cela s'applique uniquement à votre tableau de bord connecté",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Aucun calendrier installé",
|
||||
"no_calendar_installed_description": "Vous n'avez pas encore connecté vos calendriers.",
|
||||
"add_a_calendar": "Ajouter un calendrier",
|
||||
"change_email_hint": "Vous devrez peut-être vous déconnecter et vous reconnecter pour que le changement prenne effet.",
|
||||
"change_email_hint": "Vous devrez peut-être vous déconnecter et vous reconnecter pour voir les changements prendre effet.",
|
||||
"confirm_password_change_email": "Veuillez confirmer votre mot de passe avant de changer votre adresse e-mail",
|
||||
"seats": "places",
|
||||
"every_app_published": "Chaque application publiée sur l'App Store de {{appName}} est open source et soigneusement testée via des évaluations par les pairs. Néanmoins, {{companyName}} n'approuve ni ne certifie ces applications à moins qu'elles ne soient publiées par {{appName}}. Si vous rencontrez un contenu ou comportement inapproprié, veuillez le signaler.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Réinstaller l'application",
|
||||
"reschedule_reason": "Raison de la replanification",
|
||||
"choose_common_schedule_team_event": "Choisissez un horaire commun",
|
||||
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser des disponibilités communes entre les hôtes. Si cette option est désactivée, chaque hôte recevra les réservations en fonction de ses disponibilités par défaut.",
|
||||
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser un horaire commun entre les hôtes. Lorsqu'elle est désactivée, chaque hôte sera réservé en fonction de son horaire par défaut ou de l'horaire qu'il a choisi.",
|
||||
"reason": "Raison",
|
||||
"sender_id": "ID de l'expéditeur",
|
||||
"sender_id_error_message": "Seules les lettres, chiffres et espaces sont autorisés (max. 11 caractères)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Code de vérification incorrect",
|
||||
"not_verified": "Pas encore vérifié",
|
||||
"no_availability_in_month": "Aucune disponibilité en {{month}}",
|
||||
"view_previous_month": "Voir le mois précédent",
|
||||
"view_next_month": "Voir le mois suivant",
|
||||
"send_code": "Envoyer le code",
|
||||
"number_verified": "Numéro vérifié",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Événements les plus populaires",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Voici votre résumé des événements populaires pour votre équipe {{teamName}} au cours des 30 derniers jours",
|
||||
"me": "Moi",
|
||||
"monthly_digest_email": "E-mail de résumé mensuel",
|
||||
"monthly_digest_email": "E-mail de synthèse mensuelle",
|
||||
"monthly_digest_email_for_teams": "E-mail de résumé mensuel pour les équipes.",
|
||||
"verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants",
|
||||
"member_removed": "Membre supprimé",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nouvelle option",
|
||||
"update_profile": "Mettre à jour le membre",
|
||||
"attribute_updated_successfully": "Attribut mis à jour avec succès",
|
||||
"attribute_deleted_successfully": "Attribut supprimé avec succès",
|
||||
"attributes_edited_successfully": "Attributs modifiés avec succès",
|
||||
"attribute_meta_description": "Gérer les attributs de vos membres d'équipe",
|
||||
"attributes_edit_description": "Modifier les attributs de vos membres d'équipe",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "mois en cours",
|
||||
"year_to_date": "année en cours",
|
||||
"custom_range": "période personnalisée",
|
||||
"show_all_columns": "Afficher toutes les colonnes",
|
||||
"toggle_columns": "Basculer les colonnes",
|
||||
"no_columns_found": "Aucune colonne trouvée",
|
||||
"salesforce_create_record_as": "Lors de la réservation, ajouter les événements et les nouveaux participants en tant que :",
|
||||
"salesforce_lead": "Prospect",
|
||||
"salesforce_contact_under_account": "Contact sous un compte",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Si le contact n'existe pas sous un compte, créer un nouveau prospect à partir du participant",
|
||||
"salesforce_create_new_contact_under_account": "Créer un nouveau contact sous un compte basé sur le domaine e-mail du participant et les contacts existants",
|
||||
"mass_assign_attributes": "Attribuer des attributs en masse",
|
||||
"reroute_preview_custom_message": "Cela affiche un message personnalisé. Essayez de modifier la réponse pour rediriger vers un événement",
|
||||
"reroute_preview_external_redirect": "Cela entraîne une redirection vers {{externalUrl}}. Essayez de modifier la réponse pour rediriger vers un événement",
|
||||
"reroute_preview_possible_host": "Hôte possible",
|
||||
"current_routing_status": "Statut d'acheminement actuel",
|
||||
"reroute_booking": "Réacheminer la réservation",
|
||||
"reroute_booking_description": "Réacheminer la réservation vers différents membres de l'équipe",
|
||||
"verify_new_route": "Vérifier le nouvel itinéraire",
|
||||
"reroute": "Réacheminer",
|
||||
"new_routing_status": "Nouveau statut d'acheminement",
|
||||
"salesforce_book_directly_with_attendee_owner": "Si le participant existe dans Salesforce, réserver directement avec le propriétaire",
|
||||
"salesforce_check_owner_of": "Entité dont il faut vérifier le propriétaire pour réserver directement",
|
||||
"account": "Compte",
|
||||
"choose_hosts_schedule": "Choisir les horaires des hôtes",
|
||||
"hosts_schedule_description": "Vous pouvez sélectionner un horaire différent pour chaque hôte. L'horaire par défaut de chaque hôte est choisi par défaut.",
|
||||
"no_hosts_description": "Aucun hôte assigné, ajoutez des hôtes dans l'onglet d'affectation.",
|
||||
"reschedule_with_same_timeslot": "Replanifier avec le même créneau horaire",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Replanifier avec le même créneau horaire du nouvel événement",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Replanifier vers le nouvel événement avec un créneau horaire différent",
|
||||
"reschedule_with_different_timeslot": "Replanifier avec un créneau horaire différent",
|
||||
"we_dont_have_id_for_the_new_event": "Nous n'avons pas d'identifiant pour le nouvel événement. Veuillez retourner au formulaire de routage et l'enregistrer à nouveau.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Réservation réacheminée avec succès. Redirection vers la page de réservation",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "לאפשר למשתתפים לשריין איתך פגישה באמצעות הזמנות קבוצתיות דינמיות",
|
||||
"dynamic_booking": "קישורים לקבוצות דינמיות",
|
||||
"allow_seo_indexing": "מתן אפשרות למנועי חיפוש לגשת לתוכן הציבורי שלך",
|
||||
"seo_indexing": "מתן אפשרות להוספה לאינדקסים של SEO",
|
||||
"seo_indexing": "אפשר אינדוקס במנועי חיפוש",
|
||||
"allow_browser_notifications": "אפשר התראות דפדפן",
|
||||
"you_have_denied_notifications": "סירבת לקבל התראות. אפס את ההרשאה בהגדרות הדפדפן כדי לאפשר.",
|
||||
"disable_browser_notifications": "השבת התראות דפדפן",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "הסתרת הלחצן לשריון זמן של חבר/ת צוות",
|
||||
"hide_book_a_team_member_description": "הסתר/י את הלחצן לשריון זמן של חבר/ת צוות מהדפים הציבוריים שלך.",
|
||||
"danger_zone": "אזור מסוכן",
|
||||
"account_deletion_cannot_be_undone": "יש לנקוט זהירות. מחיקת חשבון היא פעולה בלתי הפיכה.",
|
||||
"team_deletion_cannot_be_undone": "יש לנקוט במשנה זהירות. מחיקת צוות היא פעולה בלתי הפיכה",
|
||||
"account_deletion_cannot_be_undone": "היזהר. לא ניתן לבטל מחיקת חשבון.",
|
||||
"team_deletion_cannot_be_undone": "היזהר. לא ניתן לבטל מחיקת צוות.",
|
||||
"back": "הקודם",
|
||||
"cancel": "ביטול",
|
||||
"cancel_all_remaining": "לבטל את כל הנותרים",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "באיזו תדירות ניתן להזמין אותך",
|
||||
"event_advanced_tab_description": "הגדרות לוח שנה ועוד...",
|
||||
"event_advanced_tab_title": "מתקדם",
|
||||
"event_payments_tab_title": "תשלומים",
|
||||
"event_payments_tab_description": "הגדרת תשלומים עבור האירוע",
|
||||
"event_setup_multiple_duration_error": "הגדרת אירוע: משכי זמן מרובים מחייבים הגדרה של אפשרות אחת לפחות.",
|
||||
"event_setup_multiple_duration_default_error": "הגדרת אירוע: יש לבחור משך זמן חוקי המוגדר כברירת מחדל.",
|
||||
"event_setup_booking_limits_error": "מגבלות על תזמונים חייבים להיות מוגדרים בסדר עולה. [יום, שבוע, חודש, שנה]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "מסלל לאדם הנכון בהתאם לתשובות בטופס שלך",
|
||||
"form_description": "צור/י טופס משלך כדי לנתב אל מזמין/ה",
|
||||
"copy_link_to_form": "להעתיק את הקישור לטופס",
|
||||
"theme": "ערכת נושא",
|
||||
"theme": "ערכת נושא לדף ההזמנות",
|
||||
"theme_applies_note": "חלה רק על דפים של הזמנות ציבוריות",
|
||||
"app_theme": "ערכת עיצוב ללוח בקרה",
|
||||
"app_theme": "ערכת נושא ללוח הבקרה",
|
||||
"app_theme_applies_note": "חל רק על לוח הבקרה שנכנסת אליו",
|
||||
"theme_system": "ברירת המחדל של המערכת",
|
||||
"add_a_team": "הוספת צוות",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "לא מותקן לוח שנה",
|
||||
"no_calendar_installed_description": "לא קישרת אף אחד מלוחות השנה שלך",
|
||||
"add_a_calendar": "הוספת לוח שנה",
|
||||
"change_email_hint": "ייתכן שיהיה צורך לצאת ולהיכנס שוב כדי לראות שהשינויים נכנסו לתוקף",
|
||||
"change_email_hint": "ייתכן שתצטרך להתנתק ולהתחבר מחדש כדי לראות את השינוי.",
|
||||
"confirm_password_change_email": "יש לאשר את הסיסמה לפני שינוי כתובת הדוא\"ל",
|
||||
"seats": "מקומות",
|
||||
"every_app_published": "כל אפליקציה שמתפרסמת ב-App Store של {{appName}} היא בעלת קוד מקור פתוח. כל אפליקציה כזו נבדקת בקפידה על ידי עמיתים. עם זאת, {{companyName}} אינה מעודדת את השימוש באף אפליקציה כזו ואינה מעניקה אישור לאפליקציות כאלה, אלא אם מדובר באפליקציות שפורסמו על ידי {{appName}}. אם נתקלת בהתנהגות או בתכנים בלתי הולמים, עליך לדווח על כך.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "להתקין את היישום מחדש",
|
||||
"reschedule_reason": "הסיבה לקביעת מועד חדש",
|
||||
"choose_common_schedule_team_event": "בחירת לוח זמנים משותף",
|
||||
"choose_common_schedule_team_event_description": "יש להפעיל אפשרות זו אם רוצים להשתמש בלוח זמנים משותף בין המארחים. אם האפשרות הזו מבוטלת, כל מארח יוזמן לפי לוח הזמנים המשמש כברירת המחדל שלו.",
|
||||
"choose_common_schedule_team_event_description": "הפעל אפשרות זו אם ברצונך להשתמש בלוח זמנים משותף בין המארחים. כאשר האפשרות מבוטלת, כל מארח יוזמן על פי לוח הזמנים המוגדר כברירת מחדל או לוח הזמנים שנבחר.",
|
||||
"reason": "סיבה",
|
||||
"sender_id": "מזהה השולח/ת",
|
||||
"sender_id_error_message": "מותר להשתמש רק באותיות, במספרים וברווחים (לכל היותר 11 תווים)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "קוד האימות שגוי",
|
||||
"not_verified": "האימות עדיין לא בוצע",
|
||||
"no_availability_in_month": "אין זמינות ב{{month}}",
|
||||
"view_previous_month": "צפה בחודש הקודם",
|
||||
"view_next_month": "צפייה בחודש הבא",
|
||||
"send_code": "שליחת קוד",
|
||||
"number_verified": "המספר אומת",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "התקנה",
|
||||
"setup_description": "התקנת מופע Cal.com",
|
||||
"configure": "הגדרה",
|
||||
"sso_configuration": "כניסת הזדהות אחידה SSO",
|
||||
"sso_configuration": "כניסה חד פעמית",
|
||||
"sso_configuration_description": "הגדר SSO מסוג SAML/OIDC ואפשר לחברי הצוות לבצע כניסה עם ספק הזדהות",
|
||||
"sso_configuration_description_orgs": "הגדרת SSO מסוג SAML/OIDC ולאפשר לחברי הארגון להיכנס עם ספק הזדהות",
|
||||
"sso_oidc_heading": "SSO עם OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "האירועים הפופולריים ביותר",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "הנה הסיכום של האירועים הפופולריים של הצוות שלך, {{teamName}}, ל-30 הימים האחרונים",
|
||||
"me": "אני",
|
||||
"monthly_digest_email": "אימייל עם סיכום חודשי",
|
||||
"monthly_digest_email": "דוא\"ל סיכום חודשי",
|
||||
"monthly_digest_email_for_teams": "אימייל עם סיכום חודשי עבור צוותים",
|
||||
"verify_team_tooltip": "אמת/י את הצוות שלך כדי לאפשר שליחת הודעות למשתתפים",
|
||||
"member_removed": "החבר הוסר",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "אפשרות חדשה",
|
||||
"update_profile": "עדכן חבר צוות",
|
||||
"attribute_updated_successfully": "המאפיין עודכן בהצלחה",
|
||||
"attribute_deleted_successfully": "התכונה נמחקה בהצלחה",
|
||||
"attributes_edited_successfully": "המאפיינים נערכו בהצלחה",
|
||||
"attribute_meta_description": "נהל מאפיינים עבור חברי הצוות שלך",
|
||||
"attributes_edit_description": "ערוך מאפיינים עבור חברי הצוות שלך",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "מתחילת החודש",
|
||||
"year_to_date": "מתחילת השנה",
|
||||
"custom_range": "טווח מותאם אישית",
|
||||
"show_all_columns": "הצג את כל העמודות",
|
||||
"toggle_columns": "החלף עמודות",
|
||||
"no_columns_found": "לא נמצאו עמודות",
|
||||
"salesforce_create_record_as": "בעת הזמנה, הוסף אירועים וחדשים משתתפים כ:",
|
||||
"salesforce_lead": "ליד",
|
||||
"salesforce_contact_under_account": "איש קשר תחת חשבון",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "אם איש הקשר לא קיים תחת חשבון, צור ליד חדש מהמשתתף",
|
||||
"salesforce_create_new_contact_under_account": "צור איש קשר חדש תחת חשבון בהתבסס על דומיין האימייל של המשתתף ואנשי קשר קיימים",
|
||||
"mass_assign_attributes": "הקצאת מאפיינים באופן גורף",
|
||||
"reroute_preview_custom_message": "התוצאה היא הצגת הודעה מותאמת אישית. נסה לשנות את התגובה כדי לנתב לאירוע",
|
||||
"reroute_preview_external_redirect": "התוצאה היא הפניה ל-{{externalUrl}}. נסה לשנות את התגובה כדי לנתב לאירוע",
|
||||
"reroute_preview_possible_host": "מארח אפשרי",
|
||||
"current_routing_status": "מצב ניתוב נוכחי",
|
||||
"reroute_booking": "ניתוב מחדש של ההזמנה",
|
||||
"reroute_booking_description": "נתב מחדש את ההזמנה לחברי צוות שונים",
|
||||
"verify_new_route": "אמת נתיב חדש",
|
||||
"reroute": "נתב מחדש",
|
||||
"new_routing_status": "מצב ניתוב חדש",
|
||||
"salesforce_book_directly_with_attendee_owner": "אם המשתתף קיים ב-Salesforce, הזמן ישירות עם הבעלים",
|
||||
"salesforce_check_owner_of": "ישות לבדוק את הבעלים שלה כדי להזמין ישירות",
|
||||
"account": "חשבון",
|
||||
"choose_hosts_schedule": "בחר לוחות זמנים למארחים",
|
||||
"hosts_schedule_description": "ניתן לבחור לוח זמנים שונה לכל מארח. לוח הזמנים המוגדר כברירת מחדל נבחר עבור כל מארח.",
|
||||
"no_hosts_description": "לא הוקצו מארחים, הוסף מארחים בלשונית ההקצאה.",
|
||||
"reschedule_with_same_timeslot": "קבע מועד חדש באותו משבצת זמן",
|
||||
"reschedule_with_same_timeslot_of_new_event": "קבע מועד חדש באותה משבצת זמן של האירוע החדש",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "קבע מועד חדש לאירוע החדש במשבצת זמן שונה",
|
||||
"reschedule_with_different_timeslot": "קבע מועד חדש במשבצת זמן שונה",
|
||||
"we_dont_have_id_for_the_new_event": "אין לנו מזהה לאירוע החדש. אנא עבור לטופס הניתוב ושמור אותו שוב.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "ההזמנה נותבה מחדש בהצלחה. מפנה לדף ההזמנות",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Lehetővé teszi a résztvevők számára, hogy dinamikus csoportos foglalásokon keresztül foglalhassanak",
|
||||
"dynamic_booking": "Dinamikus csoporthivatkozások",
|
||||
"allow_seo_indexing": "Engedélyezze a keresőmotorok számára, hogy hozzáférjenek nyilvános tartalmaihoz",
|
||||
"seo_indexing": "SEO indexelés engedélyezése",
|
||||
"seo_indexing": "Keresőmotorok indexelésének engedélyezése",
|
||||
"allow_browser_notifications": "Engedélyezze a böngésző értesítéseit",
|
||||
"you_have_denied_notifications": "Elutasította az értesítéseket. Engedélyezze újra a böngésző beállításaiban.",
|
||||
"disable_browser_notifications": "Tiltsa le a böngésző értesítéseit",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "Csapattag foglalása gomb elrejtése",
|
||||
"hide_book_a_team_member_description": "A Csoporttag foglalása gomb elrejtése nyilvános oldalairól.",
|
||||
"danger_zone": "Veszélyzóna",
|
||||
"account_deletion_cannot_be_undone": "Légy óvatos. A fiók törlése nem vonható vissza.",
|
||||
"team_deletion_cannot_be_undone": "Légy óvatos. A csapat törlése nem vonható vissza.",
|
||||
"account_deletion_cannot_be_undone": "Legyen óvatos. A fiók törlése nem vonható vissza.",
|
||||
"team_deletion_cannot_be_undone": "Legyen óvatos. A csapat törlése nem vonható vissza.",
|
||||
"back": "Vissza",
|
||||
"cancel": "Mégse",
|
||||
"cancel_all_remaining": "Összes lemondása",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Milyen gyakran vagy foglalható",
|
||||
"event_advanced_tab_description": "A naptárbeállítások és egyebek...",
|
||||
"event_advanced_tab_title": "Haladó",
|
||||
"event_payments_tab_title": "Fizetések",
|
||||
"event_payments_tab_description": "Fizetések beállítása az eseményhez",
|
||||
"event_setup_multiple_duration_error": "Esemény beállítása: Több időtartamhoz legalább 1 opció szükséges.",
|
||||
"event_setup_multiple_duration_default_error": "Esemény beállítása: Válasszon érvényes alapértelmezett időtartamot.",
|
||||
"event_setup_booking_limits_error": "A foglalási limiteknek növekvő sorrendben kell lenniük. [nap, hét, hónap, év]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Az űrlapra adott válaszok alapján irányítson a megfelelő személyhez",
|
||||
"form_description": "Hozzon létre űrlapot a foglaló átirányításához",
|
||||
"copy_link_to_form": "Link másolása az űrlapra",
|
||||
"theme": "Foglalási oldal téma",
|
||||
"theme": "Foglalási oldal témája",
|
||||
"theme_applies_note": "Ez csak a nyilvános foglalási oldalaira vonatkozik",
|
||||
"app_theme": "Irányítópult téma",
|
||||
"app_theme": "Irányítópult témája",
|
||||
"app_theme_applies_note": "Ez csak a bejelentkezett irányítópultra vonatkozik",
|
||||
"theme_system": "Rendszer alapértelmezett",
|
||||
"add_a_team": "Csapat hozzáadása",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Nincs telepítve naptár",
|
||||
"no_calendar_installed_description": "Még nem kapcsolta be egyik naptárát sem",
|
||||
"add_a_calendar": "Naptár hozzáadása",
|
||||
"change_email_hint": "Előfordulhat, hogy ki kell jelentkeznie, majd újra be kell jelentkeznie ahhoz, hogy a változtatások életbe lépjenek",
|
||||
"change_email_hint": "Előfordulhat, hogy ki kell jelentkeznie, majd újra be kell jelentkeznie, hogy lássa a változásokat.",
|
||||
"confirm_password_change_email": "Kérjük, erősítse meg jelszavát, mielőtt megváltoztatná e-mail címét",
|
||||
"seats": "ülések",
|
||||
"every_app_published": "Minden, az {{appName}} Alkalmazás Áruházban közzétett alkalmazás nyílt forráskódú, és szakértői értékeléseken keresztül alaposan tesztelték. Ennek ellenére a {{companyName}} nem támogatja és nem tanúsítja ezeket az alkalmazásokat, kivéve, ha azokat a {{appName}} tette közzé. Ha nem megfelelő tartalommal vagy viselkedéssel találkozik, kérjük, jelentse.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Alkalmazás újratelepítése",
|
||||
"reschedule_reason": "Időpontmódosítás oka",
|
||||
"choose_common_schedule_team_event": "Válasszon közös menetrendet",
|
||||
"choose_common_schedule_team_event_description": "Engedélyezze ezt, ha közös ütemezést szeretne használni a szervezők között. Ha le van tiltva, minden szervező az alapértelmezett ütemezésük alapján lesz lefoglalva.",
|
||||
"choose_common_schedule_team_event_description": "Engedélyezze ezt, ha közös ütemtervet szeretne használni a házigazdák között. Ha le van tiltva, minden házigazda az alapértelmezett vagy a választott ütemterv alapján lesz foglalva.",
|
||||
"reason": "Ok",
|
||||
"sender_id": "Feladó azonosítója",
|
||||
"sender_id_error_message": "Csak betűk, számok és szóközök megengedettek (max. 11 karakter)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Rossz ellenőrző kód",
|
||||
"not_verified": "Még nincs ellenőrizve",
|
||||
"no_availability_in_month": "Nem elérhető: {{month}}",
|
||||
"view_previous_month": "Előző hónap megtekintése",
|
||||
"view_next_month": "Következő hónap megtekintése",
|
||||
"send_code": "Kód küldése",
|
||||
"number_verified": "Szám ellenőrizve",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Legnépszerűbb események",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Íme az Ön csapata ({{teamName}}) népszerű eseményeinek összefoglalója az elmúlt 30 napban",
|
||||
"me": "Én",
|
||||
"monthly_digest_email": "Havi kivonat e-mail",
|
||||
"monthly_digest_email": "Havi összefoglaló e-mail",
|
||||
"monthly_digest_email_for_teams": "Havi kivonat e-mailek csapatoknak",
|
||||
"verify_team_tooltip": "Erősítse meg csapatát, hogy engedélyezze az üzenetek küldését a résztvevőknek",
|
||||
"member_removed": "Tag eltávolítva",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Új opció",
|
||||
"update_profile": "Tag frissítése",
|
||||
"attribute_updated_successfully": "Az attribútum sikeresen frissítve",
|
||||
"attribute_deleted_successfully": "Attribútum sikeresen törölve",
|
||||
"attributes_edited_successfully": "Az attribútumok sikeresen szerkesztve",
|
||||
"attribute_meta_description": "Attribútumok kezelése a csapattagok számára",
|
||||
"attributes_edit_description": "Attribútumok szerkesztése a csapattagok számára",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "hónap elejétől",
|
||||
"year_to_date": "év elejétől",
|
||||
"custom_range": "egyéni tartomány",
|
||||
"show_all_columns": "Összes oszlop megjelenítése",
|
||||
"toggle_columns": "Oszlopok váltása",
|
||||
"no_columns_found": "Nincsenek oszlopok",
|
||||
"salesforce_create_record_as": "Foglaláskor adja hozzá az eseményeket és az új résztvevőket mint:",
|
||||
"salesforce_lead": "Érdeklődő",
|
||||
"salesforce_contact_under_account": "Kapcsolat egy fiók alatt",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Ha a kapcsolat nem létezik egy fiók alatt, hozzon létre új érdeklődőt a résztvevőből",
|
||||
"salesforce_create_new_contact_under_account": "Hozzon létre új kapcsolatot egy fiók alatt a résztvevő e-mail domainje és meglévő kapcsolatok alapján",
|
||||
"mass_assign_attributes": "Attribútumok tömeges hozzárendelése",
|
||||
"reroute_preview_custom_message": "Egyedi üzenet megjelenítését eredményezi. Próbálja meg megváltoztatni a választ egy eseményhez való átirányításra",
|
||||
"reroute_preview_external_redirect": "A {{externalUrl}} címre történő átirányítást eredményezi. Próbálja meg megváltoztatni a választ egy eseményhez való átirányításra",
|
||||
"reroute_preview_possible_host": "Lehetséges házigazda",
|
||||
"current_routing_status": "Jelenlegi útválasztási állapot",
|
||||
"reroute_booking": "Foglalás átirányítása",
|
||||
"reroute_booking_description": "A foglalás átirányítása más csapattagokhoz",
|
||||
"verify_new_route": "Új útvonal ellenőrzése",
|
||||
"reroute": "Átirányítás",
|
||||
"new_routing_status": "Új útválasztási állapot",
|
||||
"salesforce_book_directly_with_attendee_owner": "Ha a résztvevő létezik a Salesforce-ban, foglaljon közvetlenül a tulajdonossal",
|
||||
"salesforce_check_owner_of": "Entitás, amelynek tulajdonosát ellenőrizni kell a közvetlen foglaláshoz",
|
||||
"account": "Fiók",
|
||||
"choose_hosts_schedule": "Válassza ki a házigazdák ütemtervét",
|
||||
"hosts_schedule_description": "Minden házigazdához különböző ütemtervet választhat. Alapértelmezés szerint minden házigazda alapértelmezett ütemterve van kiválasztva.",
|
||||
"no_hosts_description": "Nincsenek hozzárendelt házigazdák, adjon hozzá házigazdákat a hozzárendelési fülön.",
|
||||
"reschedule_with_same_timeslot": "Átütemezés ugyanabban az időpontban",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Átütemezés az új esemény ugyanazon időpontjában",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Átütemezés az új eseményre más időpontban",
|
||||
"reschedule_with_different_timeslot": "Átütemezés más időpontban",
|
||||
"we_dont_have_id_for_the_new_event": "Nincs azonosítónk az új eseményhez. Kérjük, menjen az Útválasztási űrlaphoz és mentse el újra.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "A foglalás átirányítása sikeres. Átirányítás a foglalási oldalra",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Consenti ai partecipanti di prenotare per te utilizzando le prenotazioni di gruppo dinamiche",
|
||||
"dynamic_booking": "Collegamenti dinamici di gruppo",
|
||||
"allow_seo_indexing": "Consenti ai motori di ricerca di accedere ai tuoi contenuti pubblici",
|
||||
"seo_indexing": "Consenti l'indicizzazione SEO",
|
||||
"seo_indexing": "Consenti l'indicizzazione dei motori di ricerca",
|
||||
"allow_browser_notifications": "Consenti notifiche del browser",
|
||||
"you_have_denied_notifications": "Hai negato le notifiche. Reimposta i permessi nelle impostazioni del browser per abilitarle.",
|
||||
"disable_browser_notifications": "Disabilita notifiche del browser",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "Nascondi pulsante Prenota un membro del team",
|
||||
"hide_book_a_team_member_description": "Nasconde il pulsante Prenota un membro del team nelle pagine pubbliche.",
|
||||
"danger_zone": "Zona pericolosa",
|
||||
"account_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione dell'account.",
|
||||
"team_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione del team",
|
||||
"account_deletion_cannot_be_undone": "Attenzione. L'eliminazione dell'account non può essere annullata.",
|
||||
"team_deletion_cannot_be_undone": "Attenzione. L'eliminazione del team non può essere annullata.",
|
||||
"back": "Indietro",
|
||||
"cancel": "Annulla",
|
||||
"cancel_all_remaining": "Annulla tutti i rimanenti",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Con quale frequenza è possibile fare una prenotazione",
|
||||
"event_advanced_tab_description": "Impostazioni calendario e altro...",
|
||||
"event_advanced_tab_title": "Avanzate",
|
||||
"event_payments_tab_title": "Pagamenti",
|
||||
"event_payments_tab_description": "Configura i pagamenti per l'evento",
|
||||
"event_setup_multiple_duration_error": "Impostazione evento: In caso di più durate è richiesta almeno 1 opzione.",
|
||||
"event_setup_multiple_duration_default_error": "Impostazione evento: Selezionare una durata predefinita valida.",
|
||||
"event_setup_booking_limits_error": "I limiti per le prenotazioni devono essere specificati in ordine crescente [giorno,settimana,mese,anno]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Instrada gli utenti verso la persona giusta in base alle risposte al modulo",
|
||||
"form_description": "Crea un modulo per instradare le persone che prenotano",
|
||||
"copy_link_to_form": "Copia il link del modulo",
|
||||
"theme": "Tema",
|
||||
"theme": "Tema della pagina di prenotazione",
|
||||
"theme_applies_note": "Si applica solo alle pagine di prenotazione pubbliche",
|
||||
"app_theme": "Tema Pannello di controllo",
|
||||
"app_theme": "Tema della dashboard",
|
||||
"app_theme_applies_note": "Questo riguarda solo il pannello di controllo per cui hai effettuato l'accesso",
|
||||
"theme_system": "Predefinito di sistema",
|
||||
"add_a_team": "Aggiungi un team",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Nessun calendario installato",
|
||||
"no_calendar_installed_description": "Non hai ancora collegato alcun calendario",
|
||||
"add_a_calendar": "Aggiungi un calendario",
|
||||
"change_email_hint": "Per rendere effettive le modifiche, potrebbe essere necessario disconnettersi e accedere nuovamente",
|
||||
"change_email_hint": "Potrebbe essere necessario disconnettersi e riconnettersi per vedere le modifiche applicate.",
|
||||
"confirm_password_change_email": "Conferma la password prima di cambiare l'indirizzo e-mail",
|
||||
"seats": "posti",
|
||||
"every_app_published": "Tutte le applicazioni pubblicate nell'App store di {{appName}} sono open source e sono state testate in modo approfondito attraverso una valutazione tra pari. Tuttavia, {{companyName}} non promuove né certifica tali applicazioni, a meno che siano pubblicate da {{appName}}. Se noti contenuti o comportamenti inappropriati, ti invitiamo a segnalarceli.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Reinstalla app",
|
||||
"reschedule_reason": "Motivo della riprogrammazione",
|
||||
"choose_common_schedule_team_event": "Scegli un programma comune",
|
||||
"choose_common_schedule_team_event_description": "Abilita questa opzione se desideri utilizzare un programma comune per tutti gli organizzatori. Quando l'opzione è disabilitata, ciascun organizzatore verrà prenotato in base al proprio programma predefinito.",
|
||||
"choose_common_schedule_team_event_description": "Abilita questa opzione se desideri utilizzare un orario comune tra gli host. Quando disabilitato, ogni host verrà prenotato in base al proprio orario predefinito o all'orario scelto.",
|
||||
"reason": "Motivo",
|
||||
"sender_id": "Id mittente",
|
||||
"sender_id_error_message": "Sono consentiti solo lettere, numeri e spazi (massimo 11 caratteri)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Codice di verifica errato",
|
||||
"not_verified": "Non ancora verificato",
|
||||
"no_availability_in_month": "Nessuna disponibilità in {{month}}",
|
||||
"view_previous_month": "Visualizza mese precedente",
|
||||
"view_next_month": "Visualizza il mese successivo",
|
||||
"send_code": "Invia codice",
|
||||
"number_verified": "Numero verificato",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Impostazione",
|
||||
"setup_description": "Imposta istanza Cal.com",
|
||||
"configure": "Configura",
|
||||
"sso_configuration": "Configurazione SAML",
|
||||
"sso_configuration": "Configurazione Single Sign-On",
|
||||
"sso_configuration_description": "Configura accesso SSO tramite SAML/OIDC e consenti ai membri del team di accedere utilizzando un provider di identità",
|
||||
"sso_configuration_description_orgs": "Configura l'accesso SSO con SAML/OIDC per consentire ai membri dell'organizzazione di accedere utilizzando un provider di identità",
|
||||
"sso_oidc_heading": "SSO con OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Eventi più popolari",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Ecco il riepilogo degli eventi popolari per il tuo team {{teamName}} negli ultimi 30 giorni",
|
||||
"me": "Io",
|
||||
"monthly_digest_email": "E-mail riepilogo mensile",
|
||||
"monthly_digest_email": "Email di riepilogo mensile",
|
||||
"monthly_digest_email_for_teams": "E-mail di riepilogo mensili per i team",
|
||||
"verify_team_tooltip": "Effettua la verifica del tuo team per abilitare l'invio di messaggi ai partecipanti",
|
||||
"member_removed": "Membro rimosso",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nuova opzione",
|
||||
"update_profile": "Aggiorna membro",
|
||||
"attribute_updated_successfully": "Attributo aggiornato con successo",
|
||||
"attribute_deleted_successfully": "Attributo eliminato con successo",
|
||||
"attributes_edited_successfully": "Attributi modificati con successo",
|
||||
"attribute_meta_description": "Gestisci gli attributi per i membri del tuo team",
|
||||
"attributes_edit_description": "Modifica gli attributi per i membri del tuo team",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "mese corrente",
|
||||
"year_to_date": "anno corrente",
|
||||
"custom_range": "intervallo personalizzato",
|
||||
"show_all_columns": "Mostra tutte le colonne",
|
||||
"toggle_columns": "Attiva/disattiva colonne",
|
||||
"no_columns_found": "Nessuna colonna trovata",
|
||||
"salesforce_create_record_as": "Al momento della prenotazione, aggiungi eventi e nuovi partecipanti come:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Contatto sotto un account",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Se il contatto non esiste sotto un account, crea un nuovo lead dal partecipante",
|
||||
"salesforce_create_new_contact_under_account": "Crea un nuovo contatto sotto un account basato sul dominio email del partecipante e contatti esistenti",
|
||||
"mass_assign_attributes": "Assegna attributi in massa",
|
||||
"reroute_preview_custom_message": "Risulta nella visualizzazione di un messaggio personalizzato. Prova a modificare la risposta per indirizzare a un evento",
|
||||
"reroute_preview_external_redirect": "Risulta nel reindirizzamento a {{externalUrl}}. Prova a modificare la risposta per indirizzare a un evento",
|
||||
"reroute_preview_possible_host": "Possibile host",
|
||||
"current_routing_status": "Stato di instradamento attuale",
|
||||
"reroute_booking": "Reindirizza prenotazione",
|
||||
"reroute_booking_description": "Reindirizza la prenotazione a diversi membri del team",
|
||||
"verify_new_route": "Verifica nuovo percorso",
|
||||
"reroute": "Reindirizza",
|
||||
"new_routing_status": "Nuovo stato di instradamento",
|
||||
"salesforce_book_directly_with_attendee_owner": "Se il partecipante esiste in Salesforce, prenota direttamente con il proprietario",
|
||||
"salesforce_check_owner_of": "Entità per verificare il proprietario per prenotare direttamente",
|
||||
"account": "Account",
|
||||
"choose_hosts_schedule": "Scegli gli orari degli host",
|
||||
"hosts_schedule_description": "Puoi selezionare un orario diverso per ogni host. L'orario predefinito di ciascun host è selezionato di default.",
|
||||
"no_hosts_description": "Nessun host assegnato, aggiungi host nella scheda di assegnazione.",
|
||||
"reschedule_with_same_timeslot": "Riprogramma con lo stesso slot orario",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Riprogramma con lo stesso slot orario del nuovo evento",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Riprogramma al nuovo evento con slot orario diverso",
|
||||
"reschedule_with_different_timeslot": "Riprogramma con slot orario diverso",
|
||||
"we_dont_have_id_for_the_new_event": "Non abbiamo l'ID per il nuovo evento. Per favore, vai al Modulo di Instradamento e salvalo di nuovo.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Prenotazione reindirizzata con successo. Reindirizzamento alla pagina di prenotazione",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "出席者が動的なグループ予約を通じてあなたを予約できるようにする",
|
||||
"dynamic_booking": "ダイナミックグループリンク",
|
||||
"allow_seo_indexing": "検索エンジンが公開コンテンツにアクセスできるようにする",
|
||||
"seo_indexing": "SEO インデックス作成を許可",
|
||||
"seo_indexing": "検索エンジンのインデックス作成を許可",
|
||||
"allow_browser_notifications": "ブラウザ通知を許可",
|
||||
"you_have_denied_notifications": "通知が拒否されました。ブラウザの設定で権限をリセットして有効にしてください。",
|
||||
"disable_browser_notifications": "ブラウザ通知を無効にする",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "「チームメンバーを予約する」ボタンを非表示にする",
|
||||
"hide_book_a_team_member_description": "公開ページから「チームメンバーを予約する」ボタンを非表示にします。",
|
||||
"danger_zone": "危険ゾーン",
|
||||
"account_deletion_cannot_be_undone": "ご注意ください。アカウントの削除は元に戻せません。",
|
||||
"team_deletion_cannot_be_undone": "注意: チームの削除は元に戻せません。",
|
||||
"account_deletion_cannot_be_undone": "注意してください。アカウントの削除は元に戻せません。",
|
||||
"team_deletion_cannot_be_undone": "注意してください。チームの削除は元に戻せません。",
|
||||
"back": "戻る",
|
||||
"cancel": "キャンセル",
|
||||
"cancel_all_remaining": "残りをすべてキャンセル",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "予約できる頻度",
|
||||
"event_advanced_tab_description": "カレンダー設定など...",
|
||||
"event_advanced_tab_title": "詳細設定",
|
||||
"event_payments_tab_title": "支払い",
|
||||
"event_payments_tab_description": "イベントの支払いを設定する",
|
||||
"event_setup_multiple_duration_error": "イベント設定:複数の期間には少なくとも1つのオプションが必要です。",
|
||||
"event_setup_multiple_duration_default_error": "イベント設定:有効なデフォルト期間を選択してください。",
|
||||
"event_setup_booking_limits_error": "予約制限の日時表記は、昇順にする必要があります。[日、週、月、年]",
|
||||
@@ -1566,7 +1568,7 @@
|
||||
"route_to_the_right_person": "フォームへの回答に基づいて適切なユーザーにルーティングする",
|
||||
"form_description": "予約者をルーティングするフォームを作成します",
|
||||
"copy_link_to_form": "フォームへのリンクをコピー",
|
||||
"theme": "テーマ",
|
||||
"theme": "予約ページのテーマ",
|
||||
"theme_applies_note": "公開予約ページにのみ適用されます",
|
||||
"app_theme": "ダッシュボードのテーマ",
|
||||
"app_theme_applies_note": "これは、ログインしているダッシュボードにのみ適用されます",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "カレンダーがインストールされていません",
|
||||
"no_calendar_installed_description": "まだどのカレンダーも接続していません",
|
||||
"add_a_calendar": "カレンダーを追加",
|
||||
"change_email_hint": "変更内容を反映させるためには、一度ログアウトしてからもう一度ログインする必要があります。",
|
||||
"change_email_hint": "変更を反映させるには、一度ログアウトして再度ログインする必要があるかもしれません。",
|
||||
"confirm_password_change_email": "メールアドレスを変更する前に、パスワードをご確認ください",
|
||||
"seats": "座席数",
|
||||
"every_app_published": "{{appName}} の App Store で公開されているすべてのアプリはオープンソースであり、ピアレビューを介して入念なテストが実施されています。しかしながら、{{companyName}} はこれらのアプリが {{appName}} で公開されていない限りは推奨または認証を行っていません。不適切なコンテンツまたは行為を発見した場合には、ぜひご一報ください。",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "アプリを再インストール",
|
||||
"reschedule_reason": "スケジュール変更の理由",
|
||||
"choose_common_schedule_team_event": "共通のスケジュールを選択",
|
||||
"choose_common_schedule_team_event_description": "ホスト間で共通のスケジュールを使用する場合には、これを有効にしてください。無効にすると、各ホストに対してはデフォルトのスケジュールに基づいて予約が行われます。",
|
||||
"choose_common_schedule_team_event_description": "ホスト間で共通のスケジュールを使用する場合はこれを有効にします。無効にすると、各ホストはデフォルトのスケジュールまたは選択されたスケジュールに基づいて予約されます。",
|
||||
"reason": "理由",
|
||||
"sender_id": "送信者 ID",
|
||||
"sender_id_error_message": "文字、数字、スペースのみが使用可能です (最大 11 文字)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "確認コードが間違っています",
|
||||
"not_verified": "まだ確認されていません",
|
||||
"no_availability_in_month": "{{month}} は参加できません",
|
||||
"view_previous_month": "前月を表示",
|
||||
"view_next_month": "来月を表示",
|
||||
"send_code": "コードを送信",
|
||||
"number_verified": "番号が確認されました",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "最も人気のイベント",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "こちらはこの 30 日間、チーム {{teamName}} で最も人気があったイベントのサマリーです",
|
||||
"me": "私",
|
||||
"monthly_digest_email": "月 1 回のダイジェストのメール",
|
||||
"monthly_digest_email": "月次ダイジェストメール",
|
||||
"monthly_digest_email_for_teams": "チームのための月 1 回のダイジェストのメール",
|
||||
"verify_team_tooltip": "出席者へのメッセージ送信ができるようにするには、チームを確認してください",
|
||||
"member_removed": "メンバーが削除されました",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "新しいオプション",
|
||||
"update_profile": "メンバーを更新",
|
||||
"attribute_updated_successfully": "属性が正常に更新されました",
|
||||
"attribute_deleted_successfully": "属性が正常に削除されました",
|
||||
"attributes_edited_successfully": "属性が正常に編集されました",
|
||||
"attribute_meta_description": "チームメンバーの属性を管理",
|
||||
"attributes_edit_description": "チームメンバーの属性を編集",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "月初から今日まで",
|
||||
"year_to_date": "年初から今日まで",
|
||||
"custom_range": "カスタム範囲",
|
||||
"show_all_columns": "すべての列を表示",
|
||||
"toggle_columns": "列を切り替える",
|
||||
"no_columns_found": "列が見つかりません",
|
||||
"salesforce_create_record_as": "予約時にイベントを追加し、新しい参加者を次のように追加します:",
|
||||
"salesforce_lead": "リード",
|
||||
"salesforce_contact_under_account": "アカウント下の連絡先",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "連絡先がアカウントに存在しない場合、参加者から新しいリードを作成する",
|
||||
"salesforce_create_new_contact_under_account": "参加者のメールドメインと既存の連絡先に基づいて、新しい連絡先をアカウント下に作成する",
|
||||
"mass_assign_attributes": "属性の一括割り当て",
|
||||
"reroute_preview_custom_message": "カスタムメッセージが表示されます。応答を変更してイベントにルーティングしてみてください",
|
||||
"reroute_preview_external_redirect": "{{externalUrl}}にリダイレクトされます。応答を変更してイベントにルーティングしてみてください",
|
||||
"reroute_preview_possible_host": "可能なホスト",
|
||||
"current_routing_status": "現在のルーティング状況",
|
||||
"reroute_booking": "予約の再ルーティング",
|
||||
"reroute_booking_description": "予約を別のチームメンバーに再ルーティングする",
|
||||
"verify_new_route": "新しいルートを確認",
|
||||
"reroute": "再ルーティング",
|
||||
"new_routing_status": "新しいルーティング状況",
|
||||
"salesforce_book_directly_with_attendee_owner": "参加者がSalesforceに存在する場合、所有者と直接予約する",
|
||||
"salesforce_check_owner_of": "直接予約するために所有者を確認するエンティティ",
|
||||
"account": "アカウント",
|
||||
"choose_hosts_schedule": "ホストのスケジュールを選択",
|
||||
"hosts_schedule_description": "各ホストに異なるスケジュールを選択できます。各ホストのデフォルトスケジュールが初期設定として選択されています。",
|
||||
"no_hosts_description": "ホストが割り当てられていません。割り当てタブでホストを追加してください。",
|
||||
"reschedule_with_same_timeslot": "同じタイムスロットで再スケジュール",
|
||||
"reschedule_with_same_timeslot_of_new_event": "新しいイベントの同じタイムスロットで再スケジュール",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "異なるタイムスロットで新しいイベントに再スケジュール",
|
||||
"reschedule_with_different_timeslot": "異なるタイムスロットで再スケジュール",
|
||||
"we_dont_have_id_for_the_new_event": "新しいイベントのIDがありません。ルーティングフォームに戻って再度保存してください。",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "予約の再ルーティングに成功しました。予約ページにリダイレクトしています",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "អនុញ្ញាតឱ្យអ្នកចូលរួមកក់អ្នកតាមរយៈការកក់ក្រុមដោយស្វ័យប្រវត្តិ",
|
||||
"dynamic_booking": "តំណភ្ជាប់ក្រុមដោយស្វ័យប្រវត្តិ",
|
||||
"allow_seo_indexing": "អនុញ្ញាតឱ្យម៉ាស៊ីនស្វែងរកចូលដំណើរការការគ្រប់គ្រងសាធារណៈរបស់អ្នក",
|
||||
"seo_indexing": "អនុញ្ញាត SEO Indexing",
|
||||
"seo_indexing": "អនុញ្ញាតឱ្យម៉ាស៊ីនស្វែងរកធ្វើការដាក់លិបិក្រម",
|
||||
"allow_browser_notifications": "អនុញ្ញាតការជូនដំណឹងតាមរយៈកម្មវិធីរុករក",
|
||||
"you_have_denied_notifications": "អ្នកបានបដិសេធការជូនដំណឹង។ កំណត់សិទ្ធិឡើងវិញក្នុងការកំណត់កម្មវិធីរុករកដើម្បីអនុញ្ញាត។",
|
||||
"disable_browser_notifications": "បិទការជូនដំណឹងតាមរយៈកម្មវិធីរុករក",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "លាក់ប៊ូតុងកក់សមាជិកក្រុម",
|
||||
"hide_book_a_team_member_description": "លាក់ប៊ូតុងកក់សមាជិកក្រុមពីទំព័រសាធារណៈរបស់អ្នក។",
|
||||
"danger_zone": "តំបន់គ្រោះថ្នាក់",
|
||||
"account_deletion_cannot_be_undone": "ប្រយ័ត្ន។ ការលុបគណនីមិនអាចត្រឡប់វិញបានទេ។",
|
||||
"team_deletion_cannot_be_undone": "ប្រយ័ត្ន។ ការលុបក្រុមមិនអាចត្រឡប់វិញបានទេ។",
|
||||
"account_deletion_cannot_be_undone": "សូមប្រុងប្រយ័ត្ន។ ការលុបគណនីមិនអាចត្រឡប់ក្រោយវិញបានទេ។",
|
||||
"team_deletion_cannot_be_undone": "សូមប្រុងប្រយ័ត្ន។ ការលុបក្រុមមិនអាចត្រឡប់ក្រោយវិញបានទេ។",
|
||||
"back": "ត្រឡប់",
|
||||
"cancel": "បោះបង់",
|
||||
"cancel_all_remaining": "បោះបង់ទាំងអស់ដែលនៅសល់",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "ភាពញឹកញាប់ដែលអ្នកអាចត្រូវបានកក់",
|
||||
"event_advanced_tab_description": "ការកំណត់កាលវិភាគ និងច្រើនទៀត...",
|
||||
"event_advanced_tab_title": "កម្រិតខ្ពស់",
|
||||
"event_payments_tab_title": "ការទូទាត់",
|
||||
"event_payments_tab_description": "រៀបចំការទូទាត់សម្រាប់ព្រឹត្តិការណ៍",
|
||||
"event_setup_multiple_duration_error": "ការកំណត់ព្រឹត្តិការណ៍: រយៈពេលច្រើនត្រូវការយ៉ាងហោចណាស់ 1 ជម្រើស។",
|
||||
"event_setup_multiple_duration_default_error": "ការកំណត់ព្រឹត្តិការណ៍: សូមជ្រើសរយៈពេលលំនាំដើមដែលត្រឹមត្រូវ។",
|
||||
"event_setup_booking_limits_error": "ដែនកំណត់ការកក់ត្រូវតែមានលំដាប់ឡើងលើ។ [ថ្ងៃ, សប្តាហ៍, ខែ, ឆ្នាំ]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "ដឹកនាំទៅកាន់មនុស្សត្រឹមត្រូវដោយផ្អែកលើចម្លើយនៃទម្រង់របស់អ្នក",
|
||||
"form_description": "បង្កើតទម្រង់របស់អ្នកដើម្បីដឹកនាំអ្នកកក់",
|
||||
"copy_link_to_form": "ចម្លងតំណទៅទម្រង់",
|
||||
"theme": "ប្រធានបទទំព័រការកក់",
|
||||
"theme": "រូបរាងទំព័រកក់",
|
||||
"theme_applies_note": "នេះអនុវត្តចំពោះទំព័រការកក់សាធារណៈរបស់អ្នកប៉ុណ្ណោះ",
|
||||
"app_theme": "ប្រធានបទផ្ទាំងគ្រប់គ្រង",
|
||||
"app_theme": "រូបរាងផ្ទាំងគ្រប់គ្រង",
|
||||
"app_theme_applies_note": "នេះអនុវត្តចំពោះផ្ទាំងគ្រប់គ្រងដែលបានចូលរបស់អ្នកប៉ុណ្ណោះ",
|
||||
"theme_system": "លំនាំប្រព័ន្ធ",
|
||||
"add_a_team": "បន្ថែមក្រុម",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "មិនមានប្រតិទិនបានដំឡើង",
|
||||
"no_calendar_installed_description": "អ្នកមិនទាន់បានភ្ជាប់ប្រតិទិនណាមួយទេ",
|
||||
"add_a_calendar": "បន្ថែមប្រតិទិន",
|
||||
"change_email_hint": "អ្នកប្រហែលជាត្រូវចាកចេញហើយចូលវិញដើម្បីឃើញការផ្លាស់ប្តូរបានប្រសិទ្ធិភាព",
|
||||
"change_email_hint": "អ្នកអាចត្រូវចេញពីគណនី និងចូលម្តងទៀតដើម្បីមើលឃើញការផ្លាស់ប្តូរណាមួយមានប្រសិទ្ធភាព។",
|
||||
"confirm_password_change_email": "សូមបញ្ជាក់ពាក្យសម្ងាត់របស់អ្នកមុនពេលផ្លាស់ប្តូរអាសយដ្ឋានអ៊ីមែល",
|
||||
"seats": "កៅអី",
|
||||
"every_app_published": "កម្មវិធីគ្រប់យ៉ាងដែលបានបោះពុម្ពផ្សាយនៅលើ {{appName}} App Store គឺជាប្រភពបើកចំហ និងត្រូវបានធ្វើតេស្តយ៉ាងម៉ត់ចត់តាមរយៈការពិនិត្យរបស់មិត្តរួមការងារ។ ទោះយ៉ាងណា {{companyName}} មិនទទួលស្គាល់ឬបញ្ជាក់កម្មវិធីទាំងនេះទេ លុះត្រាតែវាត្រូវបានបោះពុម្ពផ្សាយដោយ {{appName}}។ ប្រសិនបើអ្នកជួបប្រទះមាតិកាឬអាកប្បកិរិយាដែលមិនសមរម្យ សូមរាយការណ៍។",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "ដំឡើងកម្មវិធីឡើងវិញ",
|
||||
"reschedule_reason": "មូលហេតុការកំណត់ពេលថ្មី",
|
||||
"choose_common_schedule_team_event": "ជ្រើសរើសកាលវិភាគរួម",
|
||||
"choose_common_schedule_team_event_description": "បើកមុខងារនេះប្រសិនបើអ្នកចង់ប្រើកាលវិភាគរួមរវាងម្ចាស់ផ្ទះ។ នៅពេលបិទ ម្ចាស់ផ្ទះនីមួយៗនឹងត្រូវបានកក់ដោយផ្អែកលើកាលវិភាគលំនាំដើមរបស់ពួកគេ។",
|
||||
"choose_common_schedule_team_event_description": "បើកមុខងារនេះប្រសិនបើអ្នកចង់ប្រើកាលវិភាគរួមរវាងម្ចាស់ផ្ទះ។ នៅពេលបិទ ម្ចាស់ផ្ទះនីមួយៗនឹងត្រូវបានកក់ដោយផ្អែកលើកាលវិភាគលំនាំដើមរបស់ពួកគេ ឬកាលវិភាគដែលពួកគេបានជ្រើសរើស។",
|
||||
"reason": "មូលហេតុ",
|
||||
"sender_id": "លេខសម្គាល់អ្នកផ្ញើ",
|
||||
"sender_id_error_message": "អនុញ្ញាតអក្សរ លេខ និងចន្លោះតែប៉ុណ្ណោះ (ច្រើនបំផុត 11 តួអក្សរ)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "កូដផ្ទៀងផ្ទាត់មិនត្រឹមត្រូវ",
|
||||
"not_verified": "មិនទាន់បានផ្ទៀងផ្ទាត់",
|
||||
"no_availability_in_month": "គ្មានភាពអាចប្រើបានក្នុងខែ {{month}}",
|
||||
"view_previous_month": "មើលខែមុន",
|
||||
"view_next_month": "មើលខែក្រោយ",
|
||||
"send_code": "ផ្ញើកូដ",
|
||||
"number_verified": "លេខបានផ្ទៀងផ្ទាត់",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "ការដំឡើង",
|
||||
"setup_description": "ដំឡើងឧបករណ៍ Cal.com",
|
||||
"configure": "កំណត់រចនា",
|
||||
"sso_configuration": "Single Sign-On",
|
||||
"sso_configuration": "ការកំណត់រចនាសម្ព័ន្ធចូលប្រើប្រាស់តែមួយ",
|
||||
"sso_configuration_description": "កំណត់ SAML/OIDC SSO និងអនុញ្ញាតឱ្យសមាជិកក្រុមចូលប្រើដោយប្រើអ្នកផ្តល់អត្តសញ្ញាណ",
|
||||
"sso_configuration_description_orgs": "កំណត់ SAML/OIDC SSO និងអនុញ្ញាតឱ្យសមាជិកអង្គការចូលប្រើដោយប្រើអ្នកផ្តល់អត្តសញ្ញាណ",
|
||||
"sso_oidc_heading": "SSO ជាមួយ OIDC",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "ជម្រើសថ្មី",
|
||||
"update_profile": "ធ្វើបច្ចុប្បន្នភាពសមាជិក",
|
||||
"attribute_updated_successfully": "គុណលក្ខណៈត្រូវបានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ",
|
||||
"attribute_deleted_successfully": "លក្ខណៈពិសេសត្រូវបានលុបដោយជោគជ័យ",
|
||||
"attributes_edited_successfully": "គុណលក្ខណៈត្រូវបានកែសម្រួលដោយជោគជ័យ",
|
||||
"attribute_meta_description": "គ្រប់គ្រងគុណលក្ខណៈសម្រាប់សមាជិកក្រុមរបស់អ្នក",
|
||||
"attributes_edit_description": "កែសម្រួលគុណលក្ខណៈសម្រាប់សមាជិកក្រុមរបស់អ្នក",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "ខែនេះ",
|
||||
"year_to_date": "ឆ្នាំនេះ",
|
||||
"custom_range": "ជួរដែលកំណត់ដោយខ្លួនឯង",
|
||||
"show_all_columns": "បង្ហាញជួរឈរទាំងអស់",
|
||||
"toggle_columns": "បិទបើកជួរឈរ",
|
||||
"no_columns_found": "រកមិនឃើញជួរឈរ",
|
||||
"salesforce_create_record_as": "នៅពេលកក់ សូមបន្ថែមព្រឹត្តិការណ៍និងអ្នកចូលរួមថ្មីជា៖",
|
||||
"salesforce_lead": "អ្នកដឹកនាំ",
|
||||
"salesforce_contact_under_account": "ទំនាក់ទំនងក្រោមគណនី",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "ប្រសិនបើទំនាក់ទំនងមិនមានក្រោមគណនី សូមបង្កើតអ្នកដឹកនាំថ្មីពីអ្នកចូលរួម",
|
||||
"salesforce_create_new_contact_under_account": "បង្កើតទំនាក់ទំនងថ្មីក្រោមគណនីដោយផ្អែកលើដែនអ៊ីមែលរបស់អ្នកចូលរួម និងទំនាក់ទំនងដែលមានស្រាប់",
|
||||
"mass_assign_attributes": "ផ្ដល់គុណលក្ខណៈជាសាធារណៈ",
|
||||
"reroute_preview_custom_message": "វានាំឱ្យបង្ហាញសារផ្ទាល់ខ្លួន។ សាកល្បងផ្លាស់ប្តូរការឆ្លើយតបដើម្បីបញ្ជូនទៅកាន់ព្រឹត្តិការណ៍",
|
||||
"reroute_preview_external_redirect": "វានាំឱ្យបញ្ជូនបន្តទៅ {{externalUrl}}។ សាកល្បងផ្លាស់ប្តូរការឆ្លើយតបដើម្បីបញ្ជូនទៅកាន់ព្រឹត្តិការណ៍",
|
||||
"reroute_preview_possible_host": "ម្ចាស់ផ្ទះដែលអាចមាន",
|
||||
"current_routing_status": "ស្ថានភាពនៃការបញ្ជូនបច្ចុប្បន្ន",
|
||||
"reroute_booking": "បញ្ជូនការកក់ឡើងវិញ",
|
||||
"reroute_booking_description": "បញ្ជូនការកក់ទៅសមាជិកក្រុមផ្សេងទៀត",
|
||||
"verify_new_route": "ផ្ទៀងផ្ទាត់ផ្លូវថ្មី",
|
||||
"reroute": "បញ្ជូនឡើងវិញ",
|
||||
"new_routing_status": "ស្ថានភាពនៃការបញ្ជូនថ្មី",
|
||||
"salesforce_book_directly_with_attendee_owner": "ប្រសិនបើអ្នកចូលរួមមាននៅក្នុង Salesforce សូមកក់ដោយផ្ទាល់ជាមួយម្ចាស់",
|
||||
"salesforce_check_owner_of": "អង្គភាពត្រូវពិនិត្យម្ចាស់ដើម្បីកក់ដោយផ្ទាល់",
|
||||
"account": "គណនី",
|
||||
"choose_hosts_schedule": "ជ្រើសរើសកាលវិភាគរបស់ម្ចាស់ផ្ទះ",
|
||||
"hosts_schedule_description": "អ្នកអាចជ្រើសរើសកាលវិភាគផ្សេងៗសម្រាប់ម្ចាស់ផ្ទះនីមួយៗ។ កាលវិភាគលំនាំដើមរបស់ម្ចាស់ផ្ទះនីមួយៗត្រូវបានជ្រើសរើសជាលំនាំដើម។",
|
||||
"no_hosts_description": "មិនមានម្ចាស់ផ្ទះបានចាត់តាំងទេ សូមបន្ថែមម្ចាស់ផ្ទះនៅក្នុងផ្ទាំងចាត់តាំង។",
|
||||
"reschedule_with_same_timeslot": "កំណត់ពេលវេលាឡើងវិញជាមួយនឹងពេលវេលាដដែល",
|
||||
"reschedule_with_same_timeslot_of_new_event": "កំណត់ពេលវេលាឡើងវិញជាមួយនឹងពេលវេលាដដែលនៃព្រឹត្តិការណ៍ថ្មី",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "កំណត់ពេលវេលាឡើងវិញទៅព្រឹត្តិការណ៍ថ្មីជាមួយនឹងពេលវេលាខុសគ្នា",
|
||||
"reschedule_with_different_timeslot": "កំណត់ពេលវេលាឡើងវិញជាមួយនឹងពេលវេលាខុសគ្នា",
|
||||
"we_dont_have_id_for_the_new_event": "យើងមិនមានលេខសម្គាល់សម្រាប់ព្រឹត្តិការណ៍ថ្មីទេ។ សូមទៅកាន់ទម្រង់បញ្ជូន និងរក្សាទុកម្តងទៀត។",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "បានបញ្ជូនការកក់ដោយជោគជ័យ។ កំពុងបញ្ជូនបន្តទៅទំព័រកក់",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "참석자가 동적 그룹 예약을 통해 귀하를 예약하도록 허용",
|
||||
"dynamic_booking": "동적 그룹 링크",
|
||||
"allow_seo_indexing": "검색 엔진이 공개 콘텐츠에 액세스하도록 허용",
|
||||
"seo_indexing": "SEO 인덱싱 허용",
|
||||
"seo_indexing": "검색 엔진 색인 허용",
|
||||
"allow_browser_notifications": "브라우저 알림 허용",
|
||||
"you_have_denied_notifications": "알림이 거부되었습니다. 브라우저 설정에서 권한을 재설정하여 활성화하세요.",
|
||||
"disable_browser_notifications": "브라우저 알림 비활성화",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "팀원 예약 버튼 숨기기",
|
||||
"hide_book_a_team_member_description": "공개 페이지에서 팀원 예약 버튼을 숨깁니다.",
|
||||
"danger_zone": "위험 구역",
|
||||
"account_deletion_cannot_be_undone": "주의하세요. 계정 삭제 후에는 되돌릴 수 없습니다.",
|
||||
"team_deletion_cannot_be_undone": "주의하세요. 팀 삭제는 되돌릴 수 없습니다.",
|
||||
"account_deletion_cannot_be_undone": "주의하세요. 계정 삭제는 취소할 수 없습니다.",
|
||||
"team_deletion_cannot_be_undone": "주의하세요. 팀 삭제는 취소할 수 없습니다.",
|
||||
"back": "뒤로 가기",
|
||||
"cancel": "취소",
|
||||
"cancel_all_remaining": "나머지 모두 취소",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "예약 가능 횟수",
|
||||
"event_advanced_tab_description": "캘린더 설정 및 기타...",
|
||||
"event_advanced_tab_title": "고급",
|
||||
"event_payments_tab_title": "결제",
|
||||
"event_payments_tab_description": "이벤트 결제 설정하기",
|
||||
"event_setup_multiple_duration_error": "이벤트 설정: 복수 기간에는 최소 1개의 옵션이 필요합니다.",
|
||||
"event_setup_multiple_duration_default_error": "이벤트 설정: 유효한 기본 기간을 선택하십시오.",
|
||||
"event_setup_booking_limits_error": "예약 한도는 오름차순이어야 합니다. [일, 주, 월, 년]",
|
||||
@@ -1566,7 +1568,7 @@
|
||||
"route_to_the_right_person": "양식에 대한 답변을 바탕으로 올바른 당사자에게 전달",
|
||||
"form_description": "예약자를 라우팅할 양식 생성",
|
||||
"copy_link_to_form": "양식에 링크 복사",
|
||||
"theme": "테마",
|
||||
"theme": "예약 페이지 테마",
|
||||
"theme_applies_note": "공개 예약 페이지에만 적용됩니다",
|
||||
"app_theme": "대시보드 테마",
|
||||
"app_theme_applies_note": "이것은 로그인한 대시보드에만 적용됩니다",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "설치된 캘린더 없음",
|
||||
"no_calendar_installed_description": "아직 연결된 캘린더가 없습니다",
|
||||
"add_a_calendar": "캘린더 추가",
|
||||
"change_email_hint": "변경 사항을 적용하려면 로그아웃했다가 다시 로그인해야 할 수 있습니다",
|
||||
"change_email_hint": "변경 사항을 적용하려면 로그아웃 후 다시 로그인해야 할 수 있습니다.",
|
||||
"confirm_password_change_email": "이메일 주소 변경 전 비밀번호를 확인하세요",
|
||||
"seats": "시트",
|
||||
"every_app_published": "{{appName}} App Store에 게시된 모든 앱은 오픈 소스이며 피어 리뷰를 통해 철저한 테스트를 거쳤습니다. 그럼에도 불구하고 {{companyName}}는 {{appName}}에서 게시하지 않는 한 이 앱을 보증 또는 인증하지 않습니다. 부적절한 콘텐츠나 활동을 발견하면 신고해 주시기 바랍니다.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "앱 재설치",
|
||||
"reschedule_reason": "일정 변경 사유",
|
||||
"choose_common_schedule_team_event": "공통 일정 선택",
|
||||
"choose_common_schedule_team_event_description": "호스트 간에 공통 일정을 사용하려면 이 옵션을 활성화하세요. 비활성화되면 각 호스트는 기본 일정에 따라 예약됩니다.",
|
||||
"choose_common_schedule_team_event_description": "호스트 간에 공통 일정을 사용하려면 이 옵션을 활성화하십시오. 비활성화하면 각 호스트는 기본 일정 또는 선택한 일정에 따라 예약됩니다.",
|
||||
"reason": "사유",
|
||||
"sender_id": "발신자 ID",
|
||||
"sender_id_error_message": "영문, 숫자, 공백만 허용(최대 11자)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "잘못된 인증 코드",
|
||||
"not_verified": "아직 인증되지 않음",
|
||||
"no_availability_in_month": "{{month}}에 이용 불가",
|
||||
"view_previous_month": "이전 달 보기",
|
||||
"view_next_month": "다음 달 보기",
|
||||
"send_code": "코드 전송",
|
||||
"number_verified": "번호 인증됨",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "새 옵션",
|
||||
"update_profile": "회원 정보 업데이트",
|
||||
"attribute_updated_successfully": "속성이 성공적으로 업데이트되었습니다",
|
||||
"attribute_deleted_successfully": "속성이 성공적으로 삭제되었습니다",
|
||||
"attributes_edited_successfully": "속성이 성공적으로 편집되었습니다",
|
||||
"attribute_meta_description": "팀원의 속성을 관리하세요",
|
||||
"attributes_edit_description": "팀원의 속성을 편집하세요",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "이번 달 현재까지",
|
||||
"year_to_date": "올해 현재까지",
|
||||
"custom_range": "사용자 지정 범위",
|
||||
"show_all_columns": "모든 열 표시",
|
||||
"toggle_columns": "열 전환",
|
||||
"no_columns_found": "열을 찾을 수 없습니다",
|
||||
"salesforce_create_record_as": "예약 시, 이벤트를 추가하고 새로운 참석자를 다음으로 추가합니다:",
|
||||
"salesforce_lead": "리드",
|
||||
"salesforce_contact_under_account": "계정 내 연락처",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "연락처가 계정에 존재하지 않으면, 참석자로부터 새로운 리드를 생성합니다",
|
||||
"salesforce_create_new_contact_under_account": "참석자의 이메일 도메인과 기존 연락처를 기반으로 계정 내에 새로운 연락처를 생성합니다",
|
||||
"mass_assign_attributes": "속성 일괄 할당",
|
||||
"reroute_preview_custom_message": "사용자 지정 메시지를 표시하는 결과가 나타납니다. 응답을 변경하여 이벤트로 라우팅해 보세요",
|
||||
"reroute_preview_external_redirect": "{{externalUrl}}로 리디렉션되는 결과가 나타납니다. 응답을 변경하여 이벤트로 라우팅해 보세요",
|
||||
"reroute_preview_possible_host": "가능한 호스트",
|
||||
"current_routing_status": "현재 라우팅 상태",
|
||||
"reroute_booking": "예약 재라우팅",
|
||||
"reroute_booking_description": "예약을 다른 팀원에게 재라우팅합니다",
|
||||
"verify_new_route": "새 경로 확인",
|
||||
"reroute": "재라우팅",
|
||||
"new_routing_status": "새 라우팅 상태",
|
||||
"salesforce_book_directly_with_attendee_owner": "참석자가 Salesforce에 존재하면, 소유자와 직접 예약합니다",
|
||||
"salesforce_check_owner_of": "직접 예약하기 위해 소유자를 확인할 엔터티",
|
||||
"account": "계정",
|
||||
"choose_hosts_schedule": "호스트 일정 선택",
|
||||
"hosts_schedule_description": "각 호스트에 대해 다른 일정을 선택할 수 있습니다. 각 호스트의 기본 일정이 기본적으로 선택됩니다.",
|
||||
"no_hosts_description": "할당 탭에서 호스트를 추가하십시오. 현재 할당된 호스트가 없습니다.",
|
||||
"reschedule_with_same_timeslot": "동일한 시간대로 일정 변경",
|
||||
"reschedule_with_same_timeslot_of_new_event": "새 이벤트의 동일한 시간대로 일정 변경",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "다른 시간대의 새 이벤트로 일정 변경",
|
||||
"reschedule_with_different_timeslot": "다른 시간대로 일정 변경",
|
||||
"we_dont_have_id_for_the_new_event": "새 이벤트의 ID가 없습니다. 라우팅 양식으로 이동하여 다시 저장해 주세요.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "예약이 성공적으로 재라우팅되었습니다. 예약 페이지로 리디렉션 중입니다",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Sta deelnemers toe om u te boeken via dynamische groepsboekingen",
|
||||
"dynamic_booking": "Dynamische groepslinks",
|
||||
"allow_seo_indexing": "Geef zoekmachines toegang tot uw openbare content",
|
||||
"seo_indexing": "Sta SEO-indexering toe",
|
||||
"seo_indexing": "Indexering door zoekmachines toestaan",
|
||||
"allow_browser_notifications": "Browsermeldingen toestaan",
|
||||
"you_have_denied_notifications": "Je hebt meldingen geweigerd. Stel de toestemming opnieuw in via de browserinstellingen om deze in te schakelen.",
|
||||
"disable_browser_notifications": "Browsermeldingen uitschakelen",
|
||||
@@ -674,7 +674,7 @@
|
||||
"hide_book_a_team_member_description": "Verberg de knop 'Teamlid boeken' op uw openbare pagina's.",
|
||||
"danger_zone": "Gevarenzone",
|
||||
"account_deletion_cannot_be_undone": "Wees voorzichtig. Het verwijderen van een account kan niet ongedaan worden gemaakt.",
|
||||
"team_deletion_cannot_be_undone": "Wees voorzichtig. Het verwijderen van het team kan niet ongedaan worden gemaakt.",
|
||||
"team_deletion_cannot_be_undone": "Wees voorzichtig. Het verwijderen van een team kan niet ongedaan worden gemaakt.",
|
||||
"back": "Terug",
|
||||
"cancel": "Annuleren",
|
||||
"cancel_all_remaining": "Alle resterende annuleren",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Hoe vaak u geboekt kunt worden",
|
||||
"event_advanced_tab_description": "Agenda-instellingen en meer...",
|
||||
"event_advanced_tab_title": "Geavanceerd",
|
||||
"event_payments_tab_title": "Betalingen",
|
||||
"event_payments_tab_description": "Betalingen instellen voor evenement",
|
||||
"event_setup_multiple_duration_error": "Gebeurtenisconfiguratie: voor meerdere duren is minimaal 1 optie vereist.",
|
||||
"event_setup_multiple_duration_default_error": "Gebeurtenisconfiguratie: selecteer een geldige standaardduur.",
|
||||
"event_setup_booking_limits_error": "Reserveringslimieten moeten in oplopende volgorde zijn. [dag, week, maand, jaar]",
|
||||
@@ -1566,7 +1568,7 @@
|
||||
"route_to_the_right_person": "Route naar de juiste persoon op basis van de antwoorden op uw formulier",
|
||||
"form_description": "Maak uw formulier om naar een boeker door te verwijzen",
|
||||
"copy_link_to_form": "Link kopiëren naar formulier",
|
||||
"theme": "Thema",
|
||||
"theme": "Thema van de boekingspagina",
|
||||
"theme_applies_note": "Dit is alleen van toepassing op uw openbare boekingspagina's",
|
||||
"app_theme": "Dashboard-thema",
|
||||
"app_theme_applies_note": "Dit is alleen van toepassing op uw aangemelde dashboard",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Geen agenda geïnstalleerd",
|
||||
"no_calendar_installed_description": "U heeft nog geen agenda's gekoppeld",
|
||||
"add_a_calendar": "Voeg een agenda toe",
|
||||
"change_email_hint": "U moet zich mogelijk afmelden en weer aanmelden voordat de wijzigingen van kracht worden",
|
||||
"change_email_hint": "Mogelijk moet u uit- en weer inloggen om de wijziging te zien.",
|
||||
"confirm_password_change_email": "Bevestig uw wachtwoord voordat u uw e-mailadres wijzigt",
|
||||
"seats": "plaatsen",
|
||||
"every_app_published": "Elke app die in de App Store van {{appName}} wordt gepubliceerd is open source en grondig getest via collegiale beoordelingen. Desondanks onderschrijft of certificeert {{companyName}} deze apps niet, tenzij ze zijn gepubliceerd door {{appName}}. Meld het als u ongepaste inhoud of gedrag tegenkomt.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Herinstalleer de app",
|
||||
"reschedule_reason": "Reden voor opnieuw plannen",
|
||||
"choose_common_schedule_team_event": "Kies een gezamenlijke planning",
|
||||
"choose_common_schedule_team_event_description": "Schakel dit in als u een gemeenschappelijke planning tussen organisatoren wilt gebruiken. Indien uitgeschakeld, wordt elke organisator geboekt op basis van zijn standaardplanning.",
|
||||
"choose_common_schedule_team_event_description": "Schakel dit in als u een gemeenschappelijk schema tussen hosts wilt gebruiken. Wanneer dit is uitgeschakeld, wordt elke host geboekt op basis van hun standaardschema of hun gekozen schema.",
|
||||
"reason": "Reden",
|
||||
"sender_id": "Afzender-ID",
|
||||
"sender_id_error_message": "Alleen letters, cijfers en spaties toegestaan (max. 11 tekens)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Onjuiste verificatiecode",
|
||||
"not_verified": "Nog niet geverifieerd",
|
||||
"no_availability_in_month": "Geen beschikbaarheid in {{month}}",
|
||||
"view_previous_month": "Vorige maand bekijken",
|
||||
"view_next_month": "Volgende maand weergeven",
|
||||
"send_code": "Code verzenden",
|
||||
"number_verified": "Nummer geverifieerd",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Configuratie",
|
||||
"setup_description": "Cal.com-exemplaar configureren",
|
||||
"configure": "Configureren",
|
||||
"sso_configuration": "Eenmalige aanmelding",
|
||||
"sso_configuration": "Single sign-on",
|
||||
"sso_configuration_description": "SAML/OIDC SSO configureren en teamleden laten inloggen met behulp van een identiteitsprovider",
|
||||
"sso_configuration_description_orgs": "Configureer SAML/OIDC SSO en laat organisatieleden inloggen met een Identity Provider",
|
||||
"sso_oidc_heading": "SSO met OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Populairste gebeurtenissen",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Dit is uw overzicht van populaire evenementen voor uw team {{teamName}} van de afgelopen 30 dagen",
|
||||
"me": "Ik",
|
||||
"monthly_digest_email": "Maandelijks overzichts-e-mail",
|
||||
"monthly_digest_email": "Maandelijkse overzichtsmail",
|
||||
"monthly_digest_email_for_teams": "Maandelijks overzichts-e-mail voor teams",
|
||||
"verify_team_tooltip": "Verifieer uw team om het versturen van berichten naar deelnemers in te schakelen",
|
||||
"member_removed": "Lid verwijderd",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nieuwe optie",
|
||||
"update_profile": "Lid bijwerken",
|
||||
"attribute_updated_successfully": "Attribuut succesvol bijgewerkt",
|
||||
"attribute_deleted_successfully": "Attribuut succesvol verwijderd",
|
||||
"attributes_edited_successfully": "Attributen succesvol bewerkt",
|
||||
"attribute_meta_description": "Beheer attributen voor je teamleden",
|
||||
"attributes_edit_description": "Bewerk attributen voor je teamleden",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "maand tot nu toe",
|
||||
"year_to_date": "jaar tot nu toe",
|
||||
"custom_range": "aangepast bereik",
|
||||
"show_all_columns": "Toon alle kolommen",
|
||||
"toggle_columns": "Kolommen wisselen",
|
||||
"no_columns_found": "Geen kolommen gevonden",
|
||||
"salesforce_create_record_as": "Bij boeking, voeg evenementen toe en nieuwe deelnemers als:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Contact onder een account",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Als de contactpersoon niet onder een account bestaat, maak een nieuwe lead van de deelnemer",
|
||||
"salesforce_create_new_contact_under_account": "Maak een nieuw contact onder een account op basis van het e-maildomein van de deelnemer en bestaande contacten",
|
||||
"mass_assign_attributes": "Attribuuttoewijzing in bulk",
|
||||
"reroute_preview_custom_message": "Dit resulteert in het tonen van een aangepast bericht. Probeer het antwoord te wijzigen om naar een evenement te routeren",
|
||||
"reroute_preview_external_redirect": "Dit resulteert in een doorverwijzing naar {{externalUrl}}. Probeer het antwoord te wijzigen om naar een evenement te routeren",
|
||||
"reroute_preview_possible_host": "Mogelijke host",
|
||||
"current_routing_status": "Huidige routeringsstatus",
|
||||
"reroute_booking": "Boeking omleiden",
|
||||
"reroute_booking_description": "Leid de boeking om naar verschillende teamleden",
|
||||
"verify_new_route": "Nieuwe route verifiëren",
|
||||
"reroute": "Omleiden",
|
||||
"new_routing_status": "Nieuwe routeringsstatus",
|
||||
"salesforce_book_directly_with_attendee_owner": "Als de deelnemer in Salesforce bestaat, boek direct met de eigenaar",
|
||||
"salesforce_check_owner_of": "Entiteit om eigenaar van te controleren voor directe boeking",
|
||||
"account": "Account",
|
||||
"choose_hosts_schedule": "Kies hostschema's",
|
||||
"hosts_schedule_description": "U kunt een ander schema selecteren voor elke host. Standaard is het standaardschema van elke host geselecteerd.",
|
||||
"no_hosts_description": "Geen hosts toegewezen, voeg hosts toe in het toewijzingstabblad.",
|
||||
"reschedule_with_same_timeslot": "Opnieuw plannen met hetzelfde tijdslot",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Opnieuw plannen met hetzelfde tijdslot van het nieuwe evenement",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Opnieuw plannen naar het nieuwe evenement met een ander tijdslot",
|
||||
"reschedule_with_different_timeslot": "Opnieuw plannen met een ander tijdslot",
|
||||
"we_dont_have_id_for_the_new_event": "We hebben geen ID voor het nieuwe evenement. Ga naar het Routeringsformulier en sla het opnieuw op.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Boeking succesvol omgeleid. U wordt doorgestuurd naar de boekingspagina",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Tillat deltakere å booke deg gjennom dynamiske gruppebookinger",
|
||||
"dynamic_booking": "Dynamiske gruppelenker",
|
||||
"allow_seo_indexing": "Tillat søkemotorer å få tilgang til ditt offentlige innhold",
|
||||
"seo_indexing": "Tillat SEO-indeksering",
|
||||
"seo_indexing": "Tillat indeksering av søkemotorer",
|
||||
"allow_browser_notifications": "Tillat nettleservarsler",
|
||||
"you_have_denied_notifications": "Du har avslått varsler. Tilbakestill tillatelse i nettleserinnstillingene for å aktivere.",
|
||||
"disable_browser_notifications": "Deaktiver nettleservarsler",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Hvor ofte du kan bookes",
|
||||
"event_advanced_tab_description": "Kalender-innstillinger og mer...",
|
||||
"event_advanced_tab_title": "Avansert",
|
||||
"event_payments_tab_title": "Betalinger",
|
||||
"event_payments_tab_description": "Sett opp betalinger for arrangement",
|
||||
"event_setup_multiple_duration_error": "Hendelsesoppsett: Flere varigheter krever minst 1 alternativ.",
|
||||
"event_setup_multiple_duration_default_error": "Hendelsesoppsett: Velg en gyldig standard-varighet.",
|
||||
"event_setup_booking_limits_error": "Booking-grensene må være i stigende rekkefølge. [dag,uke,måned,år]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Rute til riktig person basert på svarene i skjemaet ditt",
|
||||
"form_description": "Lag skjemaet ditt for å videresende noen som booker",
|
||||
"copy_link_to_form": "Kopier lenken til skjemaet",
|
||||
"theme": "Tema",
|
||||
"theme": "Tema for bookingside",
|
||||
"theme_applies_note": "Dette gjelder kun for dine offentlige bookingsider",
|
||||
"app_theme": "Dashboard-tema",
|
||||
"app_theme": "Dashbord-tema",
|
||||
"app_theme_applies_note": "Dette gjelder kun ditt påloggede dashbord",
|
||||
"theme_system": "System standard",
|
||||
"add_a_team": "Legg til et team",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Ingen kalender installert",
|
||||
"no_calendar_installed_description": "Du har ikke koblet til noen av kalenderne dine ennå",
|
||||
"add_a_calendar": "Legg til en kalender",
|
||||
"change_email_hint": "Det kan hende du må logge ut og inn igjen for å se at endringer trer i kraft",
|
||||
"change_email_hint": "Du må kanskje logge ut og inn igjen for å se endringene tre i kraft.",
|
||||
"confirm_password_change_email": "Vennligst bekreft passordet ditt før du endrer e-postadressen din",
|
||||
"seats": "seter",
|
||||
"every_app_published": "Hver app publisert på {{appName}} App Butikken er åpen kildekode og grundig testet via fagfellevurderinger. Uansett, {{companyName}} godkjenner eller sertifiserer likevel ikke disse appene med mindre de er publisert av {{appName}}. Hvis du møter upassende innhold eller oppførsel, vennligst rapporter det.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Installer appen på nytt",
|
||||
"reschedule_reason": "Årsak for endring av tidspunkt",
|
||||
"choose_common_schedule_team_event": "Velg en felles timeplan",
|
||||
"choose_common_schedule_team_event_description": "Aktiver dette hvis du vil bruke en felles tidsplan mellom verter. Når den er deaktivert, vil hver vert bookes basert på deres standard tidsplan.",
|
||||
"choose_common_schedule_team_event_description": "Aktiver dette hvis du vil bruke en felles plan mellom verter. Når det er deaktivert, vil hver vert bli booket basert på sin standardplan eller sin valgte plan.",
|
||||
"reason": "Årsak",
|
||||
"sender_id": "Avsender ID",
|
||||
"sender_id_error_message": "Kun bokstaver, tall og mellomrom er tillatt (maks. 11 tegn)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Feil verifiseringskode",
|
||||
"not_verified": "Ikke verifisert ennå",
|
||||
"no_availability_in_month": "Ingen tilgjengelighet i {{month}}",
|
||||
"view_previous_month": "Vis forrige måned",
|
||||
"view_next_month": "Vis neste måned",
|
||||
"send_code": "Send kode",
|
||||
"number_verified": "Nummer Bekreftet",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Oppsett",
|
||||
"setup_description": "Sett opp Cal.com-instans",
|
||||
"configure": "Konfigurer",
|
||||
"sso_configuration": "Enkel Pålogging",
|
||||
"sso_configuration": "Single sign-on",
|
||||
"sso_configuration_description": "Konfigurer SAML/OIDC SSO og la teammedlemmer logge inn ved hjelp av en identitetsleverandør",
|
||||
"sso_configuration_description_orgs": "Konfigurer SAML/OIDC SSO og la organisasjonsmedlemmer logge inn ved hjelp av en identitetsleverandør",
|
||||
"sso_oidc_heading": "SSO med OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Mest populære arrangementer",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Her er din oppsummering av populære arrangementer for teamet ditt {{teamName}} de siste 30 dagene",
|
||||
"me": "Meg",
|
||||
"monthly_digest_email": "Månedlig oppsummerings-e-post",
|
||||
"monthly_digest_email": "Månedlig sammendrag på e-post",
|
||||
"monthly_digest_email_for_teams": "Månedlig oppsummerings-e-post for team",
|
||||
"verify_team_tooltip": "Verifiser teamet ditt for å aktivere sending av meldinger til deltakere",
|
||||
"member_removed": "Medlem fjernet",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nytt alternativ",
|
||||
"update_profile": "Oppdater medlem",
|
||||
"attribute_updated_successfully": "Egenskap oppdatert vellykket",
|
||||
"attribute_deleted_successfully": "Attributet ble slettet",
|
||||
"attributes_edited_successfully": "Egenskaper redigert vellykket",
|
||||
"attribute_meta_description": "Administrer egenskaper for teammedlemmene dine",
|
||||
"attributes_edit_description": "Rediger egenskaper for teammedlemmene dine",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "måned til dags dato",
|
||||
"year_to_date": "år til dags dato",
|
||||
"custom_range": "tilpasset periode",
|
||||
"show_all_columns": "Vis alle kolonner",
|
||||
"toggle_columns": "Veksle kolonner",
|
||||
"no_columns_found": "Ingen kolonner funnet",
|
||||
"salesforce_create_record_as": "Ved booking, legg til hendelser og nye deltakere som:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Kontakt under en konto",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Hvis kontakten ikke finnes under en konto, opprett ny lead fra deltaker",
|
||||
"salesforce_create_new_contact_under_account": "Opprett en ny kontakt under en konto basert på e-postdomenet til deltaker og eksisterende kontakter",
|
||||
"mass_assign_attributes": "Masseopptildel attributter",
|
||||
"reroute_preview_custom_message": "Det resulterer i å vise en tilpasset melding. Prøv å endre svaret for å rute til en hendelse",
|
||||
"reroute_preview_external_redirect": "Det resulterer i omdirigering til {{externalUrl}}. Prøv å endre svaret for å rute til en hendelse",
|
||||
"reroute_preview_possible_host": "Mulig vert",
|
||||
"current_routing_status": "Nåværende rutingsstatus",
|
||||
"reroute_booking": "Omdirigér booking",
|
||||
"reroute_booking_description": "Omdirigér bookingen til forskjellige teammedlemmer",
|
||||
"verify_new_route": "Bekreft ny rute",
|
||||
"reroute": "Omdirigér",
|
||||
"new_routing_status": "Ny rutingsstatus",
|
||||
"salesforce_book_directly_with_attendee_owner": "Hvis deltaker finnes i Salesforce, book direkte med eieren",
|
||||
"salesforce_check_owner_of": "Enhet for å sjekke eier av for å booke direkte",
|
||||
"account": "Konto",
|
||||
"choose_hosts_schedule": "Velg vertsplaner",
|
||||
"hosts_schedule_description": "Du kan velge en annen plan for hver vert. Hver verts standardplan er valgt som standard.",
|
||||
"no_hosts_description": "Ingen verter tildelt, legg til verter i tildelingsfanen.",
|
||||
"reschedule_with_same_timeslot": "Endre tidspunkt med samme tidsrom",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Endre tidspunkt med samme tidsrom for den nye hendelsen",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Endre tidspunkt til den nye hendelsen med et annet tidsrom",
|
||||
"reschedule_with_different_timeslot": "Endre tidspunkt med et annet tidsrom",
|
||||
"we_dont_have_id_for_the_new_event": "Vi har ikke ID for den nye hendelsen. Gå til rutingsskjemaet og lagre det på nytt.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Bookingen ble omdirigert. Omdirigerer til bookingsiden",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"allow_dynamic_booking": "Pozwól uczestnikom rezerwować cię przez dynamiczne rezerwacje grupowe",
|
||||
"dynamic_booking": "Dynamiczne linki grup",
|
||||
"allow_seo_indexing": "Zezwól wyszukiwarkom na dostęp do Twoich publicznych treści",
|
||||
"seo_indexing": "Zezwalaj na indeksowanie SEO",
|
||||
"seo_indexing": "Zezwól na indeksowanie przez wyszukiwarki",
|
||||
"allow_browser_notifications": "Zezwól na powiadomienia przeglądarki",
|
||||
"you_have_denied_notifications": "Odrzuciłeś powiadomienia. Zresetuj uprawnienia w ustawieniach przeglądarki, aby je włączyć.",
|
||||
"disable_browser_notifications": "Wyłącz powiadomienia przeglądarki",
|
||||
@@ -673,8 +673,8 @@
|
||||
"hide_book_a_team_member": "Ukryj przycisk Zarezerwuj członka zespołu",
|
||||
"hide_book_a_team_member_description": "Ukryj przycisk Zarezerwuj członka zespołu na swoich stronach publicznych.",
|
||||
"danger_zone": "Strefa zagrożenia",
|
||||
"account_deletion_cannot_be_undone": "Uwaga. Usunięcie konta jest nieodwracalne.",
|
||||
"team_deletion_cannot_be_undone": "Uwaga. Usunięcie zespołu jest nieodwracalne.",
|
||||
"account_deletion_cannot_be_undone": "Uwaga. Usunięcia konta nie można cofnąć.",
|
||||
"team_deletion_cannot_be_undone": "Uwaga. Usunięcia zespołu nie można cofnąć.",
|
||||
"back": "Wróć",
|
||||
"cancel": "Anuluj",
|
||||
"cancel_all_remaining": "Anuluj wszystkie pozostałe",
|
||||
@@ -1387,6 +1387,8 @@
|
||||
"event_limit_tab_description": "Jak często można rezerwować Twoje usługi",
|
||||
"event_advanced_tab_description": "Ustawienia kalendarza i więcej...",
|
||||
"event_advanced_tab_title": "Zaawansowane",
|
||||
"event_payments_tab_title": "Płatności",
|
||||
"event_payments_tab_description": "Skonfiguruj płatności za wydarzenie",
|
||||
"event_setup_multiple_duration_error": "Konfiguracja wydarzeń: wiele czasów trwania wymaga co najmniej 1 opcji.",
|
||||
"event_setup_multiple_duration_default_error": "Konfiguracja wydarzenia: wybierz prawidłowy domyślny czas trwania.",
|
||||
"event_setup_booking_limits_error": "Limity rezerwacji muszą być podane w kolejności rosnącej. [dzień, tydzień, miesiąc, rok]",
|
||||
@@ -1566,9 +1568,9 @@
|
||||
"route_to_the_right_person": "Przekierowuj do właściwej osoby na podstawie odpowiedzi na Twój formularz",
|
||||
"form_description": "Utwórz formularz, aby przekierowywać osoby rezerwujące",
|
||||
"copy_link_to_form": "Skopiuj link do formularza",
|
||||
"theme": "Motyw",
|
||||
"theme": "Motyw strony rezerwacji",
|
||||
"theme_applies_note": "Dotyczy tylko publicznych stron rezerwacji",
|
||||
"app_theme": "Motyw pulpitu nawigacyjnego",
|
||||
"app_theme": "Motyw panelu kontrolnego",
|
||||
"app_theme_applies_note": "To dotyczy tylko pulpitu nawigacyjnego, do którego się zalogowałeś",
|
||||
"theme_system": "Ustawienia domyślne systemu",
|
||||
"add_a_team": "Dodaj zespół",
|
||||
@@ -1599,7 +1601,7 @@
|
||||
"no_calendar_installed": "Brak zainstalowanego kalendarza",
|
||||
"no_calendar_installed_description": "Nie połączono jeszcze żadnego kalendarza.",
|
||||
"add_a_calendar": "Dodaj kalendarz",
|
||||
"change_email_hint": "Aby zmiany weszły w życie, konieczne może być wylogowanie się i ponowne zalogowanie",
|
||||
"change_email_hint": "Może być konieczne wylogowanie się i ponowne zalogowanie, aby zobaczyć efekt zmiany.",
|
||||
"confirm_password_change_email": "Potwierdź hasło przed zmianą adresu e-mail",
|
||||
"seats": "miejsca",
|
||||
"every_app_published": "Każda aplikacja opublikowana w sklepie App Store serwisu {{appName}} jest otwartym oprogramowaniem i jest dokładnie testowana przez innych użytkowników. Niemniej jednak, {{companyName}} nie odpowiada za aplikacje ani nie certyfikuje ich, chyba że są one publikowane przez {{appName}}. Jeśli natrafisz na nieodpowiednie treści lub zachowanie, zgłoś je.",
|
||||
@@ -1702,7 +1704,7 @@
|
||||
"invalid_credential_action": "Zainstaluj ponownie aplikację",
|
||||
"reschedule_reason": "Powód zmiany terminu",
|
||||
"choose_common_schedule_team_event": "Wybierz wspólny harmonogram",
|
||||
"choose_common_schedule_team_event_description": "Włącz tę opcję, jeśli chcesz używać wspólnego harmonogramu gospodarzy. Jeśli ją wyłączysz, rezerwacje u każdego gospodarza będą dokonywane na podstawie jego domyślnego harmonogramu.",
|
||||
"choose_common_schedule_team_event_description": "Włącz to, jeśli chcesz używać wspólnego harmonogramu między gospodarzami. Gdy jest wyłączone, każdy gospodarz będzie rezerwowany na podstawie swojego domyślnego harmonogramu lub wybranego harmonogramu.",
|
||||
"reason": "Powód",
|
||||
"sender_id": "Identyfikator nadawcy",
|
||||
"sender_id_error_message": "Dozwolone są tylko litery, cyfry i spacje (maksymalnie 11 znaków)",
|
||||
@@ -1797,6 +1799,7 @@
|
||||
"wrong_code": "Nieprawidłowy kod weryfikacyjny",
|
||||
"not_verified": "Jeszcze nie zweryfikowano",
|
||||
"no_availability_in_month": "Brak dostępności w danym miesiącu ({{month}})",
|
||||
"view_previous_month": "Zobacz poprzedni miesiąc",
|
||||
"view_next_month": "Wyświetl następny miesiąc",
|
||||
"send_code": "Wyślij kod",
|
||||
"number_verified": "Numer zweryfikowany",
|
||||
@@ -1848,7 +1851,7 @@
|
||||
"setup": "Konfiguracja",
|
||||
"setup_description": "Skonfiguruj wystąpienie Cal.com",
|
||||
"configure": "Konfiguruj",
|
||||
"sso_configuration": "Pojedyncze logowanie",
|
||||
"sso_configuration": "Konfiguracja pojedynczego logowania",
|
||||
"sso_configuration_description": "Skonfiguruj logowanie jednokrotne SAML/OIDC i zezwól członkom zespołu na logowanie się przy użyciu dostawcy tożsamości",
|
||||
"sso_configuration_description_orgs": "Skonfiguruj SAML/OIDC SSO i pozwól członkom organizacji logować się za pomocą dostawcy tożsamości",
|
||||
"sso_oidc_heading": "Logowanie jednokrotne za pomocą OIDC",
|
||||
@@ -2222,7 +2225,7 @@
|
||||
"most_popular_events": "Najpopularniejsze wydarzenia",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Oto podsumowanie popularnych wydarzeń dla Twojego zespołu {{teamName}} z ostatnich 30 dni",
|
||||
"me": "Ja",
|
||||
"monthly_digest_email": "E-mail z miesięcznym podsumowaniem",
|
||||
"monthly_digest_email": "Miesięczny e-mail z podsumowaniem",
|
||||
"monthly_digest_email_for_teams": "E-mail dla zespołów z miesięcznym podsumowaniem",
|
||||
"verify_team_tooltip": "Zweryfikuj zespół, aby włączyć wysyłanie wiadomości do uczestników",
|
||||
"member_removed": "Usunięto członka",
|
||||
@@ -2595,6 +2598,7 @@
|
||||
"new_option": "Nowa opcja",
|
||||
"update_profile": "Aktualizuj członka",
|
||||
"attribute_updated_successfully": "Atrybut zaktualizowany pomyślnie",
|
||||
"attribute_deleted_successfully": "Atrybut został pomyślnie usunięty",
|
||||
"attributes_edited_successfully": "Atrybuty edytowane pomyślnie",
|
||||
"attribute_meta_description": "Zarządzaj atrybutami członków zespołu",
|
||||
"attributes_edit_description": "Edytuj atrybuty członków zespołu",
|
||||
@@ -2640,6 +2644,9 @@
|
||||
"month_to_date": "od początku miesiąca",
|
||||
"year_to_date": "od początku roku",
|
||||
"custom_range": "zakres niestandardowy",
|
||||
"show_all_columns": "Pokaż wszystkie kolumny",
|
||||
"toggle_columns": "Przełącz kolumny",
|
||||
"no_columns_found": "Nie znaleziono kolumn",
|
||||
"salesforce_create_record_as": "Podczas rezerwacji dodaj wydarzenia i nowych uczestników jako:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Kontakt w ramach konta",
|
||||
@@ -2647,8 +2654,26 @@
|
||||
"salesforce_if_account_does_not_exist": "Jeśli kontakt nie istnieje w ramach konta, utwórz nowy lead z uczestnika",
|
||||
"salesforce_create_new_contact_under_account": "Utwórz nowy kontakt w ramach konta na podstawie domeny e-mail uczestnika i istniejących kontaktów",
|
||||
"mass_assign_attributes": "Masowe przypisywanie atrybutów",
|
||||
"reroute_preview_custom_message": "Skutkuje to wyświetleniem niestandardowej wiadomości. Spróbuj zmienić odpowiedź, aby przekierować do wydarzenia",
|
||||
"reroute_preview_external_redirect": "Skutkuje to przekierowaniem do {{externalUrl}}. Spróbuj zmienić odpowiedź, aby przekierować do wydarzenia",
|
||||
"reroute_preview_possible_host": "Możliwy gospodarz",
|
||||
"current_routing_status": "Aktualny status przekierowania",
|
||||
"reroute_booking": "Zmień trasę rezerwacji",
|
||||
"reroute_booking_description": "Przekieruj rezerwację do innych członków zespołu",
|
||||
"verify_new_route": "Zweryfikuj nową trasę",
|
||||
"reroute": "Zmień trasę",
|
||||
"new_routing_status": "Nowy status przekierowania",
|
||||
"salesforce_book_directly_with_attendee_owner": "Jeśli uczestnik istnieje w Salesforce, zarezerwuj bezpośrednio z właścicielem",
|
||||
"salesforce_check_owner_of": "Podmiot do sprawdzenia właściciela w celu bezpośredniej rezerwacji",
|
||||
"account": "Konto",
|
||||
"choose_hosts_schedule": "Wybierz harmonogramy gospodarzy",
|
||||
"hosts_schedule_description": "Możesz wybrać inny harmonogram dla każdego gospodarza. Domyślny harmonogram każdego gospodarza jest wybierany automatycznie.",
|
||||
"no_hosts_description": "Brak przypisanych gospodarzy, dodaj gospodarzy w zakładce przypisania.",
|
||||
"reschedule_with_same_timeslot": "Zmień termin na ten sam przedział czasowy",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Zmień termin na ten sam przedział czasowy nowego wydarzenia",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Zmień termin na nowe wydarzenie z innym przedziałem czasowym",
|
||||
"reschedule_with_different_timeslot": "Zmień termin na inny przedział czasowy",
|
||||
"we_dont_have_id_for_the_new_event": "Nie mamy ID dla nowego wydarzenia. Przejdź do Formularza Przekierowań i zapisz go ponownie.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Pomyślnie zmieniono trasę rezerwacji. Przekierowywanie do strony rezerwacji",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user