Compare commits

...
31 Commits
Author SHA1 Message Date
emrysal 2e6ad8be9a chore: release v5.6.17
Post release / release (push) Canceled after 0s
2025-09-10 23:17:01 +00:00
Calcom BotandLingo.dev 1aca18324d feat: update translations via @LingoDotDev (#23753)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-10 21:38:05 +05:30
Udit TakkarandPeer Richelsen a8a3e48465 feat: add web phone call for cal ai (#23527)
* feat: add web phone call for cal ai

* fix: web call

* refactor: improvements

* chore

* refactor: web call improvements

* chore: error message

* refactor: improvements

* fix: err

* refactor: feedback and improvements

* fix: type errors

* refactor: hasAvailable

* chore: use orgid

* fix: types

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-09-10 12:41:10 -03:00
Volnei Munhoz 2e96600529 Revert "fix(workflows): hide organizer email in workflow emails (#23525)" (#23739)
This reverts commit 7f200df1ac.
2025-09-10 11:49:38 +00:00
Hariom Balhara b9074aeb81 Better ioctopus type-safety (#23541) 2025-09-10 17:10:25 +05:30
Udit Takkar 13add03479 chore: update vitest (#23732) 2025-09-10 16:59:25 +05:30
Hariom Balhara bb2dcc51c6 Ignore requests from bot for headless router (#23733) 2025-09-10 16:59:04 +05:30
RomitandKartik Saini 8f82e78597 fix: loading skeleton is rendered even when no attributes filter is selected (#23656)
* fix

* fix tests

* fix tests

* test fidx

* chore

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-09-10 16:57:56 +05:30
Eunjae LeeandDevin AI 34d2a13080 fix: move admin permission check to server-side to prevent flash (#23517)
- Move permission validation from AdminLayoutAppDirClient.tsx (client) to layout.tsx (server)
- Add server-side redirect using next/navigation redirect() function
- Remove client-side useEffect permission check to eliminate flash
- Follow same pattern as organizations layout for consistent auth handling
- Users without admin role are redirected to /settings/my-account/profile before any content renders

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-10 11:15:37 +01:00
Eunjae LeeandDevin AI 6066ed206b fix: Fix booking count discrepancy between KPI cards and trends chart (#23619)
* fix: correct booking count in EventTrendsChart by restructuring SQL query

- Replace JOIN-based query with CTE approach to count distinct bookings
- Separate booking stats from guest stats to prevent duplicate counting
- Preserve no-show guest functionality with LEFT JOIN
- Ensure EventTrendsChart 'Created' sum matches BookingKPICards 'events_created'

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: use positional GROUP BY references to resolve PostgreSQL error

- Replace column expressions in GROUP BY with positional references (1, 2, 3)
- Fixes PostgreSQL error 42803 caused by parameter resolution in CTE queries
- Maintains the same query logic while avoiding column reference conflicts

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* address feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-10 11:13:29 +01:00
d30d2792cd fix: hide notes (#22061)
* fix: hide notes

* test: re-enable confirmHandler tests and add tests for hide notes fix

* fix: replace deprecated getDate with hardcoded dates for timezone safety

* fix: hide notes test

* refactor: move test from trpc to web package

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2025-09-10 10:51:41 +01:00
ba50268634 feat: support auto create agent (#23493)
* feat: support auto create agent

* feat: add prompts

* chore: add number to call

* fix typo

---------

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-09-10 08:42:39 +00:00
Benny Joo 4ca78f46df chore: bump platform libraries version (#23730) 2025-09-10 08:37:17 +00:00
Eunjae LeeandAnik Dhabal Babu 3a7ca0246c docs: add knowledge base against unnecessary Day.js usage (#23719)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-10 09:54:48 +02:00
Vansh GilhotraandAnik Dhabal Babu 2cc8b1c2af fix: Use db-deploy instead of db-migrate for yarn dx in non-interactive environments (#23253)
Fixes the issue where 'yarn dx' fails in non-interactive environments
(CI/CD, Docker, automated scripts) by changing Turbo dependencies:

- @calcom/prisma#db-seed: db-migrate → db-deploy
- @calcom/prisma#dx: db-migrate → db-up

This ensures that non-interactive 'prisma migrate deploy' is used
instead of interactive 'prisma migrate dev', allowing yarn dx to
work correctly in automated environments.

Resolves: Cal.com App Store performance optimization - yarn dx failure

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-10 06:46:46 +00:00
Lauris Skraucis a2d0cbfe5b chore: v2 dont allow mixing platform teams/users with non-platform (#20296)
* chore: v2 dont allow mixing platform teams/users with non-platform

* chore: finish main merge

* test: e2e

* refactor: simplify can add user to team logic

* refactor: fetch only needed oauth data

* refactor: cleanup
2025-09-09 21:24:18 +02:00
PallavandKartik Saini d65c0753ef conventional left aligned dropdown (#23643)
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-09-09 16:02:43 +00:00
github-actions[bot] 76bd90b228 chore: version packages (#23714)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-09 15:23:30 +00:00
Benny Joo d908403a99 refactor: circular deps between app store and lib [1] (#23653)
* move dailyApiFetcher.ts to app store package

* update imports

* bookingLocationService

* revert

* move findFieldValueByIdentifier

* wip

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* move to features package

* update imports

* update imports

* move

* fix typechecks

* move getCalendarLinks

* mv event test
2025-09-09 14:38:29 +00:00
Amit SharmaandUdit Takkar 0fe775962f fix: intercom showing on cal video page (#23713)
* feat: support auto create agent

* fix: intercom showing on cal.video page

* Revert "feat: support auto create agent"

This reverts commit 084319c419b3baa44a3a82c0a2d79a4242a64d4d.

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2025-09-09 14:28:59 +00:00
Calcom BotandLingo.dev 412a6992c7 feat: update translations via @LingoDotDev (#23715)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-09 11:04:39 -03:00
Rajiv Sahal cfd1992733 feat: add customReplyToEmail for EventTypeSettings atom (#23686)
* init: endpoint for fetching verified emails

* fix: enable custom reply to email in frontend

* init endpoint to add verified emails

* add verified emails option for platform in frontend

* fixup: move useGetVerifiedEmails hook to correct folder

* update atoms module

* fixup: teamId should be string

* add methond to fetch team member emails

* update logic to fetch and add emails

* fixup: append client id with email

* fixup: pass teamId for fetching verified emails

* fixup: simplify check for existing emails

* fix: cleanup comments

* fix: implement code rabbit feedback

* fix: add translations

* fixup: update transaltions

* fix: update logic for addVerifiedEmail

* add changesets

* fix: implement PR feedback
2025-09-09 16:52:28 +03:00
RomitandAnik Dhabal Babu e86d6169ba fix (#23705)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-09 19:05:17 +05:30
sean-brydonandcoderabbitai[bot] a379bd2b52 fix: org layout authentication (#23707)
* refactor layout to not check session

* add actions for orgs + org admins

* update types on actions to be correctly non nullable

* Add tests for utils

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* restore lock file

* rename _actions to actions

* update tests to be mocked

* restore lock

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-09 22:27:00 +09:00
Eunjae LeeandDevin AI 1137047606 feat: add PBAC permission checks to insights (#23592)
* feat: add PBAC permission checks to insights

- Add layout-level protection for insights pages with session.user.org.id check
- Add API-level protection for insights tRPC endpoints using insightsPbacProcedure
- Enhance routing form insights with PBAC team filtering
- Use insights.read permission with OWNER/ADMIN fallback roles
- Replace all userBelongsToTeamProcedure with insightsPbacProcedure in insights router
- Fix lint issues: remove unused variables and functions, fix non-null assertion

Implements Permission-Based Access Control (PBAC) for insights functionality following the same patterns as event type PBAC implementation from PR #22618.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: clean up unused imports in routing-forms

- Remove unused isFormCreateEditAllowed import
- Remove unused TRPCError import
- Change MembershipRole to type import for better tree-shaking

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* address feedback

* clean up permission check

* revert unnecessary changes

* fix procedure

* allow MEMBER for insights

* support teams not under orgs

* fix type error

* revert unnecessary changes

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-09 14:26:20 +01:00
Lauris Skraucis ad4cf0cc3a fix: v2 hidden event types (#23584)
* fix: return hidden event types only if owner auth provided

* chore: add tests

* chore: remove optional chaining when defined
2025-09-09 13:23:50 +02:00
RomitandKartik Saini e7d2e0dc2f fix: Prevent loading of dashboard GTM container on booking success page (#22620)
* moved bookings folder

* removed test

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-09-09 16:31:07 +05:30
Alex van Andel aa51218428 perf: move to disable prisma client extension inference (#23692)
* perf: move to disable prisma client extension inference

* Prisma doesn't like it when you pass Record<string, unknown>

* API v1 type fixes

* Missed one

* Fix unit test fail due to faulty expect

* Assigning to prisma InputJsonValue/Array must be done as object, not interface

* Fix @calcom/web ts error, teams not defined

* Run eslint formatter

* fixed the routingFormHelpers file causing a failing app store e2e test
2025-09-09 10:56:58 +00:00
Yadong (Adam) Zhang 38bc5fb71e chore(apps/web): add Scan for React performance issues and eliminate slow renders in web app (#23696)
* chore: add Scan for React performance issues and eliminate slow renders in web app

* chore(apps/web): always install latest `react-scan`.
2025-09-09 10:50:44 +00:00
411516ddb9 fix: duplicate event created in attendee calendar due to incorrect icalUid (#23658)
* chre: update reassigment to delete old events

* fix

* Update yarn.lock

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2025-09-09 10:29:14 +00:00
Morgan c6c17ac388 fix: delegated credentials timezone outlook (#23700) 2025-09-09 11:25:48 +01:00
255 changed files with 9300 additions and 1886 deletions
+9
View File
@@ -318,3 +318,12 @@ dates.map((date) => dayjs.utc(date).add(1, "day").format());
// ✅ Best - Use native Date when possible
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
```
## Avoid using Dayjs if you don’t need to be strictly tz aware.
When doing logic like Dayjs.startOf(".."), you can instead use date-fns' `startOfMonth(dateObj)` / `endOfDay(dateObj)`;
When doing logic that depends on Browser locale, use i18n.language (prefer to deconstruct) like: `const { i18n: { language } } = useLocale();`, in combination with built-in Intl.
Note that with Date, you’re dealing with System time, so it’s not suited to everywhere (such as in the Booker, where instead we’ll likely migrate to Temporal) - but in most cases the above are suitable.
The main reason for doing so is that Dayjs uses a useful, but highly risky plugin system, which has led us to create `@calcom/dayjs` - this is heavy however, because it pre-loads ALL plugins, including locale handling. It’s a non-ideal solution to a problem that unfortunately exists due to Dayjs.
View File
@@ -3,7 +3,8 @@ import type { NextApiRequest } from "next";
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { schemaCredentialPatchParams, schemaCredentialPatchBody } from "~/lib/validations/credential-sync";
@@ -71,7 +72,7 @@ async function handler(req: NextApiRequest) {
userId,
},
data: {
key,
key: key as unknown as Prisma.InputJsonValue,
},
select: {
id: true,
@@ -6,7 +6,8 @@ import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { schemaCredentialPostBody, schemaCredentialPostParams } from "~/lib/validations/credential-sync";
@@ -92,7 +93,7 @@ async function handler(req: NextApiRequest) {
data: {
userId,
appId: appSlug,
key,
key: key as unknown as Prisma.InputJsonValue,
type: appMetadata.type,
},
select: credentialForCalendarServiceSelect,
+1 -1
View File
@@ -77,7 +77,7 @@ describe("POST /api/attendees", () => {
const userBooking = { id: 1 };
prismaMock.booking.findFirst.mockResolvedValue(userBooking);
prismaMock.booking.findFirst.mockResolvedValue(userBooking as any);
const attendeeData = {
id: 1,
@@ -25,7 +25,7 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
name: "Test User",
});
} as any);
});
afterEach(() => {
@@ -20,7 +20,7 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
name: "Test User",
});
} as any);
});
afterEach(() => {
+3 -2
View File
@@ -30,12 +30,13 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
name: "Test User",
});
} as any);
(getAccessibleUsers as any).mockResolvedValue([userId]);
(retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([userId]);
prismaMock.membership.findMany.mockResolvedValue([
{
// @ts-expect-error Will be fixed by Prisma 6.7.0 upgrade mock changes - which uses vitest-mock-extended
team: {
id: 1,
isOrganization: true,
@@ -48,7 +49,7 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
},
]);
] as any);
});
afterEach(() => {
+12 -4
View File
@@ -11,9 +11,9 @@ import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBook
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import { CreationSource } from "@calcom/prisma/enums";
import { CreationSource, BookingStatus } from "@calcom/prisma/enums";
import handler from "../../../pages/api/bookings/_post";
@@ -303,6 +303,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -340,6 +341,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -376,6 +378,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -417,6 +420,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -468,7 +472,7 @@ describe("POST /api/bookings", () => {
prismaMock.booking.findUnique.mockResolvedValue({
...originalBooking,
status: "cancelled",
status: "CANCELLED",
});
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
@@ -492,6 +496,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -532,7 +537,7 @@ describe("POST /api/bookings", () => {
const previousBooking = await prisma.booking.findUnique({
where: { uid: "original-booking-uid" },
});
expect(previousBooking?.status).toBe("cancelled");
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
});
test("Creates source as api_v1", async () => {
@@ -556,6 +561,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -627,6 +633,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -684,6 +691,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -50,6 +50,7 @@ describe("DELETE /api/event-types/[id]", () => {
// Mocking team.findUnique
prismaMock.team.findUnique.mockResolvedValue({
id: teamId,
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
members: [
{ userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId },
{ userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId },
@@ -84,6 +84,7 @@ describe("GET /api/event-types/[id]", () => {
prismaMock.team.findFirst.mockResolvedValue({
id: teamId,
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
members: [
{
userId,
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.347",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.350",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -12,6 +12,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
@@ -88,11 +89,16 @@ describe("Event types Endpoints", () => {
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
let apiKeyOrgUser: string;
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
const name = `event-types-2024-06-14-user-${randomString()}`;
const username = name;
let eventType: EventTypeOutput_2024_06_14;
let hiddenEventType: EventTypeOutput_2024_06_14;
let user: User;
let orgUser: User;
let falseTestUser: User;
@@ -165,13 +171,10 @@ describe("Event types Endpoints", () => {
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
)
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
@@ -198,12 +201,20 @@ describe("Event types Endpoints", () => {
name,
username,
});
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `cal_test_${keyString}`;
orgUser = await userRepositoryFixture.create({
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
name: `event-types-2024-06-14-org-user-${randomString()}`,
username: `event-types-2024-06-14-org-user-${randomString()}`,
});
const { keyString: orgUserApiKeyString } = await apiKeysRepositoryFixture.createApiKey(
orgUser.id,
null
);
apiKeyOrgUser = `cal_test_${orgUserApiKeyString}`;
profileRepositoryFixture.create({
uid: `usr-${orgUser.id}`,
@@ -334,6 +345,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(404);
});
@@ -370,6 +382,7 @@ describe("Event types Endpoints", () => {
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(400);
@@ -395,6 +408,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(400);
});
@@ -504,6 +518,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201)
.then(async (response) => {
@@ -576,21 +591,52 @@ describe("Event types Endpoints", () => {
});
});
it("should create a hidden event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class hidden",
slug: "coding-class-hidden",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
hidden: true,
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.hidden).toEqual(body.hidden);
expect(createdEventType.ownerId).toEqual(user.id);
hiddenEventType = responseBody.data;
});
it(`/GET/event-types by username`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}`)
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
expect(responseBody.data?.length).toEqual(2);
const fetchedEventType = responseBody.data?.[0];
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
const fetchedHiddenEventType = responseBody.data?.find((et) => et.id === hiddenEventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
@@ -602,33 +648,149 @@ describe("Event types Endpoints", () => {
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType.customName).toEqual(eventType.customName);
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType?.customName).toEqual(eventType.customName);
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
eventType.requiresBookerEmailVerification
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType.color).toEqual(eventType.color);
expect(fetchedEventType?.color).toEqual(eventType.color);
expect(fetchedEventType?.hidden).toEqual(false);
expect(fetchedHiddenEventType?.id).toEqual(hiddenEventType.id);
expect(fetchedHiddenEventType?.hidden).toEqual(true);
});
it(`/GET/event-types by username should not return hidden event type if auth of non event type owner provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username should not return hidden event type if no auth provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username and eventSlug`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${eventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
expect(fetchedEventType?.description).toEqual(eventType.description);
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType?.customName).toEqual(eventType.customName);
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
eventType.requiresBookerEmailVerification
);
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType?.color).toEqual(eventType.color);
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username and eventSlug should not return hidden event type if auth of non event type owner provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(0);
});
it(`/GET/event-types by username and eventSlug should not return hidden event type if no auth provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(0);
});
it(`/GET/event-types by username and orgSlug`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -647,8 +809,7 @@ describe("Event types Endpoints", () => {
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -663,8 +824,7 @@ describe("Event types Endpoints", () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -705,6 +865,7 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(400);
});
@@ -732,6 +893,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201);
@@ -748,6 +910,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
seats: {
@@ -779,6 +942,7 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
@@ -799,6 +963,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
@@ -817,6 +982,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send({
seats: {
seatsPerTimeSlot: 4,
@@ -844,6 +1010,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201);
@@ -860,6 +1027,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
locations: [
@@ -897,6 +1065,7 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
@@ -918,6 +1087,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
@@ -935,6 +1105,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
confirmationPolicy: {
@@ -1034,6 +1205,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(200)
.then(async (response) => {
@@ -1135,6 +1307,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(404);
@@ -1152,6 +1325,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
@@ -1161,8 +1335,7 @@ describe("Event types Endpoints", () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
@@ -1203,8 +1376,7 @@ describe("Event types Endpoints", () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -1245,13 +1417,16 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.get(`/api/v2/event-types/1000`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(404);
});
it("should delete event type", async () => {
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
return request(app.getHttpServer())
.delete(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
});
afterAll(async () => {
@@ -6,11 +6,22 @@ import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import {
API_KEY_OR_ACCESS_TOKEN_HEADER,
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import {
AuthOptionalUser,
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
@@ -40,7 +51,6 @@ import {
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
@Controller({
@@ -62,7 +72,8 @@ export class EventTypesController_2024_06_14 {
constructor(
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14
) {}
@Post("/")
@@ -109,30 +120,23 @@ export class EventTypesController_2024_06_14 {
}
@Get("/")
@ApiOperation({ summary: "Get all event types" })
@ApiOperation({
summary: "Get all event types",
description:
"Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
})
@UseGuards(OptionalApiAuthGuard)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
async getEventTypes(
@Query() queryParams: GetEventTypesQuery_2024_06_14
@Query() queryParams: GetEventTypesQuery_2024_06_14,
@GetOptionalUser() authUser: AuthOptionalUser
): Promise<GetEventTypesOutput_2024_06_14> {
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
if (!eventTypes || eventTypes.length === 0) {
throw new NotFoundException(`Event types not found`);
}
const eventTypes = await this.eventTypesService.getEventTypes(queryParams, authUser);
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
return {
...eventType,
bookingFields: Array.isArray(eventType?.bookingFields)
? eventType?.bookingFields
.map((field) => {
if ("hidden" in field) {
return field.hidden !== true ? field : null;
}
return field;
})
.filter((f) => f)
: [],
};
}) as EventTypeOutput_2024_06_14[];
const eventTypesWithoutHiddenFields =
this.outputEventTypesService.getResponseEventTypesWithoutHiddenFields(eventTypesFormatted);
return {
status: SUCCESS_STATUS,
@@ -75,6 +75,16 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getUserEventTypesPublic(userId: number) {
return this.dbRead.prisma.eventType.findMany({
where: {
userId,
hidden: false,
},
include: { users: true, schedule: true, destinationCalendar: true },
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -3,6 +3,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
@@ -38,6 +39,7 @@ export class EventTypesService_2024_06_14 {
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { destinationCalendar: _destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
@@ -93,34 +95,47 @@ export class EventTypesService_2024_06_14 {
}
}
async getEventTypeByUsernameAndSlug(
username: string,
eventTypeSlug: string,
orgSlug?: string,
orgId?: number
) {
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
async getEventTypeByUsernameAndSlug(params: {
username: string;
eventTypeSlug: string;
orgSlug?: string;
orgId?: number;
authUser?: AuthOptionalUser;
}) {
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
if (!user) {
return null;
}
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, eventTypeSlug);
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, params.eventTypeSlug);
if (!eventType) {
return null;
}
if (eventType.hidden && params.authUser?.id !== user.id) {
return null;
}
return {
ownerId: user.id,
...eventType,
};
}
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
async getEventTypesByUsername(params: {
username: string;
orgSlug?: string;
orgId?: number;
authUser?: AuthOptionalUser;
}) {
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
if (!user) {
return [];
}
if (params.authUser?.id !== user.id) {
return await this.getUserEventTypesPublic(user.id);
}
return await this.getUserEventTypes(user.id);
}
@@ -172,6 +187,14 @@ export class EventTypesService_2024_06_14 {
});
}
async getUserEventTypesPublic(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId);
return eventTypes.map((eventType) => {
return { ownerId: userId, ...eventType };
});
}
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
const user = await this.usersRepository.findByUsername(username);
if (!user) {
@@ -181,15 +204,26 @@ export class EventTypesService_2024_06_14 {
return await getEventTypesPublic(user.id);
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
if (username && eventSlug) {
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
const eventType = await this.getEventTypeByUsernameAndSlug({
username,
eventTypeSlug: eventSlug,
orgSlug,
orgId,
authUser,
});
return eventType ? [eventType] : [];
}
if (username) {
return await this.getEventTypesByUsername(username, orgSlug, orgId);
return await this.getEventTypesByUsername({
username,
orgSlug,
orgId,
authUser,
});
}
if (usernames) {
@@ -31,6 +31,7 @@ import {
EventTypeOutput_2024_06_14,
OutputUnknownLocation_2024_06_14,
OutputUnknownBookingField_2024_06_14,
OutputBookingField_2024_06_14,
} from "@calcom/platform-types";
type EventTypeRelations = {
@@ -253,7 +254,7 @@ export class OutputEventTypesService_2024_06_14 {
type: "unknown",
slug: "unknown",
bookingField: JSON.stringify(bookingField),
});
} satisfies OutputUnknownBookingField_2024_06_14);
}
}
@@ -344,4 +345,26 @@ export class OutputEventTypesService_2024_06_14 {
seatsShowAvailabilityCount: !!seatsShowAvailabilityCount,
});
}
getResponseEventTypesWithoutHiddenFields(
eventTypes: EventTypeOutput_2024_06_14[]
): EventTypeOutput_2024_06_14[] {
return eventTypes.map((eventType) => this.getResponseEventTypeWithoutHiddenFields(eventType));
}
getResponseEventTypeWithoutHiddenFields(eventType: EventTypeOutput_2024_06_14): EventTypeOutput_2024_06_14 {
if (!Array.isArray(eventType?.bookingFields) || eventType.bookingFields.length === 0) return eventType;
const visibleBookingFields: OutputBookingField_2024_06_14[] = [];
for (const bookingField of eventType.bookingFields) {
if ("hidden" in bookingField && bookingField.hidden === true) {
continue;
}
visibleBookingFields.push(bookingField);
}
return {
...eventType,
bookingFields: visibleBookingFields,
};
}
}
@@ -17,6 +17,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/inde
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisService } from "@/modules/redis/redis.service";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@@ -36,6 +37,7 @@ import { Module } from "@nestjs/common";
SchedulesAtomsService,
VerificationAtomsService,
RedisService,
TeamsRepository,
],
exports: [EventTypesAtomService],
controllers: [
@@ -64,4 +64,47 @@ export class AtomsRepository {
return userTeams;
}
async getSecondaryEmails(userId: number) {
return await this.dbRead.prisma.secondaryEmail.findMany({
where: {
userId,
emailVerified: {
not: null,
},
},
});
}
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
userId_email: { userId, email },
},
});
return existingSecondaryEmailRecord?.email;
}
async getExistingSecondaryEmail(email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
email,
},
});
return existingSecondaryEmailRecord?.email;
}
async addSecondaryEmail(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
data: {
userId,
email,
emailVerified: new Date(),
},
});
return existingSecondaryEmailRecord?.email;
}
}
@@ -1,8 +1,11 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { Throttle } from "@/lib/endpoint-throttler-decorator";
import { AddVerifiedEmailInput } from "@/modules/atoms/inputs/add-verified-email.input";
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
import { GetVerifiedEmailsParams } from "@/modules/atoms/inputs/get-verified-emails-params";
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
import { GetVerifiedEmailsOutput } from "@/modules/atoms/outputs/get-verified-emails-output";
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
@@ -105,4 +108,44 @@ export class AtomsVerificationController {
status: SUCCESS_STATUS,
};
}
@Get("/emails/verified-emails")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async getVerifiedEmails(
@Query() query: GetVerifiedEmailsParams,
@GetUser() user: UserWithProfile
): Promise<GetVerifiedEmailsOutput> {
const verifiedEmails = await this.verificationService.getVerifiedEmails({
userId: user.id,
userEmail: user.email,
teamId: query.teamId ? Number(query.teamId) : undefined,
});
return {
data: verifiedEmails,
status: SUCCESS_STATUS,
};
}
@Post("/emails/verified-emails")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async addVerifiedEmails(
@Body() body: AddVerifiedEmailInput,
@GetUser() user: UserWithProfile
): Promise<ApiResponse<{ emailVerified: boolean }>> {
const emailVerified = await this.verificationService.addVerifiedEmail({
userId: user.id,
existingPrimaryEmail: user.email,
email: body.email,
});
return {
data: { emailVerified },
status: SUCCESS_STATUS,
};
}
}
@@ -0,0 +1,8 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail } from "class-validator";
export class AddVerifiedEmailInput {
@ApiProperty({ example: "user@example.com" })
@IsEmail()
email!: string;
}
@@ -0,0 +1,25 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNumber, IsOptional, IsString, IsEmail } from "class-validator";
export class GetVerifiedEmailsParams {
@ApiPropertyOptional({ example: "12345" })
@IsOptional()
@IsString()
teamId?: string;
}
export class GetVerifiedEmailsInput {
@ApiProperty()
@IsNumber()
userId!: number;
@ApiProperty()
@IsEmail()
@IsString()
userEmail!: string;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
teamId?: number;
}
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { IsString, IsArray } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class GetVerifiedEmailsOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsString()
@Expose()
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
readonly data!: string[];
}
@@ -1,6 +1,9 @@
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
@@ -13,6 +16,11 @@ import {
@Injectable()
export class VerificationAtomsService {
constructor(
private readonly atomsRepository: AtomsRepository,
private readonly teamsRepository: TeamsRepository
) {}
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
return await checkEmailVerificationRequired(input);
}
@@ -61,4 +69,78 @@ export class VerificationAtomsService {
isVerifyingEmail: input.isVerifyingEmail,
});
}
async getVerifiedEmails(input: GetVerifiedEmailsInput): Promise<string[]> {
const { userId, userEmail, teamId } = input;
const userEmailWithoutOauthClientId = this.removeClientIdFromEmail(userEmail);
if (teamId) {
const verifiedEmails: string[] = [];
const teamMembers = await this.teamsRepository.getTeamMemberEmails(teamId);
if (teamMembers.length === 0) {
return verifiedEmails;
}
teamMembers.forEach((member) => {
const memberEmailWithoutOauthClientId = this.removeClientIdFromEmail(member.email);
verifiedEmails.push(memberEmailWithoutOauthClientId);
member.secondaryEmails.forEach((secondaryEmail) => {
verifiedEmails.push(this.removeClientIdFromEmail(secondaryEmail.email));
});
});
return verifiedEmails;
}
let verifiedEmails = [userEmailWithoutOauthClientId];
const secondaryEmails = await this.atomsRepository.getSecondaryEmails(userId);
verifiedEmails = verifiedEmails.concat(
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
);
return verifiedEmails;
}
async addVerifiedEmail({
userId,
existingPrimaryEmail,
email,
}: {
userId: number;
existingPrimaryEmail: string;
email: string;
}): Promise<boolean> {
const existingSecondaryEmail = await this.atomsRepository.getExistingSecondaryEmailByUserAndEmail(
userId,
email
);
const alreadyExistingEmail = await this.atomsRepository.getExistingSecondaryEmail(email);
if (alreadyExistingEmail) {
throw new BadRequestException("Email already exists");
}
if (existingPrimaryEmail === email || existingSecondaryEmail === email) {
return true;
}
await this.atomsRepository.addSecondaryEmail(userId, email);
return true;
}
removeClientIdFromEmail(email: string): string {
const [localPart, domain] = email.split("@");
const localPartSegments = localPart.split("+");
localPartSegments.pop();
const baseEmail = localPartSegments.join("+");
const normalizedEmail = `${baseEmail}@${domain}`;
return normalizedEmail;
}
}
@@ -119,6 +119,15 @@ export class OAuthClientRepository {
});
}
async getByOrgId(organizationId: number) {
return this.dbRead.prisma.platformOAuthClient.findMany({
where: {
organizationId,
},
select: { id: true },
});
}
async getByEventTypeHosts(eventTypeId: number) {
const hostWithUserPlatformClient = await this.dbRead.prisma.host.findFirst({
select: {
@@ -1,17 +1,23 @@
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR = `Can't add user to organization - the user is platform managed user but organization is not because organization probably was not created using OAuth credentials.`;
export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR = `Can't add user to organization - the user is not platform managed user but organization is platform managed. Both have to be created using OAuth credentials.`;
export const MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to organization - managed user and organization were created using different OAuth clients.`;
@Injectable()
export class OrganizationsMembershipService {
constructor(
private readonly organizationsMembershipRepository: OrganizationsMembershipRepository,
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService,
private readonly oAuthClientsRepository: OAuthClientRepository
) {}
async getOrgMembership(organizationId: number, membershipId: number) {
@@ -95,7 +101,33 @@ export class OrganizationsMembershipService {
}
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
await this.canUserBeAddedToOrg(data.userId, organizationId);
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
}
async canUserBeAddedToOrg(userId: number, orgId: number) {
const [userOAuthClient, orgOAuthClients] = await Promise.all([
this.oAuthClientsRepository.getByUserId(userId),
this.oAuthClientsRepository.getByOrgId(orgId),
]);
if (!userOAuthClient && orgOAuthClients.length === 0) {
return true;
}
if (userOAuthClient && orgOAuthClients.some((orgClient) => orgClient.id === userOAuthClient.id)) {
return true;
}
if (!userOAuthClient) {
throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR);
}
if (orgOAuthClients.length === 0) {
throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR);
}
throw new BadRequestException(MANAGED_USER_AND_MANAGED_ORG_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR);
}
}
@@ -13,7 +13,8 @@ import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.ser
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { EmailModule } from "@/modules/email/email.module";
import { EmailService } from "@/modules/email/email.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
import { OrganizationsAttributesController } from "@/modules/organizations/attributes/index/controllers/organizations-attributes.controller";
@@ -65,6 +66,8 @@ import { RedisModule } from "@/modules/redis/redis.module";
import { RedisService } from "@/modules/redis/redis.service";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
@@ -93,6 +96,7 @@ import { Module } from "@nestjs/common";
OrganizationsOrganizationsModule,
OrganizationsStripeModule,
OrganizationsTeamsRoutingFormsModule,
MembershipsModule,
OrganizationsConferencingModule,
EventTypesPrivateLinksModule,
],
@@ -101,7 +105,6 @@ import { Module } from "@nestjs/common";
OrganizationsTeamsRepository,
OrganizationsService,
OrganizationsTeamsService,
MembershipsRepository,
OrganizationsSchedulesService,
OrganizationsUsersRepository,
OrganizationsUsersService,
@@ -147,6 +150,9 @@ import { Module } from "@nestjs/common";
TeamsSchedulesService,
SchedulesService_2024_06_11,
InputSchedulesService_2024_06_11,
TeamsMembershipsService,
TeamsMembershipsRepository,
OAuthClientRepository,
],
exports: [
OrganizationsService,
@@ -0,0 +1,470 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { EmailService } from "@/modules/email/email.service";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import {
PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR,
REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR,
} from "@/modules/organizations/memberships/services/organizations-membership.service";
import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input";
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import {
PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR,
REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR,
PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR,
} from "@/modules/teams/memberships/services/teams-memberships.service";
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, User } from "@prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiSuccessResponse } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
describe("Organizations Teams Memberships Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let org: Team;
let orgOwner: User;
let orgUser: User;
let orgTeam: Team;
let orgApiKey: string;
let platformOrg: Team;
let platformOrgOwner: User;
let platformOrgUser: CreateManagedUserData;
let platformOrgTeam: Team;
let platformOAuthClient: PlatformOAuthClient;
let secondPlatformOAuthClient: PlatformOAuthClient;
let secondPlatformOrgUser: CreateManagedUserData;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule, TeamsMembershipsModule],
}).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
const orgName = `organizations-teams-memberships-organization-${randomString()}`;
org = await organizationsRepositoryFixture.create({
name: orgName,
slug: orgName,
isOrganization: true,
});
const platformOrgName = `organizations-teams-memberships-platform-organization-${randomString()}`;
platformOrg = await organizationsRepositoryFixture.create({
isPlatform: true,
name: platformOrgName,
slug: platformOrgName,
isOrganization: true,
platformBilling: {
create: {
customerId: "cus_999",
plan: "ESSENTIALS",
subscriptionId: "sub_999",
},
},
});
platformOAuthClient = await createOAuthClient(platformOrg.id);
secondPlatformOAuthClient = await createOAuthClient(platformOrg.id);
orgOwner = await userRepositoryFixture.create({
email: `organizations-teams-memberships-org-owner-${randomString()}@api.com`,
username: `organizations-teams-memberships-org-owner-${randomString()}`,
});
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
const { keyString } = await apiKeysRepositoryFixture.createApiKey(orgOwner.id, null);
orgApiKey = `cal_test_${keyString}`;
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: orgOwner.id } },
team: { connect: { id: org.id } },
accepted: true,
});
platformOrgOwner = await userRepositoryFixture.create({
email: `platform-org-owner-${randomString()}@api.com`,
username: `platform-org-owner-${randomString()}`,
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: platformOrgOwner.id } },
team: { connect: { id: platformOrg.id } },
accepted: true,
});
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
username: `platform-org-owner-${randomString()}`,
organization: { connect: { id: platformOrg.id } },
user: {
connect: { id: platformOrgOwner.id },
},
});
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd-2",
username: `org-owner-${randomString()}`,
organization: { connect: { id: org.id } },
user: {
connect: { id: orgOwner.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:4321"],
permissions: 1023,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
describe("should create org users", () => {
it("should create a new org user", async () => {
const newOrgUser: CreateUserInput = {
email: `organization-user-${randomString()}@api.com`,
};
jest
.spyOn(EmailService.prototype, "sendSignupToOrganizationEmail")
.mockImplementation(() => Promise.resolve());
const { body } = await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users`)
.send(newOrgUser)
.set("Content-Type", "application/json")
.set("Authorization", `Bearer ${orgApiKey}`)
.set("Accept", "application/json");
const userData = body.data;
expect(body.status).toBe(SUCCESS_STATUS);
expect(userData.email).toBe(newOrgUser.email);
orgUser = userData;
});
it(`should create a new platform org manager user using first oAuth client`, async () => {
const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`;
const requestBody: CreateManagedUserInput = {
email: managedUserEmail,
timeZone: "Europe/Berlin",
weekStart: "Monday",
timeFormat: 24,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
bio: "I am a bio",
metadata: {
key: "value",
},
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${platformOAuthClient.id}/users`)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
expect(responseBody.data.user.name).toEqual(requestBody.name);
await userConnectedToOAuth(platformOAuthClient.id, responseBody.data.user.email, 1);
platformOrgUser = responseBody.data;
});
it(`should create a new platform org manager user using second oAuth client`, async () => {
const managedUserEmail = `platform-organization-manager-user-${randomString()}@api.com`;
const requestBody: CreateManagedUserInput = {
email: managedUserEmail,
timeZone: "Europe/Berlin",
weekStart: "Monday",
timeFormat: 24,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
bio: "I am a bio",
metadata: {
key: "value",
},
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${secondPlatformOAuthClient.id}/users`)
.set("x-cal-secret-key", secondPlatformOAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
expect(responseBody.data.user.name).toEqual(requestBody.name);
await userConnectedToOAuth(secondPlatformOAuthClient.id, responseBody.data.user.email, 1);
secondPlatformOrgUser = responseBody.data;
});
});
describe("should create org teams", () => {
it("should create the team for org", async () => {
const teamName = `organization team ${randomString()}`;
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.set("Authorization", `Bearer ${orgApiKey}`)
.send({
name: teamName,
} satisfies CreateOrgTeamDto)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
orgTeam = responseBody.data;
expect(orgTeam.name).toEqual(teamName);
expect(orgTeam.parentId).toEqual(org.id);
});
});
it("should create the team for platform org", async () => {
const teamName = `platform organization team ${randomString()}`;
return request(app.getHttpServer())
.post(`/v2/organizations/${platformOrg.id}/teams`)
.set("x-cal-client-id", platformOAuthClient.id)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send({
name: teamName,
} satisfies CreateOrgTeamDto)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
platformOrgTeam = responseBody.data;
expect(platformOrgTeam.name).toEqual(teamName);
expect(platformOrgTeam.parentId).toEqual(platformOrg.id);
});
});
});
describe("organization memberships", () => {
describe("negative tests", () => {
it("should not add managed user to organization", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/memberships`)
.set("Authorization", `Bearer ${orgApiKey}`)
.send({
userId: platformOrgUser.user.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(400);
expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR);
});
it("should not add managed user to organization team", async () => {
await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
.set("Authorization", `Bearer ${orgApiKey}`)
.send({
userId: platformOrgUser.user.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(422);
});
it("should not add managed user to organization team", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/teams/${orgTeam.id}/memberships`)
.set("Authorization", `Bearer ${orgApiKey}`)
.send({
userId: platformOrgUser.user.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(400);
expect(response.body.error.message).toEqual(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR);
});
});
describe("positive tests", () => {
it("should add user to organization", async () => {
await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/memberships`)
.set("Authorization", `Bearer ${orgApiKey}`)
.send({
userId: orgUser.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(201);
});
it("should add user to organization team", async () => {
await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
.set("Authorization", `Bearer ${orgApiKey}`)
.send({
userId: orgUser.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(201);
});
});
});
describe("platform organization memberships", () => {
describe("negative tests", () => {
it("should not add user to platform organization", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/organizations/${platformOrg.id}/memberships`)
.set("x-cal-client-id", platformOAuthClient.id)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send({
userId: orgUser.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(400);
expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR);
});
it("should not add user to platform organization team", async () => {
await request(app.getHttpServer())
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
.set("x-cal-client-id", platformOAuthClient.id)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send({
userId: orgUser.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(422);
});
it("should not add user to platform organization team", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/teams/${platformOrgTeam.id}/memberships`)
.set("x-cal-client-id", platformOAuthClient.id)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send({
userId: orgUser.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(400);
expect(response.body.error.message).toEqual(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR);
});
it("should not add user to platform organization team because user is created using different oAuth client", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
.set("x-cal-client-id", platformOAuthClient.id)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send({
userId: secondPlatformOrgUser.user.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(400);
expect(response.body.error.message).toEqual(
PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR
);
});
});
describe("positive tests", () => {
it("should add user to organization", async () => {
await request(app.getHttpServer())
.post(`/v2/organizations/${platformOrg.id}/memberships`)
.set("x-cal-client-id", platformOAuthClient.id)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send({
userId: platformOrgUser.user.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(201);
});
it("should add user to organization team", async () => {
await request(app.getHttpServer())
.post(`/v2/organizations/${platformOrg.id}/teams/${platformOrgTeam.id}/memberships`)
.set("x-cal-client-id", platformOAuthClient.id)
.set("x-cal-secret-key", platformOAuthClient.secret)
.send({
userId: platformOrgUser.user.id,
accepted: true,
role: "MEMBER",
} satisfies CreateOrgTeamMembershipDto)
.expect(201);
});
});
});
async function userConnectedToOAuth(oAuthClientId: string, userEmail: string, usersCount: number) {
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId);
const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail);
expect(oAuthUsers?.length).toEqual(usersCount);
expect(newOAuthUser?.email).toEqual(userEmail);
}
afterAll(async () => {
await organizationsRepositoryFixture.delete(org.id);
await teamsRepositoryFixture.delete(orgTeam.id);
await organizationsRepositoryFixture.delete(platformOrg.id);
await teamsRepositoryFixture.delete(platformOrgTeam.id);
await userRepositoryFixture.deleteByEmail(platformOrgUser.user.email);
await userRepositoryFixture.deleteByEmail(orgUser.email);
await userRepositoryFixture.deleteByEmail(orgOwner.email);
await userRepositoryFixture.deleteByEmail(platformOrgOwner.email);
await userRepositoryFixture.deleteByEmail(secondPlatformOrgUser.user.email);
await app.close();
});
});
@@ -1,6 +1,7 @@
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/create-organization-team-membership.input";
import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/memberships/inputs/update-organization-team-membership.input";
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
import { Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
@@ -8,10 +9,12 @@ import { TeamService } from "@calcom/platform-libraries";
@Injectable()
export class OrganizationsTeamsMembershipsService {
constructor(
private readonly organizationsTeamsMembershipsRepository: OrganizationsTeamsMembershipsRepository
private readonly organizationsTeamsMembershipsRepository: OrganizationsTeamsMembershipsRepository,
private readonly teamsMembershipsService: TeamsMembershipsService
) {}
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
await this.teamsMembershipsService.canUserBeAddedToTeam(data.userId, teamId);
const teamMembership = await this.organizationsTeamsMembershipsRepository.createOrgTeamMembership(
teamId,
data
@@ -1,15 +1,24 @@
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR = `Can't add user to team - the user is platform managed user but team is not because team probably was not created using OAuth credentials.`;
export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR = `Can't add user to team - the user is not platform managed user but team is platform managed. Both have to be created using OAuth credentials.`;
export const PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR = `Can't add user to team - managed user and team were created using different OAuth clients.`;
@Injectable()
export class TeamsMembershipsService {
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
constructor(
private readonly teamsMembershipsRepository: TeamsMembershipsRepository,
private readonly oAuthClientsRepository: OAuthClientRepository
) {}
async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) {
await this.canUserBeAddedToTeam(data.userId, teamId);
const teamMembership = await this.teamsMembershipsRepository.createTeamMembership(teamId, data);
return teamMembership;
}
@@ -54,4 +63,29 @@ export class TeamsMembershipsService {
return teamMembership;
}
async canUserBeAddedToTeam(userId: number, teamId: number) {
const [userOAuthClient, teamOAuthClient] = await Promise.all([
this.oAuthClientsRepository.getByUserId(userId),
this.oAuthClientsRepository.getByTeamId(teamId),
]);
if (!userOAuthClient && !teamOAuthClient) {
return true;
}
if (userOAuthClient && teamOAuthClient && userOAuthClient.id === teamOAuthClient.id) {
return true;
}
if (!teamOAuthClient) {
throw new BadRequestException(PLATFORM_USER_BEING_ADDED_TO_REGULAR_TEAM_ERROR);
}
if (!userOAuthClient) {
throw new BadRequestException(REGULAR_USER_BEING_ADDED_TO_PLATFORM_TEAM_ERROR);
}
throw new BadRequestException(PLATFORM_USER_AND_PLATFORM_TEAM_CREATED_WITH_DIFFERENT_OAUTH_CLIENTS_ERROR);
}
}
@@ -12,5 +12,6 @@ import { Module } from "@nestjs/common";
imports: [PrismaModule, RedisModule, OrganizationsModule, MembershipsModule, TeamsEventTypesModule],
providers: [TeamsMembershipsRepository, TeamsMembershipsService],
controllers: [TeamsMembershipsController],
exports: [TeamsMembershipsService],
})
export class TeamsMembershipsModule {}
@@ -120,4 +120,30 @@ export class TeamsRepository {
},
});
}
async getTeamMemberEmails(teamId: number) {
return this.dbRead.prisma.user.findMany({
where: {
teams: {
some: {
teamId,
},
},
},
select: {
id: true,
email: true,
secondaryEmails: {
where: {
emailVerified: {
not: null,
},
},
select: {
email: true,
},
},
},
});
}
}
+28
View File
@@ -9858,6 +9858,7 @@
"get": {
"operationId": "EventTypesController_2024_06_14_getEventTypes",
"summary": "Get all event types",
"description": "Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
"parameters": [
{
"name": "cal-api-version",
@@ -9913,6 +9914,33 @@
"schema": {
"type": "number"
}
},
{
"name": "Authorization",
"in": "header",
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
@@ -0,0 +1,34 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { hasInsightsPermission } from "@calcom/features/insights/server/hasInsightsPermission";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function checkInsightsPagePermission() {
const featuresRepository = new FeaturesRepository(prisma);
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
if (!insightsEnabled) {
redirect("/");
}
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.id) {
redirect("/auth/login");
}
const hasPermission = await hasInsightsPermission({
userId: session.user.id,
organizationId: session.user.org?.id,
});
if (!hasPermission) {
redirect("/");
}
return session;
}
@@ -1,34 +1,22 @@
import { getTranslate } from "app/_utils";
import { notFound } from "next/navigation";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import Shell from "@calcom/features/shell/Shell";
import { prisma } from "@calcom/prisma";
import UpgradeTipWrapper from "./UpgradeTipWrapper";
export default async function InsightsLayout({ children }: { children: React.ReactNode }) {
const featuresRepository = new FeaturesRepository(prisma);
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
if (!insightsEnabled) {
return notFound();
}
const t = await getTranslate();
return (
<>
<div>
<Shell
withoutMain={false}
heading={t("insights")}
subtitle={t("insights_subtitle")}
actions={<div className={`mb-2 flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
<UpgradeTipWrapper>{children}</UpgradeTipWrapper>
</Shell>
</div>
</>
<div>
<Shell
withoutMain={false}
heading={t("insights")}
subtitle={t("insights_subtitle")}
actions={<div className={`mb-2 flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
<UpgradeTipWrapper>{children}</UpgradeTipWrapper>
</Shell>
</div>
);
}
@@ -1,13 +1,11 @@
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import InsightsPage from "~/insights/insights-view";
import { checkInsightsPagePermission } from "./checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("insights"),
@@ -18,7 +16,7 @@ export const generateMetadata = async () =>
);
const ServerPage = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await checkInsightsPagePermission();
const { timeZone } = await prisma.user.findUniqueOrThrow({
where: { id: session?.user.id ?? -1 },
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import InsightsVirtualQueuesPage from "~/insights/insights-virtual-queues-view";
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("insights"),
@@ -12,5 +14,7 @@ export const generateMetadata = async () =>
);
export default async function Page() {
await checkInsightsPagePermission();
return <InsightsVirtualQueuesPage />;
}
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import InsightsRoutingPage from "~/insights/insights-routing-view";
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("insights"),
@@ -12,5 +14,7 @@ export const generateMetadata = async () =>
);
export default async function Page() {
await checkInsightsPagePermission();
return <InsightsRoutingPage />;
}
@@ -1,11 +1,11 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { usePathname } from "next/navigation";
import type { ComponentProps } from "react";
import React, { useEffect } from "react";
import React from "react";
import type Shell from "@calcom/features/shell/Shell";
import { UserPermissionRole } from "@calcom/prisma/enums";
import type { UserPermissionRole } from "@calcom/prisma/enums";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
export type AdminLayoutProps = {
@@ -14,14 +14,6 @@ export type AdminLayoutProps = {
} & ComponentProps<typeof Shell>;
export default function AdminLayoutAppDirClient({ userRole, children }: AdminLayoutProps) {
const pathname = usePathname();
const router = useRouter();
// Force redirect on component level
useEffect(() => {
if (userRole !== UserPermissionRole.ADMIN) {
router.replace("/settings/my-account/profile");
}
}, [userRole, router]);
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
return (
@@ -1,7 +1,9 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import React from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -15,5 +17,9 @@ export default async function AdminLayoutAppDir(props: AdminLayoutAppDirProps) {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const userRole = session?.user?.role;
if (userRole !== UserPermissionRole.ADMIN) {
return redirect("/settings/my-account/profile");
}
return await SettingsLayoutAppDir({ children: <AdminLayoutAppDirClient {...props} userRole={userRole} /> });
}
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
import { validateUserHasOrgAdmin } from "../../../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("attribute"),
@@ -12,6 +14,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
);
const OrgAttributesEditPageWrapper = async () => {
await validateUserHasOrgAdmin();
return <OrgAttributesEditPage />;
};
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
import { validateUserHasOrgAdmin } from "../../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("attribute"),
@@ -12,6 +14,8 @@ export const generateMetadata = async () =>
);
const OrgAttributesCreatePageWrapper = async () => {
await validateUserHasOrgAdmin();
return <OrgAttributesCreatePage />;
};
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,11 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
}
const session = await validateUserHasOrgAdmin();
const { canRead, canEdit, canDelete, canCreate } = await getResourcePermissions({
userId: session.user.id,
@@ -1,5 +1,35 @@
import BillingPage, { generateMetadata } from "../../../billing/page";
import { _generateMetadata } from "app/_utils";
import { getTranslate } from "app/_utils";
export { generateMetadata };
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export default BillingPage;
import BillingView from "~/settings/billing/billing-view";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("billing"),
(t) => t("manage_billing_description"),
undefined,
undefined,
"/settings/organizations/billing"
);
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrgAdmin();
// TODO(SEAN): Add PBAC to this page in the next PR
return (
<SettingsHeader
title={t("billing")}
description={t("manage_billing_description")}
borderInShellHeader={true}>
<BillingView />
</SettingsHeader>
);
};
export default Page;
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import DelegationCredentialList from "@calcom/features/ee/organizations/pages/settings/delegationCredential";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("delegation_credential"),
@@ -15,6 +17,8 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrgAdmin();
return (
<SettingsHeader
borderInShellHeader
@@ -1,15 +1,12 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import DirectorySyncTeamView from "@calcom/features/ee/dsync/page/team-dsync-view";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,11 +19,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/organizations/general");
}
const session = await validateUserHasOrgAdmin();
const { canEdit } = await getResourcePermissions({
userId: session.user.id,
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -21,9 +19,9 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const [t, _headers, _cookies] = await Promise.all([getTranslate(), headers(), cookies()]);
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(_headers, _cookies) });
const session = await validateUserHasOrgAdmin();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,7 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrgAdmin();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/organizations/general");
@@ -0,0 +1,229 @@
import type { Session } from "next-auth";
import { redirect } from "next/navigation";
import { describe, it, vi, expect, beforeEach, type MockedFunction } from "vitest";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrg, type ValidatedOrgSession } from "./validateUserHasOrg";
// Mock the dependencies
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
vi.mock("next/headers", () => ({
cookies: vi.fn(() => Promise.resolve({})),
headers: vi.fn(() => Promise.resolve({})),
}));
vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@lib/buildLegacyCtx", () => ({
buildLegacyRequest: vi.fn(() => ({})),
}));
const mockedGetServerSession = getServerSession as MockedFunction<typeof getServerSession>;
const mockedRedirect = vi.mocked(redirect);
describe("validateUserHasOrg", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockSession = (overrides: Partial<Session> = {}): Session => ({
expires: "2025-01-01",
hasValidLicense: true,
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.ADMIN,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
profileId: 789,
upId: "usr-123",
...overrides,
});
describe("when user has valid organization", () => {
it("should return session when user has org in user.org", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
const result = await validateUserHasOrg();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(result.user.id).toBe(123);
expect(result.user.org?.id).toBe(456);
expect(result.user.profile?.organizationId).toBe(456);
});
it("should return session when user has organizationId but no org", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const result = await validateUserHasOrg();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
});
});
describe("when user does not have valid organization", () => {
it("should redirect when session is null", async () => {
mockedGetServerSession.mockResolvedValue(null);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
it("should redirect when user has no organization data", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: null,
organization: null,
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
it("should redirect when user has no user id", async () => {
const mockSession = createMockSession({
user: {
id: undefined as unknown as number,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.ADMIN,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
});
describe("return type validation", () => {
it("should return ValidatedOrgSession type with non-nullable properties", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
const result: ValidatedOrgSession = await validateUserHasOrg();
// Type assertions to ensure proper typing
expect(typeof result.user.id).toBe("number");
expect(result.user.org).toBeTruthy();
expect(result.user.profile).toBeTruthy();
expect(typeof result.user.profile.organizationId).toBe("number");
// Runtime checks that the types guarantee these properties exist
expect(result.user.id).toBe(123);
expect(result.user.org.id).toBe(456);
expect(result.user.profile.organizationId).toBe(456);
});
});
describe("error handling", () => {
it("should propagate errors from getServerSession", async () => {
const error = new Error("Session error");
mockedGetServerSession.mockRejectedValue(error);
await expect(validateUserHasOrg()).rejects.toThrow("Session error");
});
});
});
@@ -0,0 +1,30 @@
import type { Session } from "next-auth";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export type ValidatedOrgSession = NonNullable<Session> & {
user: NonNullable<Session["user"]> & {
id: number;
org: NonNullable<NonNullable<Session["user"]>["org"]>;
profile: NonNullable<NonNullable<Session["user"]>["profile"]> & {
organizationId: number;
};
};
};
export async function validateUserHasOrg(): Promise<ValidatedOrgSession> {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const orgExists =
session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
if (!orgExists || !session?.user?.id || !session?.user?.profile?.organizationId) {
redirect("/settings/my-account/profile");
}
return session as ValidatedOrgSession;
}
@@ -0,0 +1,418 @@
import type { Session } from "next-auth";
import { redirect } from "next/navigation";
import { describe, it, vi, expect, beforeEach, type MockedFunction } from "vitest";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin, type ValidatedOrgAdminSession } from "./validateUserHasOrgAdmin";
// Mock the dependencies
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
vi.mock("next/headers", () => ({
cookies: vi.fn(() => Promise.resolve({})),
headers: vi.fn(() => Promise.resolve({})),
}));
vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@calcom/features/auth/lib/checkAdminOrOwner", () => ({
checkAdminOrOwner: vi.fn(),
}));
vi.mock("@lib/buildLegacyCtx", () => ({
buildLegacyRequest: vi.fn(() => ({})),
}));
const mockedGetServerSession = getServerSession as MockedFunction<typeof getServerSession>;
const mockedCheckAdminOrOwner = vi.mocked(checkAdminOrOwner);
const mockedRedirect = vi.mocked(redirect);
describe("validateUserHasOrgAdmin", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockSession = (overrides: Partial<Session> = {}): Session => ({
expires: "2025-01-01",
hasValidLicense: true,
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.ADMIN,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
profileId: 789,
upId: "usr-123",
...overrides,
});
describe("when user has valid organization and admin role", () => {
it("should return session when user is admin via user.org.role", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result = await validateUserHasOrgAdmin();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
expect(result.user.id).toBe(123);
expect(result.user.org?.id).toBe(456);
expect(result.user.profile?.organizationId).toBe(456);
});
it("should return session when user is owner", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.OWNER,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result = await validateUserHasOrgAdmin();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.OWNER);
});
it("should return session when role comes from organization members", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [
{ userId: 123, role: MembershipRole.ADMIN },
{ userId: 124, role: MembershipRole.MEMBER },
],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result = await validateUserHasOrgAdmin();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
});
});
describe("when user does not have valid organization", () => {
it("should redirect when session is null", async () => {
mockedGetServerSession.mockResolvedValue(null);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
it("should redirect when user has no organization data", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: null,
organization: null,
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
});
describe("when user has organization but is not admin", () => {
it("should redirect when user is only a member", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.MEMBER,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(false);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/organizations/profile");
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.MEMBER);
});
it("should redirect when role is undefined", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [
{ userId: 999, role: MembershipRole.ADMIN }, // Different user
],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(false);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/organizations/profile");
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(undefined);
});
});
describe("return type validation", () => {
it("should return ValidatedOrgAdminSession type with non-nullable properties", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result: ValidatedOrgAdminSession = await validateUserHasOrgAdmin();
// Type assertions to ensure proper typing
expect(typeof result.user.id).toBe("number");
expect(result.user.org).toBeTruthy();
expect(result.user.profile).toBeTruthy();
expect(typeof result.user.profile.organizationId).toBe("number");
expect(result.user.org.role).toBeTruthy();
// Runtime checks that the types guarantee these properties exist
expect(result.user.id).toBe(123);
expect(result.user.org.id).toBe(456);
expect(result.user.org.role).toBe(MembershipRole.ADMIN);
expect(result.user.profile.organizationId).toBe(456);
});
});
describe("role resolution logic", () => {
it("should prefer org role over organization members role", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.OWNER, // This should be preferred
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [
{ userId: 123, role: MembershipRole.MEMBER }, // Different role
],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
await validateUserHasOrgAdmin();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.OWNER);
});
it("should fall back to organization members role when org role is not available", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [{ userId: 123, role: MembershipRole.ADMIN }],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
await validateUserHasOrgAdmin();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
});
});
describe("error handling", () => {
it("should propagate errors from getServerSession", async () => {
const error = new Error("Session error");
mockedGetServerSession.mockRejectedValue(error);
await expect(validateUserHasOrgAdmin()).rejects.toThrow("Session error");
});
it("should propagate errors from checkAdminOrOwner", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
const error = new Error("Permission check error");
mockedCheckAdminOrOwner.mockImplementation(() => {
throw error;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("Permission check error");
});
});
});
@@ -0,0 +1,45 @@
import type { Session } from "next-auth";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export type ValidatedOrgAdminSession = NonNullable<Session> & {
user: NonNullable<Session["user"]> & {
id: number;
org: NonNullable<NonNullable<Session["user"]>["org"]> & {
role: MembershipRole;
};
profile: NonNullable<NonNullable<Session["user"]>["profile"]> & {
organizationId: number;
};
};
};
export async function validateUserHasOrgAdmin(): Promise<ValidatedOrgAdminSession> {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const orgExists =
session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
if (!orgExists || !session?.user?.id || !session?.user?.profile?.organizationId) {
redirect("/settings/my-account/profile");
}
const userProfile = session?.user?.profile;
const userId = session?.user?.id;
const orgRole =
session?.user?.org?.role ??
userProfile?.organization?.members.find((m: { userId: number }) => m.userId === userId)?.role;
const isOrgAdminOrOwner = checkAdminOrOwner(orgRole);
if (!isOrgAdminOrOwner) {
redirect("/settings/organizations/profile");
}
return session as ValidatedOrgAdminSession;
}
@@ -3,6 +3,8 @@ import { getTranslate, _generateMetadata } from "app/_utils";
import { AdminAPIView } from "@calcom/features/ee/organizations/pages/settings/admin-api";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${t("admin")} ${t("api_reference")}`,
@@ -15,6 +17,8 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader
title={`${t("admin")} ${t("api_reference")}`}
@@ -1,15 +1,12 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -23,11 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
}
const session = await validateUserHasOrg();
const { canRead, canEdit } = await getResourcePermissions({
userId: session.user.id,
@@ -1,19 +1,4 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const SettingsOrganizationsLayout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const orgExists =
session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
if (!orgExists) {
return redirect("/settings/my-account/profile");
}
return children;
};
@@ -1,8 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
@@ -10,7 +8,7 @@ import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import type { Membership } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,20 +20,20 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrg();
const t = await getTranslate();
const orgRole = session?.user.profile?.organization.members?.find(
(member: Membership) => member.userId === session?.user.id
const orgRole = session.user.profile.organization.members?.find(
(member: Membership) => member.userId === session.user.id
)?.role;
if (!session?.user.id || !session?.user.profile?.organizationId || !orgRole) {
if (!orgRole) {
return redirect("/settings/profile");
}
const { canRead, canEdit, canDelete } = await getResourcePermissions({
userId: session.user.id,
teamId: session?.user.profile?.organizationId,
teamId: session.user.profile.organizationId,
resource: Resource.Organization,
userRole: orgRole,
fallbackRoles: {
@@ -1,9 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
@@ -13,8 +11,7 @@ import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
import { CreateRoleCTA } from "./_components/CreateRoleCta";
import { RolesList } from "./_components/RolesList";
import { roleSearchParamsCache } from "./_components/searchParams";
@@ -58,7 +55,7 @@ export const generateMetadata = async () =>
const Page = async ({ searchParams }: { searchParams: Record<string, string | string[] | undefined> }) => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrg();
if (!session?.user?.org?.id || !session.user.id) {
return notFound();
@@ -1,13 +1,11 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { OtherTeamsListing } from "@calcom/features/ee/organizations/pages/components/OtherTeamsListing";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -20,7 +18,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrg();
if (!session?.user?.id) {
redirect("/auth/login");
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("booking_appearance"),
@@ -15,6 +17,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader
title={t("booking_appearance")}
@@ -5,6 +5,8 @@ import LegacyPage, {
} from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("team_members"),
@@ -17,6 +19,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader
title={t("team_members")}
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("profile"),
@@ -15,6 +17,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader title={t("profile")} description={t("profile_team_description")}>
<LegacyPage />
@@ -26,12 +26,12 @@ import {
SystemField,
TITLE_FIELD,
} from "@calcom/features/bookings/lib/SystemField";
import { getCalendarLinks, CalendarLinkType } from "@calcom/features/bookings/lib/getCalendarLinks";
import { RATING_OPTIONS, validateRating } from "@calcom/features/bookings/lib/rating";
import { getCalendarLinks, CalendarLinkType } from "@calcom/lib/bookings/getCalendarLinks";
import type { nameObjectSchema } from "@calcom/features/eventtypes/lib/eventNaming";
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
import { APP_NAME } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime, formatToLocalizedTimezone } from "@calcom/lib/dayjs";
import type { nameObjectSchema } from "@calcom/lib/event";
import { getEventName } from "@calcom/lib/event";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
+1 -1
View File
@@ -35,7 +35,7 @@ vi.mock("@calcom/app-store/utils", () => ({
getEventTypeAppData: vi.fn(),
}));
vi.mock("@calcom/lib/event", () => ({
vi.mock("@calcom/features/eventtypes/lib/eventNaming", () => ({
getEventName: vi.fn(),
}));
+4 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.6.16",
"version": "5.6.17",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -8,6 +8,7 @@
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "yarn copy-static && next dev --turbopack",
"dev:scan": "yarn dev & npx --yes react-scan@latest localhost:3000",
"dev:cron": "npx tsx cron-tester.ts",
"dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https",
"dx": "yarn dev",
@@ -92,6 +93,7 @@
"handlebars": "^4.7.7",
"ical.js": "^1.4.0",
"ics": "^2.37.0",
"isbot": "^5.1.30",
"jose": "^4.13.1",
"jotai": "^2.12.2",
"jsdom": "^22.0.0",
@@ -137,6 +139,7 @@
"react-use-intercom": "1.5.1",
"recoil": "^0.7.7",
"remove-markdown": "^0.5.0",
"retell-client-js-sdk": "^2.0.0",
"retell-sdk": "^4.40.0",
"rrule": "^2.7.1",
"sanitize-html": "^2.10.0",
@@ -29,8 +29,11 @@ interface RouteConfig {
value: string;
};
isFallback?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryValue?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
attributesQueryValue?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fallbackAttributesQueryValue?: any;
}
@@ -129,6 +132,7 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
const teamEventsConfig = config.attributeRouting.teamEvents;
// Create team events
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const teamEvents: { [key: string]: any } = {};
for (const eventConfig of teamEventsConfig) {
const eventType = await createRoundRobinTeamEventType({
@@ -149,10 +153,8 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
throw new Error("Attributes not found");
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const skillAttribute = attributes.find((attr) => attr.name === "Skills")!;
const locationAttribute = attributes.find((attr) => attr.name === "Location")!;
console.log("Attributes", JSON.stringify(attributes, null, 2));
const javascriptEventRoute = {
id: "8a898988-89ab-4cde-b012-31823f708642",
@@ -316,41 +318,44 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
queryValue: { id: "814899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
],
fields: config.form?.fields || [
{
id: form.formFieldLocation.id,
type: "select",
label: "Location",
options: formFieldLocationOptions,
required: true,
},
{
id: form.formFieldSkills.id,
type: "multiselect",
label: "skills",
options: formFieldSkillsOptions,
required: true,
},
{
id: form.formFieldEmail.id,
type: "email",
label: "Email",
required: true,
},
{
id: form.formFieldName.id,
identifier: "name",
type: "text",
label: "Name",
required: false,
},
{
id: form.formFieldRating.id,
type: "number",
label: "Rating",
required: false,
},
],
fields: (
config.form?.fields ||
([
{
id: form.formFieldLocation.id,
type: "select",
label: "Location",
options: formFieldLocationOptions,
required: true,
},
{
id: form.formFieldSkills.id,
type: "multiselect",
label: "skills",
options: formFieldSkillsOptions,
required: true,
},
{
id: form.formFieldEmail.id,
type: "email",
label: "Email",
required: true,
},
{
id: form.formFieldName.id,
identifier: "name",
type: "text",
label: "Name",
required: false,
},
{
id: form.formFieldRating.id,
type: "number",
label: "Rating",
required: false,
},
] satisfies FieldConfig[])
).map((field) => ({ ...field })),
team: {
connect: {
id: teamId,
@@ -379,172 +384,178 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
const form = await prisma.app_RoutingForms_Form.create({
data: {
routes: config.form?.routes || [
{
id: "8a898988-89ab-4cde-b012-31823f708642",
action: { type: "eventTypeRedirectUrl", value: "pro/30min" },
queryValue: {
routes: (
config.form?.routes ||
([
{
id: "8a898988-89ab-4cde-b012-31823f708642",
type: "group",
children1: {
"8988bbb8-0123-4456-b89a-b1823f70c5ff": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["event-routing"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
action: { type: "eventTypeRedirectUrl", value: "pro/30min" },
queryValue: {
id: "8a898988-89ab-4cde-b012-31823f708642",
type: "group",
children1: {
"8988bbb8-0123-4456-b89a-b1823f70c5ff": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["event-routing"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
},
},
},
},
},
},
{
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
action: { type: "customPageMessage", value: "Custom Page Result" },
queryValue: {
{
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
type: "group",
children1: {
"b99b8a89-89ab-4cde-b012-31823f718ff5": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["custom-page"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
action: { type: "customPageMessage", value: "Custom Page Result" },
queryValue: {
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
type: "group",
children1: {
"b99b8a89-89ab-4cde-b012-31823f718ff5": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["custom-page"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
},
},
},
},
},
},
{
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
action: { type: "externalRedirectUrl", value: `${WEBAPP_URL}/pro` },
queryValue: {
{
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
type: "group",
children1: {
"998b9b9a-0123-4456-b89a-b1823f7232b9": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["external-redirect"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
action: { type: "externalRedirectUrl", value: `${WEBAPP_URL}/pro` },
queryValue: {
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
type: "group",
children1: {
"998b9b9a-0123-4456-b89a-b1823f7232b9": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["external-redirect"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
},
},
},
},
},
},
{
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
action: { type: "customPageMessage", value: "Multiselect(Legacy) chosen" },
queryValue: {
{
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectLegacyFieldUuid,
value: [["Option-2"]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
action: { type: "customPageMessage", value: "Multiselect(Legacy) chosen" },
queryValue: {
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectLegacyFieldUuid,
value: [["Option-2"]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
},
},
},
},
},
},
{
id: "bb9ea8b9-0123-4456-b89a-b182623406d8",
action: { type: "customPageMessage", value: "Multiselect chosen" },
queryValue: {
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectFieldUuid,
value: [[multiSelectOption2Uuid]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
{
id: "bb9ea8b9-0123-4456-b89a-b182623406d8",
action: { type: "customPageMessage", value: "Multiselect chosen" },
queryValue: {
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectFieldUuid,
value: [[multiSelectOption2Uuid]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
},
},
},
},
},
},
{
id: "898899aa-4567-489a-bcde-f1823f708646",
action: { type: "customPageMessage", value: "Fallback Message" },
isFallback: true,
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
],
fields: config.form?.fields || [
{
id: "c4296635-9f12-47b1-8153-c3a854649182",
type: "text",
label: "Test field",
required: true,
},
{
id: multiSelectLegacyFieldUuid,
type: "multiselect",
label: "Multi Select(with Legacy `selectText`)",
identifier: "multi",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: multiSelectFieldUuid,
type: "multiselect",
label: "Multi Select",
identifier: "multi-new-format",
options: [
{
id: multiSelectOption1Uuid,
label: "Option-1",
},
{
id: multiSelectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
{
id: legacySelectFieldUuid,
type: "select",
label: "Legacy Select",
identifier: "test-select",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: selectFieldUuid,
type: "select",
label: "Select",
identifier: "test-select-new-format",
options: [
{
id: selectOption1Uuid,
label: "Option-1",
},
{
id: selectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
],
{
id: "898899aa-4567-489a-bcde-f1823f708646",
action: { type: "customPageMessage", value: "Fallback Message" },
isFallback: true,
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
] satisfies RouteConfig[])
).map((field) => ({ ...field })),
fields: (
config.form?.fields ||
([
{
id: "c4296635-9f12-47b1-8153-c3a854649182",
type: "text",
label: "Test field",
required: true,
},
{
id: multiSelectLegacyFieldUuid,
type: "multiselect",
label: "Multi Select(with Legacy `selectText`)",
identifier: "multi",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: multiSelectFieldUuid,
type: "multiselect",
label: "Multi Select",
identifier: "multi-new-format",
options: [
{
id: multiSelectOption1Uuid,
label: "Option-1",
},
{
id: multiSelectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
{
id: legacySelectFieldUuid,
type: "select",
label: "Legacy Select",
identifier: "test-select",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: selectFieldUuid,
type: "select",
label: "Select",
identifier: "test-select-new-format",
options: [
{
id: selectOption1Uuid,
label: "Option-1",
},
{
id: selectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
] satisfies FieldConfig[])
).map((field) => ({ ...field })),
user: {
connect: {
id: userId,
+22
View File
@@ -478,6 +478,24 @@
<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2z" ></path>
<path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" ></path>
</symbol>
<symbol class="lucide lucide-mic-off" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="mic-off">
<line x1="2" x2="22" y1="2" y2="22" ></line>
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" ></path>
<path d="M5 10v2a7 7 0 0 0 12 5" ></path>
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33" ></path>
<path d="M9 9v3a3 3 0 0 0 5.12 2.12" ></path>
<line x1="12" x2="12" y1="19" y2="22" ></line>
</symbol>
<symbol class="lucide lucide-mic" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="mic">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" ></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2" ></path>
<line x1="12" x2="12" y1="19" y2="22" ></line>
</symbol>
<symbol class="lucide lucide-monitor" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="monitor">
<rect width="20" height="14" x="2" y="3" rx="2" ></rect>
<line x1="8" x2="16" y1="21" y2="21" ></line>
<line x1="12" x2="12" y1="17" y2="21" ></line>
</symbol>
<symbol class="lucide lucide-moon" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="moon">
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" ></path>
</symbol>
@@ -498,6 +516,10 @@
<path d="M14.05 2a9 9 0 0 1 8 7.94" ></path>
<path d="M14.05 6A5 5 0 0 1 18 10" ></path>
</symbol>
<symbol class="lucide lucide-phone-off" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="phone-off">
<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91" ></path>
<line x1="22" x2="2" y1="2" y2="22" ></line>
</symbol>
<symbol class="lucide lucide-phone" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="phone">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" ></path>
</symbol>

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 53 KiB

@@ -230,6 +230,7 @@
"org_upgraded_successfully": "تمت ترقية خطة Organization بنجاح!",
"use_link_to_reset_password": "استخدم الرابط أدناه لإعادة تعيين كلمة المرور",
"hey_there": "مرحبًا",
"there": "هناك",
"forgot_your_password_calcom": "نسيت كلمة المرور الخاصة بك؟ - {{appName}}",
"delete_webhook_confirmation_message": "هل أنت متأكد أنك تريد حذف الويب هوك هذا؟ لن تتلقى بعد الآن بيانات اجتماعات {{appName}} على عنوان URL محدد، وفي الوقت الفعلي، عندما تتم جدولة حدث أو يتم إلغاؤه.",
"confirm_delete_webhook": "نعم، قم بحذف الويبهوك",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "رقم الهاتف (ليتصل به وكيلنا)",
"buy_a_phone_number_or_import_one_you_already_have": "شراء رقم هاتف أو استيراد رقم لديك بالفعل على twilio",
"test_agent": "اختبار الوكيل",
"test_web_call": "اختبار المكالمة عبر الويب",
"phone_numbers": "أرقام الهواتف",
"unsubscribe": "إلغاء الاشتراك",
"delete_workflow_step": "حذف خطوة سير العمل",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "تمت إزالة التطبيق بنجاح",
"error_removing_app": "خطأ في إزالة التطبيق",
"web_conference": "مؤتمر عبر الإنترنت",
"web_call": "مكالمة ويب",
"start_web_call": "بدء مكالمة الويب",
"end_call": "إنهاء المكالمة",
"test_your_agent_with_web_call": "اختبر وكيلك بمكالمة ويب دون الحاجة إلى رقم هاتف",
"phone_call": "مكالمة هاتفية",
"connecting_to_agent": "جارٍ الاتصال بالوكيل...",
"call_active": "المكالمة نشطة",
"call_ended": "انتهت المكالمة",
"call_error": "خطأ في المكالمة",
"ready_to_start_call": "جاهز لبدء المكالمة",
"failed_to_start_web_call_try_again": "فشل بدء مكالمة الويب. يرجى المحاولة مرة أخرى.",
"call_encountered_error_try_again": "واجهت المكالمة خطأ. يرجى المحاولة مرة أخرى.",
"failed_initialize_web_call_microphone_permissions": "فشل تهيئة مكالمة الويب. يرجى التأكد من منح أذونات الميكروفون.",
"start_call_to_see_conversation": "ابدأ مكالمة لرؤية المحادثة",
"waiting_for_conversation": "في انتظار المحادثة...",
"ai_agent": "وكيل الذكاء الاصطناعي",
"live": "مباشر",
"connecting": "جارٍ الاتصال...",
"requires_confirmation": "يتطلب التأكيد",
"requires_confirmation_threshold": "يتطلب تأكيد إذا تم حجزه بإشعار أقل من {{time}} $t({{unit}}_timeUnit) ",
"may_require_confirmation": "قد يتطلب التأكيد",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "التفريغ النصي مفعّل الآن",
"transcription_stopped": "التفريغ النصي متوقف الآن",
"download_transcript": "تحميل التفريغ النصي",
"transcript": "النص المكتوب",
"recording_from_your_recent_call": "تسجيل من مكالمتك الأخيرة على {{appName}} جاهز للتنزيل",
"transcript_from_previous_call": "التفريغ النصي من مكالمة الأخيرة على {{appName}} جاهز للتحميل. الروابط متاحة لمدة ساعة واحدة فقط",
"you_can_download_transcript_from_attachments": "يمكنك أيضاً تحميل النص المكتوب من المرفقات",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "إخفاء عنوان البريد الإلكتروني للمنظم من شاشة الحجز وإشعارات البريد الإلكتروني وأحداث التقويم",
"you_and_conjunction": "& أنت",
"select_verified_email": "اختر بريدًا إلكترونيًا تم التحقق منه",
"add_verified_email": "إضافة بريد إلكتروني موثق",
"add_verified_emails": "إضافة بريد إلكتروني موثق",
"custom_reply_to_email_title": "بريد إلكتروني مخصص للرد",
"custom_reply_to_email_description": "استخدم عنوان بريد إلكتروني مختلف كعنوان للرد على رسائل التأكيد بدلاً من بريد المنظم",
"email_survey_triggered_by_workflow": "تم تشغيل هذا الاستبيان بواسطة سير العمل في كال.",
@@ -3619,6 +3642,11 @@
"send_another_message": "إرسال رسالة أخرى",
"sending": "جارٍ الإرسال",
"no_variables_found": "لم يتم العثور على متغيرات",
"credits_required": "الرصيد المطلوب",
"web_call_credits_info": "تستهلك مكالمات الويب رصيداً. تأكد من وجود رصيد كافٍ لبدء المكالمة.",
"web_call_no_credits": "تتطلب مكالمات الويب رصيداً. لديك 0 رصيد متبقي.",
"purchase_credits": "شراء رصيد",
"credits_required_tooltip": "يتطلب هذا الإجراء رصيداً للمتابعة",
"api_key_name_too_long": "يجب ألا يتجاوز الاسم {{max}} حرفًا",
"auto_charge_for_last_minute_cancellation": "فرض رسوم التخلف عن الحضور تلقائياً للإلغاءات في اللحظة الأخيرة",
"before_scheduled_start_time": "قبل وقت البدء المحدد",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Təşkilatınız uğurla təkmilləşdirildi!",
"use_link_to_reset_password": "Şifrənizi sıfırlamaq üçün aşağıdakı linkdən istifadə edin",
"hey_there": "Salam",
"there": "orada",
"forgot_your_password_calcom": "Şifrənizi unutmusunuz? - {{appName}}",
"delete_webhook_confirmation_message": "Bu webhook-u silmək istədiyinizə əminsinizmi? Tədbir planlaşdırıldıqda və ya ləğv edildikdə, real vaxtda müəyyən edilmiş URL-də {{appName}} görüş məlumatlarını artıq almayacaqsınız.",
"confirm_delete_webhook": "Bəli, webhook-u silin",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefon nömrəsi (agentimizin zəng edəcəyi)",
"buy_a_phone_number_or_import_one_you_already_have": "Telefon nömrəsi alın və ya artıq twilio-da malik olduğunuz nömrəni idxal edin",
"test_agent": "Test agent",
"test_web_call": "Veb Zəngi Test Edin",
"phone_numbers": "Telefon nömrələri",
"unsubscribe": "Abunəlikdən çıx",
"delete_workflow_step": "İş axını addımını sil",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Tətbiq uğurla silindi",
"error_removing_app": "Tətbiqi silərkən səhv baş verdi",
"web_conference": "Veb konfrans",
"web_call": "Veb zəng",
"start_web_call": "Veb zəngi başladın",
"end_call": "Zəngi bitirin",
"test_your_agent_with_web_call": "Telefon nömrəsi tələb etmədən agentinizi veb zənglə test edin",
"phone_call": "Telefon zəngi",
"connecting_to_agent": "Agentə qoşulur...",
"call_active": "Zəng aktivdir",
"call_ended": "Zəng bitdi",
"call_error": "Zəng xətası",
"ready_to_start_call": "Zəngi başlatmağa hazırdır",
"failed_to_start_web_call_try_again": "Veb zəngi başlatmaq alınmadı. Zəhmət olmasa yenidən cəhd edin.",
"call_encountered_error_try_again": "Zəng zamanı xəta baş verdi. Zəhmət olmasa yenidən cəhd edin.",
"failed_initialize_web_call_microphone_permissions": "Veb zəngi başlatmaq alınmadı. Zəhmət olmasa mikrofon icazələrinin verildiyindən əmin olun.",
"start_call_to_see_conversation": "Söhbəti görmək üçün zəngi başladın",
"waiting_for_conversation": "Söhbət gözlənilir...",
"ai_agent": "Sİ Agenti",
"live": "Canlı",
"connecting": "Qoşulur...",
"requires_confirmation": "Təsdiq tələb edir",
"requires_confirmation_threshold": "< {{time}} $t({{unit}}_timeUnit) xəbərdarlıqla rezervasiya edildikdə təsdiq tələb edir",
"may_require_confirmation": "Təsdiq tələb edə bilər",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transkripsiyalar indi aktivdir",
"transcription_stopped": "Transkripsiyalar dayandırıldı",
"download_transcript": "Transkripti yüklə",
"transcript": "Transkript",
"recording_from_your_recent_call": "{{appName}} üzərindən son zənginizin qeydi yükləməyə hazırdır",
"transcript_from_previous_call": "{{appName}} üzərindən son zənginizin transkripti yükləməyə hazırdır. Link yalnız 1 saat etibarlıdır",
"you_can_download_transcript_from_attachments": "Siz həmçinin transkripti əlavələrdən yükləyə bilərsiniz",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Təşkilatçının e-poçt ünvanını rezervasiya ekranından, e-poçt bildirişlərindən və təqvim tədbirlərindən gizlədin",
"you_and_conjunction": "Siz &",
"select_verified_email": "Təsdiqlənmiş e-poçt seçin",
"add_verified_email": "Təsdiqlənmiş e-poçt əlavə et",
"add_verified_emails": "Təsdiqlənmiş e-poçtlar əlavə et",
"custom_reply_to_email_title": "Xüsusi 'Cavab-Ünvanı' e-poçtu",
"custom_reply_to_email_description": "Təsdiq e-poçtları üçün təşkilatçının e-poçtu əvəzinə fərqli bir e-poçt ünvanını cavab ünvanı kimi istifadə edin",
"email_survey_triggered_by_workflow": "Bu sorğu Cal-da Workflow tərəfindən başladıldı.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Başqa bir mesaj göndər",
"sending": "Göndərilir",
"no_variables_found": "Heç bir dəyişən tapılmadı",
"credits_required": "Kreditlər Tələb Olunur",
"web_call_credits_info": "Veb zənglər kredit istifadə edir. Zəngi başlatmaq üçün kifayət qədər kreditinizin olduğundan əmin olun.",
"web_call_no_credits": "Veb zənglər kredit tələb edir. Sizin 0 kredit qalıb.",
"purchase_credits": "Kredit Alın",
"credits_required_tooltip": "Bu əməliyyat davam etmək üçün kredit tələb edir",
"api_key_name_too_long": "Ad {{max}} simvolu keçməməlidir",
"auto_charge_for_last_minute_cancellation": "Son dəqiqə ləğvləri üçün avtomatik olaraq gəlməmə haqqını tutun",
"before_scheduled_start_time": "Planlaşdırılmış başlama vaxtından əvvəl",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Вашата организация беше успешно ъпгрейдната!",
"use_link_to_reset_password": "Използвайте линка по-долу, за да възстановите паролата си",
"hey_there": "Здравейте",
"there": "там",
"forgot_your_password_calcom": "Забравена парола? - {{appName}}",
"delete_webhook_confirmation_message": "Сигурни ли сте, че искате да изтриете този уебхук? Вече няма да получавате данни за срещите от {{appName}} на посочения URL адрес в реално време, когато събитие бъде планирано или отменено.",
"confirm_delete_webhook": "Да, изтрий уебхука",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Телефонен номер (за обаждане от нашия агент)",
"buy_a_phone_number_or_import_one_you_already_have": "Купете телефонен номер или импортирайте такъв, който вече имате в twilio",
"test_agent": "Тестов агент",
"test_web_call": "Тестово уеб обаждане",
"phone_numbers": "Телефонни номера",
"unsubscribe": "Отпишете се",
"delete_workflow_step": "Изтрийте стъпка от работния процес",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Приложението е премахнато успешно",
"error_removing_app": "Грешка при премахване на приложението",
"web_conference": "Уеб конференция",
"web_call": "Уеб обаждане",
"start_web_call": "Започнете уеб обаждане",
"end_call": "Прекратете обаждането",
"test_your_agent_with_web_call": "Тествайте вашия агент с уеб обаждане без да е необходим телефонен номер",
"phone_call": "Телефонно обаждане",
"connecting_to_agent": "Свързване с агент...",
"call_active": "Активно обаждане",
"call_ended": "Обаждането приключи",
"call_error": "Грешка при обаждането",
"ready_to_start_call": "Готови за започване на обаждане",
"failed_to_start_web_call_try_again": "Неуспешно започване на уеб обаждане. Моля, опитайте отново.",
"call_encountered_error_try_again": "Възникна грешка при обаждането. Моля, опитайте отново.",
"failed_initialize_web_call_microphone_permissions": "Неуспешно инициализиране на уеб обаждане. Моля, уверете се, че са предоставени разрешения за микрофона.",
"start_call_to_see_conversation": "Започнете обаждане, за да видите разговора",
"waiting_for_conversation": "Изчакване на разговор...",
"ai_agent": "AI агент",
"live": "На живо",
"connecting": "Свързване...",
"requires_confirmation": "Изисква потвърждение",
"requires_confirmation_threshold": "Изисква потвърждение, ако е резервирано с < {{time}} $t({{unit}}_timeUnit) предизвестие",
"may_require_confirmation": "Може да изисква потвърждение",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Транскрипциите са активирани",
"transcription_stopped": "Транскрипциите са спрени",
"download_transcript": "Изтегли транскрипт",
"transcript": "Транскрипт",
"recording_from_your_recent_call": "Записът от последния ви разговор в {{appName}} е готов за изтегляне",
"transcript_from_previous_call": "Транскриптът от последния ви разговор в {{appName}} е готов за изтегляне. Линковете са валидни само за 1 час",
"you_can_download_transcript_from_attachments": "Можете също да изтеглите транскрипта от прикачените файлове",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Скриване на имейл адреса на организатора от екрана за резервации, имейл известията и календарните събития",
"you_and_conjunction": "Вие &",
"select_verified_email": "Изберете потвърден имейл",
"add_verified_email": "Добави потвърден имейл",
"add_verified_emails": "Добави потвърдени имейли",
"custom_reply_to_email_title": "Персонализиран имейл за отговор",
"custom_reply_to_email_description": "Използвайте различен имейл адрес като адрес за отговор в имейлите за потвърждение вместо имейла на организатора",
"email_survey_triggered_by_workflow": "Тази анкета беше стартирана от Workflow в Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Изпратете друго съобщение",
"sending": "Изпращане",
"no_variables_found": "Не са намерени променливи",
"credits_required": "Необходими кредити",
"web_call_credits_info": "Уеб обажданията консумират кредити. Уверете се, че имате достатъчно кредити, за да започнете обаждане.",
"web_call_no_credits": "Уеб обажданията изискват кредити. Имате 0 оставащи кредити.",
"purchase_credits": "Закупете кредити",
"credits_required_tooltip": "Това действие изисква кредити, за да продължите",
"api_key_name_too_long": "Името не трябва да надвишава {{max}} символа",
"auto_charge_for_last_minute_cancellation": "Автоматично таксуване на такса за неявяване при отмяна в последния момент",
"before_scheduled_start_time": "Преди планираното начално време",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "আপনার সংস্থা সফলভাবে আপগ্রেড করা হয়েছিল!",
"use_link_to_reset_password": "আপনার পাসওয়ার্ডটি পুনরায় সেট করতে নীচের লিঙ্কটি ব্যবহার করুন",
"hey_there": "আরে ওখানে",
"there": "সেখানে",
"forgot_your_password_calcom": "আপনার পাসওয়ার্ড ভুলে গেছেন? {{appName}}",
"delete_webhook_confirmation_message": "আপনি কি নিশ্চিত যে আপনি এই ওয়েবহুকটি মুছতে চান? {{appName}} রিয়েল-টাইমে একটি নির্দিষ্ট ইউআরএলে ডেটা সভা করা, যখন কোনও ইভেন্ট নির্ধারিত বা বাতিল হয়।",
"confirm_delete_webhook": "হ্যাঁ, ওয়েবহুক মুছুন",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "ফোন নম্বর (আমাদের এজেন্ট কল করার জন্য)",
"buy_a_phone_number_or_import_one_you_already_have": "একটি ফোন নম্বর কিনুন অথবা আপনার কাছে ইতিমধ্যে থাকা twilio নম্বর ইম্পোর্ট করুন",
"test_agent": "টেস্ট এজেন্ট",
"test_web_call": "ওয়েব কল পরীক্ষা করুন",
"phone_numbers": "ফোন নম্বরসমূহ",
"unsubscribe": "আনসাবস্ক্রাইব",
"delete_workflow_step": "ওয়ার্কফ্লো স্টেপ মুছুন",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "অ্যাপ্লিকেশন সফলভাবে সরানো হয়েছে",
"error_removing_app": "অ্যাপ্লিকেশন অপসারণ করতে ত্রুটি",
"web_conference": "ওয়েব সম্মেলন",
"web_call": "ওয়েব কল",
"start_web_call": "ওয়েব কল শুরু করুন",
"end_call": "কল শেষ করুন",
"test_your_agent_with_web_call": "ফোন নম্বর ছাড়াই ওয়েব কলের মাধ্যমে আপনার এজেন্ট পরীক্ষা করুন",
"phone_call": "ফোন কল",
"connecting_to_agent": "এজেন্টের সাথে সংযোগ করা হচ্ছে...",
"call_active": "কল সক্রিয়",
"call_ended": "কল শেষ হয়েছে",
"call_error": "কল ত্রুটি",
"ready_to_start_call": "কল শুরু করতে প্রস্তুত",
"failed_to_start_web_call_try_again": "ওয়েব কল শুরু করতে ব্যর্থ হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"call_encountered_error_try_again": "কলে একটি ত্রুটি দেখা দিয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।",
"failed_initialize_web_call_microphone_permissions": "ওয়েব কল শুরু করতে ব্যর্থ হয়েছে। অনুগ্রহ করে মাইক্রোফোন অনুমতি প্রদান করা হয়েছে তা নিশ্চিত করুন।",
"start_call_to_see_conversation": "কথোপকথন দেখতে একটি কল শুরু করুন",
"waiting_for_conversation": "কথোপকথনের জন্য অপেক্ষা করছে...",
"ai_agent": "এআই এজেন্ট",
"live": "লাইভ",
"connecting": "সংযোগ করা হচ্ছে...",
"requires_confirmation": "নিশ্চিতকরণ প্রয়োজন",
"requires_confirmation_threshold": "<দিয়ে বুক করা থাকলে নিশ্চিতকরণের প্রয়োজন {{time}} $t({{unit}}_timeUnit) বিজ্ঞপ্তি",
"may_require_confirmation": "নিশ্চিতকরণের প্রয়োজন হতে পারে",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "প্রতিলিপি এখন সক্ষম করা হয়",
"transcription_stopped": "প্রতিলিপি এখন বন্ধ",
"download_transcript": "ট্রান্সক্রিপ্ট ডাউনলোড করুন",
"transcript": "ট্রান্সক্রিপ্ট",
"recording_from_your_recent_call": "আপনার সাম্প্রতিক কল থেকে একটি রেকর্ডিং {{appName}} ডাউনলোডের জন্য প্রস্তুত",
"transcript_from_previous_call": "আপনার সাম্প্রতিক কল থেকে প্রতিলিপি {{appName}} ডাউনলোড করতে প্রস্তুত। ",
"you_can_download_transcript_from_attachments": "আপনি সংযুক্তি থেকেও ট্রান্সক্রিপ্ট ডাউনলোড করতে পারেন",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "বুকিং স্ক্রিন, ইমেইল বিজ্ঞপ্তি এবং ক্যালেন্ডার ইভেন্ট থেকে আয়োজকের ইমেইল ঠিকানা লুকান",
"you_and_conjunction": "আপনি এবং",
"select_verified_email": "একটি যাচাইকৃত ইমেইল নির্বাচন করুন",
"add_verified_email": "যাচাইকৃত ইমেইল যোগ করুন",
"add_verified_emails": "যাচাইকৃত ইমেইলগুলি যোগ করুন",
"custom_reply_to_email_title": "কাস্টম 'রিপ্লাই-টু' ইমেইল",
"custom_reply_to_email_description": "আয়োজকের ইমেইলের পরিবর্তে নিশ্চিতকরণ ইমেইলের জন্য রিপ্লাইটু হিসাবে একটি ভিন্ন ইমেইল ঠিকানা ব্যবহার করুন",
"email_survey_triggered_by_workflow": "এই সমীক্ষাটি Cal-এ একটি ওয়ার্কফ্লো দ্বারা ট্রিগার করা হয়েছিল।",
@@ -3619,6 +3642,11 @@
"send_another_message": "আরেকটি বার্তা পাঠান",
"sending": "পাঠানো হচ্ছে",
"no_variables_found": "কোন ভেরিয়েবল পাওয়া যায়নি",
"credits_required": "ক্রেডিট প্রয়োজন",
"web_call_credits_info": "ওয়েব কল ক্রেডিট ব্যবহার করে। কল শুরু করার জন্য আপনার পর্যাপ্ত ক্রেডিট আছে তা নিশ্চিত করুন।",
"web_call_no_credits": "ওয়েব কলের জন্য ক্রেডিট প্রয়োজন। আপনার ০ ক্রেডিট অবশিষ্ট আছে।",
"purchase_credits": "ক্রেডিট কিনুন",
"credits_required_tooltip": "এই কাজটি করতে ক্রেডিট প্রয়োজন",
"api_key_name_too_long": "নাম অবশ্যই {{max}} অক্ষরের বেশি হবে না",
"auto_charge_for_last_minute_cancellation": "শেষ মুহূর্তের বাতিলকরণের জন্য স্বয়ংক্রিয়ভাবে অনুপস্থিতি ফি চার্জ করুন",
"before_scheduled_start_time": "নির্ধারিত শুরুর সময়ের আগে",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "La teva Organització s'ha actualitzat correctament!",
"use_link_to_reset_password": "Utilitza l'enllaç següent per restablir la teva contrasenya",
"hey_there": "Hola",
"there": "allà",
"forgot_your_password_calcom": "Has oblidat la teva contrasenya? - {{appName}}",
"delete_webhook_confirmation_message": "Estàs segur que vols eliminar aquest webhook? Ja no rebràs dades de reunions de {{appName}} a l'URL especificada en temps real quan es programi o cancel·li un esdeveniment.",
"confirm_delete_webhook": "Sí, elimina el webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Número de telèfon (perquè el nostre agent truqui)",
"buy_a_phone_number_or_import_one_you_already_have": "Compra un número de telèfon o importa'n un que ja tinguis a Twilio",
"test_agent": "Prova l'agent",
"test_web_call": "Prova de trucada web",
"phone_numbers": "Números de telèfon",
"unsubscribe": "Donar de baixa",
"delete_workflow_step": "Eliminar pas del flux de treball",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "L'aplicació s'ha eliminat correctament",
"error_removing_app": "Error en eliminar l'aplicació",
"web_conference": "Conferència web",
"web_call": "Trucada web",
"start_web_call": "Inicia la trucada web",
"end_call": "Finalitza la trucada",
"test_your_agent_with_web_call": "Prova el teu agent amb una trucada web sense necessitar un número de telèfon",
"phone_call": "Trucada telefònica",
"connecting_to_agent": "Connectant amb l'agent...",
"call_active": "Trucada activa",
"call_ended": "Trucada finalitzada",
"call_error": "Error de trucada",
"ready_to_start_call": "Preparat per iniciar la trucada",
"failed_to_start_web_call_try_again": "No s'ha pogut iniciar la trucada web. Si us plau, torna-ho a provar.",
"call_encountered_error_try_again": "La trucada ha trobat un error. Si us plau, torna-ho a provar.",
"failed_initialize_web_call_microphone_permissions": "No s'ha pogut inicialitzar la trucada web. Assegura't que els permisos del micròfon estiguin concedits.",
"start_call_to_see_conversation": "Inicia una trucada per veure la conversa",
"waiting_for_conversation": "Esperant la conversa...",
"ai_agent": "Agent d'IA",
"live": "En directe",
"connecting": "Connectant...",
"requires_confirmation": "Requereix confirmació",
"requires_confirmation_threshold": "Requereix confirmació si es reserva amb < {{time}} $t({{unit}}_timeUnit) d'antelació",
"may_require_confirmation": "Pot requerir confirmació",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Les transcripcions estan activades ara",
"transcription_stopped": "Les transcripcions s'han aturat ara",
"download_transcript": "Descarrega la transcripció",
"transcript": "Transcripció",
"recording_from_your_recent_call": "Un enregistrament de la teva trucada recent a {{appName}} està llest per descarregar",
"transcript_from_previous_call": "La transcripció de la teva trucada recent a {{appName}} està llesta per descarregar. Els enllaços només són vàlids durant 1 hora",
"you_can_download_transcript_from_attachments": "També podeu descarregar la transcripció des dels arxius adjunts",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Amaga l'adreça de correu electrònic de l'organitzador de la pantalla de reserva, les notificacions per correu electrònic i els esdeveniments del calendari",
"you_and_conjunction": "Vosaltres &",
"select_verified_email": "Selecciona un correu electrònic verificat",
"add_verified_email": "Afegir correu electrònic verificat",
"add_verified_emails": "Afegir correus electrònics verificats",
"custom_reply_to_email_title": "Correu electrònic personalitzat de 'Respondre a'",
"custom_reply_to_email_description": "Utilitza una adreça de correu electrònic diferent com a resposta per als correus de confirmació en lloc del correu de l'organitzador",
"email_survey_triggered_by_workflow": "Aquesta enquesta va ser activada per un Workflow a Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Envia un altre missatge",
"sending": "Enviant",
"no_variables_found": "No s'han trobat variables",
"credits_required": "Crèdits necessaris",
"web_call_credits_info": "Les trucades web consumeixen crèdits. Assegura't de tenir prou crèdits per iniciar una trucada.",
"web_call_no_credits": "Les trucades web requereixen crèdits. Et queden 0 crèdits.",
"purchase_credits": "Compra crèdits",
"credits_required_tooltip": "Aquesta acció requereix crèdits per continuar",
"api_key_name_too_long": "El nom no ha de superar els {{max}} caràcters",
"auto_charge_for_last_minute_cancellation": "Càrrec automàtic de la tarifa per no assistència en cas de cancel·lacions d'última hora",
"before_scheduled_start_time": "Abans de l'hora d'inici programada",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Váš upgrade na tarif Organization byl úspěšně dokončen!",
"use_link_to_reset_password": "Pro obnovení hesla použijte odkaz níž",
"hey_there": "Ahoj",
"there": "tam",
"forgot_your_password_calcom": "Zapomněli jste heslo? - {{appName}}",
"delete_webhook_confirmation_message": "Opravdu chcete smazat tento webhook? Přestanete v reálném čase dostávat {{appName}} data o schůzce na dané URL, pokud bude událost naplánována nebo zrušena.",
"confirm_delete_webhook": "Ano, smazat webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefonní číslo (pro volání našeho agenta)",
"buy_a_phone_number_or_import_one_you_already_have": "Kupte si telefonní číslo nebo importujte to, které již máte na Twilio",
"test_agent": "Otestovat agenta",
"test_web_call": "Testovací webový hovor",
"phone_numbers": "Telefonní čísla",
"unsubscribe": "Odhlásit",
"delete_workflow_step": "Smazat krok workflow",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplikace byla odstraněna",
"error_removing_app": "Při odstraňování aplikace došlo k chybě",
"web_conference": "Webová konference",
"web_call": "Webový hovor",
"start_web_call": "Zahájit webový hovor",
"end_call": "Ukončit hovor",
"test_your_agent_with_web_call": "Otestujte svého agenta webovým hovorem bez nutnosti telefonního čísla",
"phone_call": "Telefonní hovor",
"connecting_to_agent": "Připojování k agentovi...",
"call_active": "Hovor je aktivní",
"call_ended": "Hovor byl ukončen",
"call_error": "Chyba hovoru",
"ready_to_start_call": "Připraveno k zahájení hovoru",
"failed_to_start_web_call_try_again": "Nepodařilo se zahájit webový hovor. Zkuste to prosím znovu.",
"call_encountered_error_try_again": "Během hovoru došlo k chybě. Zkuste to prosím znovu.",
"failed_initialize_web_call_microphone_permissions": "Nepodařilo se inicializovat webový hovor. Ujistěte se, že jsou udělena oprávnění pro mikrofon.",
"start_call_to_see_conversation": "Zahajte hovor pro zobrazení konverzace",
"waiting_for_conversation": "Čekání na konverzaci...",
"ai_agent": "AI Agent",
"live": "Živě",
"connecting": "Připojování...",
"requires_confirmation": "Vyžaduje potvrzení",
"requires_confirmation_threshold": "Pokud je rezervace provedena s předstihem < {{time}} $t({{unit}}_timeUnit), je nutné potvrzení",
"may_require_confirmation": "Může vyžadovat potvrzení",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Přepisy jsou nyní povoleny",
"transcription_stopped": "Přepisy byly zastaveny",
"download_transcript": "Stáhnout přepis",
"transcript": "Přepis",
"recording_from_your_recent_call": "Nahrávka z vašeho nedávného hovoru v {{appName}} je připravena ke stažení",
"transcript_from_previous_call": "Přepis z vašeho nedávného hovoru na {{appName}} je připraven ke stažení. Odkazy jsou platné pouze 1 hodinu",
"you_can_download_transcript_from_attachments": "Přepis si můžete také stáhnout z příloh",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Skrýt e-mailovou adresu organizátora na obrazovce rezervace, v e-mailových oznámeních a událostech kalendáře",
"you_and_conjunction": "Vy &",
"select_verified_email": "Vyberte ověřený e-mail",
"add_verified_email": "Přidat ověřený e-mail",
"add_verified_emails": "Přidat ověřené e-maily",
"custom_reply_to_email_title": "Vlastní e-mail pro odpověď",
"custom_reply_to_email_description": "Použít jinou e-mailovou adresu jako adresu pro odpověď v potvrzovacích e-mailech místo e-mailu organizátora",
"email_survey_triggered_by_workflow": "Tento průzkum byl spuštěn pracovním postupem v Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Poslat další zprávu",
"sending": "Odesílání",
"no_variables_found": "Nebyly nalezeny žádné proměnné",
"credits_required": "Vyžadovány kredity",
"web_call_credits_info": "Webové hovory spotřebovávají kredity. Ujistěte se, že máte dostatek kreditů pro zahájení hovoru.",
"web_call_no_credits": "Webové hovory vyžadují kredity. Zbývá vám 0 kreditů.",
"purchase_credits": "Zakoupit kredity",
"credits_required_tooltip": "Tato akce vyžaduje kredity k pokračování",
"api_key_name_too_long": "Název nesmí přesáhnout {{max}} znaků",
"auto_charge_for_last_minute_cancellation": "Automaticky účtovat poplatek za nedostavení se při zrušení na poslední chvíli",
"before_scheduled_start_time": "Před plánovaným časem zahájení",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Din organisation blev opgraderet med succes!",
"use_link_to_reset_password": "Brug nedenstående link for at nulstille din adgangskode",
"hey_there": "Hej der",
"there": "der",
"forgot_your_password_calcom": "Glemt din adgangskode? - {{appName}}",
"delete_webhook_confirmation_message": "Er du sikker på, at du vil slette denne webhook? Du vil ikke længere modtage {{appName}} mødedata på en specificeret URL, i realtid, når en begivenhed bliver planlagt eller annulleret.",
"confirm_delete_webhook": "Ja, slet webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefonnummer (til vores agent at ringe til)",
"buy_a_phone_number_or_import_one_you_already_have": "Køb et telefonnummer eller importer et, du allerede har på Twilio",
"test_agent": "Test agent",
"test_web_call": "Test webopkald",
"phone_numbers": "Telefonnumre",
"unsubscribe": "Afmeld",
"delete_workflow_step": "Slet workflow-trin",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "App fjernet",
"error_removing_app": "Fejl ved fjernelse af app",
"web_conference": "Web konference",
"web_call": "Webopkald",
"start_web_call": "Start webopkald",
"end_call": "Afslut opkald",
"test_your_agent_with_web_call": "Test din agent med et webopkald uden at kræve et telefonnummer",
"phone_call": "Telefonopkald",
"connecting_to_agent": "Forbinder til agent...",
"call_active": "Opkald aktivt",
"call_ended": "Opkald afsluttet",
"call_error": "Opkaldsfejl",
"ready_to_start_call": "Klar til at starte opkald",
"failed_to_start_web_call_try_again": "Kunne ikke starte webopkald. Prøv igen.",
"call_encountered_error_try_again": "Opkaldet stødte på en fejl. Prøv igen.",
"failed_initialize_web_call_microphone_permissions": "Kunne ikke initialisere webopkald. Sørg for, at mikrofontilladelser er givet.",
"start_call_to_see_conversation": "Start et opkald for at se samtalen",
"waiting_for_conversation": "Venter på samtale...",
"ai_agent": "AI-agent",
"live": "Live",
"connecting": "Forbinder...",
"requires_confirmation": "Kræver bekræftelse",
"requires_confirmation_threshold": "Kræver bekræftelse hvis booket med < {{time}} $t({{unit}}_timeUnit) meddelelse",
"may_require_confirmation": "Kan kræve bekræftelse",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transskriptioner er nu aktiveret",
"transcription_stopped": "Transskriptioner er nu stoppet",
"download_transcript": "Download transskription",
"transcript": "Transskription",
"recording_from_your_recent_call": "En optagelse fra dit seneste opkald på {{appName}} er klar til download",
"transcript_from_previous_call": "Udskrift fra dit seneste opkald på {{appName}} er klar til download. Links er kun gyldige i 1 time",
"you_can_download_transcript_from_attachments": "Du kan også downloade udskriften fra vedhæftede filer",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Skjul arrangørens e-mailadresse fra bookingskærmen, e-mailnotifikationer og kalenderbegivenheder",
"you_and_conjunction": "Du &",
"select_verified_email": "Vælg en verificeret e-mail",
"add_verified_email": "Tilføj verificeret e-mail",
"add_verified_emails": "Tilføj verificerede e-mails",
"custom_reply_to_email_title": "Brugerdefineret 'Svar-til'-e-mail",
"custom_reply_to_email_description": "Brug en anden e-mailadresse som svar-til for bekræftelses-e-mails i stedet for arrangørens e-mail",
"email_survey_triggered_by_workflow": "Denne undersøgelse blev udløst af en Arbejdsgang i Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Send en ny besked",
"sending": "Sender",
"no_variables_found": "Ingen variable fundet",
"credits_required": "Credits påkrævet",
"web_call_credits_info": "Webopkald bruger credits. Sørg for, at du har nok credits til at starte et opkald.",
"web_call_no_credits": "Webopkald kræver credits. Du har 0 credits tilbage.",
"purchase_credits": "Køb credits",
"credits_required_tooltip": "Denne handling kræver credits for at fortsætte",
"api_key_name_too_long": "Navnet må ikke overstige {{max}} tegn",
"auto_charge_for_last_minute_cancellation": "Automatisk opkrævning af gebyr for udeblivelse ved aflysning i sidste øjeblik",
"before_scheduled_start_time": "Før den planlagte starttid",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Das Upgrade für Ihre Organization war erfolgreich!",
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
"hey_there": "Hallo",
"there": "dort",
"forgot_your_password_calcom": "Passwort vergessen? - {{appName}}",
"delete_webhook_confirmation_message": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten? Sie werden keine {{appName}}-Meetingdaten mehr unter einer bestimmten URL, in Echtzeit erhalten, wenn ein Ereignis geplant oder abgesagt wird.",
"confirm_delete_webhook": "Ja, Webhook löschen",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefonnummer (für unseren Agenten zum Anrufen)",
"buy_a_phone_number_or_import_one_you_already_have": "Kaufen Sie eine Telefonnummer oder importieren Sie eine, die Sie bereits bei Twilio haben",
"test_agent": "Agent testen",
"test_web_call": "Web-Anruf testen",
"phone_numbers": "Telefonnummern",
"unsubscribe": "Abmelden",
"delete_workflow_step": "Workflow-Schritt löschen",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "App erfolgreich entfernt",
"error_removing_app": "Fehler beim Entfernen der App",
"web_conference": "Webkonferenz",
"web_call": "Web-Anruf",
"start_web_call": "Web-Anruf starten",
"end_call": "Anruf beenden",
"test_your_agent_with_web_call": "Testen Sie Ihren Agenten mit einem Web-Anruf ohne Telefonnummer",
"phone_call": "Telefonanruf",
"connecting_to_agent": "Verbindung zum Agenten wird hergestellt...",
"call_active": "Anruf aktiv",
"call_ended": "Anruf beendet",
"call_error": "Anruffehler",
"ready_to_start_call": "Bereit zum Starten des Anrufs",
"failed_to_start_web_call_try_again": "Web-Anruf konnte nicht gestartet werden. Bitte versuchen Sie es erneut.",
"call_encountered_error_try_again": "Bei dem Anruf ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"failed_initialize_web_call_microphone_permissions": "Web-Anruf konnte nicht initialisiert werden. Bitte stellen Sie sicher, dass Mikrofonberechtigungen erteilt wurden.",
"start_call_to_see_conversation": "Starten Sie einen Anruf, um die Konversation zu sehen",
"waiting_for_conversation": "Warte auf Konversation...",
"ai_agent": "KI-Agent",
"live": "Live",
"connecting": "Verbinde...",
"requires_confirmation": "Erfordert Bestätigung",
"requires_confirmation_threshold": "Erfordert eine Bestätigung bei Buchung mit < {{time}} $t({{unit}}_timeUnit) Vorlauf",
"may_require_confirmation": "Kann Bestätigung erfordern",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transkriptionen sind jetzt aktiviert",
"transcription_stopped": "Transkriptionen werden jetzt gestoppt",
"download_transcript": "Transkript herunterladen",
"transcript": "Transkript",
"recording_from_your_recent_call": "Eine Aufnahme von Ihrem letzten Anruf auf {{appName}} steht zum Download bereit",
"transcript_from_previous_call": "Transkript von Ihrem letzten Anruf bei {{appName}} ist zum Herunterladen bereit. Links sind nur für 1 Stunde gültig",
"you_can_download_transcript_from_attachments": "Sie können das Protokoll auch aus den Anhängen herunterladen",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "E-Mail-Adresse des Organisators im Buchungsbildschirm, in E-Mail-Benachrichtigungen und Kalenderereignissen verbergen",
"you_and_conjunction": "Sie &",
"select_verified_email": "Verifizierte E-Mail-Adresse auswählen",
"add_verified_email": "Verifizierte E-Mail hinzufügen",
"add_verified_emails": "Verifizierte E-Mails hinzufügen",
"custom_reply_to_email_title": "Benutzerdefinierte 'Antwort an'-E-Mail",
"custom_reply_to_email_description": "Verwende eine andere E-Mail-Adresse als 'Antwort an' für Bestätigungs-E-Mails anstelle der E-Mail-Adresse des Organisators",
"email_survey_triggered_by_workflow": "Diese Umfrage wurde durch einen Workflow in Cal ausgelöst.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Weitere Nachricht senden",
"sending": "Wird gesendet",
"no_variables_found": "Keine Variablen gefunden",
"credits_required": "Credits erforderlich",
"web_call_credits_info": "Web-Anrufe verbrauchen Credits. Stellen Sie sicher, dass Sie genügend Credits haben, um einen Anruf zu starten.",
"web_call_no_credits": "Web-Anrufe erfordern Credits. Sie haben 0 Credits übrig.",
"purchase_credits": "Credits kaufen",
"credits_required_tooltip": "Diese Aktion erfordert Credits, um fortzufahren",
"api_key_name_too_long": "Der Name darf {{max}} Zeichen nicht überschreiten",
"auto_charge_for_last_minute_cancellation": "Automatische Berechnung der Ausfallgebühr für kurzfristige Stornierungen",
"before_scheduled_start_time": "Vor dem geplanten Startzeitpunkt",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Ο οργανισμός σας αναβαθμίστηκε με επιτυχία!",
"use_link_to_reset_password": "Χρησιμοποιήστε τον παρακάτω σύνδεσμο για να επαναφέρετε τον κωδικό πρόσβασης",
"hey_there": "Γεια σας,",
"there": "εκεί",
"forgot_your_password_calcom": "Ξεχάσατε τον κωδικό πρόσβασης? - {{appName}}",
"delete_webhook_confirmation_message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το webhook; Δεν θα λαμβάνετε πλέον δεδομένα συναντήσεων του {{appName}} σε συγκεκριμένο URL, σε πραγματικό χρόνο, όταν προγραμματίζεται ή ακυρώνεται μια εκδήλωση.",
"confirm_delete_webhook": "Ναι, διαγραφή webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Αριθμός τηλεφώνου (για κλήση από τον πράκτορά μας)",
"buy_a_phone_number_or_import_one_you_already_have": "Αγοράστε έναν αριθμό τηλεφώνου ή εισάγετε έναν που έχετε ήδη στο twilio",
"test_agent": "Δοκιμή πράκτορα",
"test_web_call": "Δοκιμή διαδικτυακής κλήσης",
"phone_numbers": "Αριθμοί τηλεφώνου",
"unsubscribe": "Κατάργηση εγγραφής",
"delete_workflow_step": "Διαγραφή βήματος ροής εργασίας",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Η εφαρμογή αφαιρέθηκε με επιτυχία",
"error_removing_app": "Σφάλμα κατά την αφαίρεση της εφαρμογής",
"web_conference": "Διαδικτυακή διάσκεψη",
"web_call": "Διαδικτυακή κλήση",
"start_web_call": "Έναρξη διαδικτυακής κλήσης",
"end_call": "Τερματισμός κλήσης",
"test_your_agent_with_web_call": "Δοκιμάστε τον πράκτορά σας με μια διαδικτυακή κλήση χωρίς να απαιτείται αριθμός τηλεφώνου",
"phone_call": "Τηλεφωνική κλήση",
"connecting_to_agent": "Σύνδεση με τον πράκτορα...",
"call_active": "Ενεργή κλήση",
"call_ended": "Η κλήση τερματίστηκε",
"call_error": "Σφάλμα κλήσης",
"ready_to_start_call": "Έτοιμοι για έναρξη κλήσης",
"failed_to_start_web_call_try_again": "Αποτυχία έναρξης διαδικτυακής κλήσης. Παρακαλώ δοκιμάστε ξανά.",
"call_encountered_error_try_again": "Η κλήση αντιμετώπισε σφάλμα. Παρακαλώ δοκιμάστε ξανά.",
"failed_initialize_web_call_microphone_permissions": "Αποτυχία αρχικοποίησης διαδικτυακής κλήσης. Βεβαιωθείτε ότι έχουν παραχωρηθεί δικαιώματα μικροφώνου.",
"start_call_to_see_conversation": "Ξεκινήστε μια κλήση για να δείτε τη συνομιλία",
"waiting_for_conversation": "Αναμονή για συνομιλία...",
"ai_agent": "Πράκτορας AI",
"live": "Ζωντανά",
"connecting": "Σύνδεση...",
"requires_confirmation": "Απαιτεί επιβεβαίωση",
"requires_confirmation_threshold": "Απαιτεί επιβεβαίωση εάν η κράτηση γίνει με < {{time}} $t({{unit}}_timeUnit) ειδοποίηση",
"may_require_confirmation": "Ενδέχεται να απαιτεί επιβεβαίωση",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Οι απομαγνητοφωνήσεις ενεργοποιήθηκαν",
"transcription_stopped": "Οι απομαγνητοφωνήσεις σταμάτησαν",
"download_transcript": "Λήψη απομαγνητοφώνησης",
"transcript": "Απομαγνητοφώνηση",
"recording_from_your_recent_call": "Μια εγγραφή από την πρόσφατη κλήση σας στο {{appName}} είναι έτοιμη για λήψη",
"transcript_from_previous_call": "Η απομαγνητοφώνηση από την πρόσφατη κλήση σας στο {{appName}} είναι έτοιμη για λήψη. Οι σύνδεσμοι ισχύουν μόνο για 1 ώρα",
"you_can_download_transcript_from_attachments": "Μπορείτε επίσης να κατεβάσετε το αντίγραφο από τα συνημμένα",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Απόκρυψη της διεύθυνσης email του διοργανωτή από την οθόνη κράτησης, τις ειδοποιήσεις email και τα συμβάντα ημερολογίου",
"you_and_conjunction": "Εσείς &",
"select_verified_email": "Επιλογή επαληθευμένου email",
"add_verified_email": "Προσθήκη επαληθευμένου email",
"add_verified_emails": "Προσθήκη επαληθευμένων email",
"custom_reply_to_email_title": "Προσαρμοσμένο email 'Απάντησης-Προς'",
"custom_reply_to_email_description": "Χρησιμοποιήστε διαφορετική διεύθυνση email ως replyTo για τα email επιβεβαίωσης αντί για το email του διοργανωτή",
"email_survey_triggered_by_workflow": "Αυτή η έρευνα ενεργοποιήθηκε από μια ροή εργασίας στο Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Αποστολή άλλου μηνύματος",
"sending": "Αποστολή",
"no_variables_found": "Δεν βρέθηκαν μεταβλητές",
"credits_required": "Απαιτούνται μονάδες",
"web_call_credits_info": "Οι διαδικτυακές κλήσεις καταναλώνουν μονάδες. Βεβαιωθείτε ότι έχετε αρκετές μονάδες για να ξεκινήσετε μια κλήση.",
"web_call_no_credits": "Οι διαδικτυακές κλήσεις απαιτούν μονάδες. Έχετε 0 μονάδες που απομένουν.",
"purchase_credits": "Αγορά μονάδων",
"credits_required_tooltip": "Αυτή η ενέργεια απαιτεί μονάδες για να προχωρήσει",
"api_key_name_too_long": "Το όνομα δεν πρέπει να υπερβαίνει τους {{max}} χαρακτήρες",
"auto_charge_for_last_minute_cancellation": "Αυτόματη χρέωση τέλους μη εμφάνισης για ακυρώσεις τελευταίας στιγμής",
"before_scheduled_start_time": "Πριν την προγραμματισμένη ώρα έναρξης",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Your Organization was upgraded successfully!",
"use_link_to_reset_password": "Use the link below to reset your password",
"hey_there": "Hey there",
"there": "there",
"forgot_your_password_calcom": "Forgot your password? - {{appName}}",
"delete_webhook_confirmation_message": "Are you sure you want to delete this webhook? You will no longer receive {{appName}} meeting data at a specified URL, in real-time, when an event is scheduled or canceled.",
"confirm_delete_webhook": "Yes, delete webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Phone number (for our agent to call)",
"buy_a_phone_number_or_import_one_you_already_have": "Buy a phone number or import one you already have on twilio",
"test_agent": "Test Agent",
"test_web_call": "Test Web Call",
"phone_numbers": "Phone Numbers",
"unsubscribe": "Unsubscribe",
"delete_workflow_step": "Delete Workflow Step",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "App removed successfully",
"error_removing_app": "Error removing app",
"web_conference": "Web conference",
"web_call": "Web call",
"start_web_call": "Start web call",
"end_call": "End call",
"test_your_agent_with_web_call": "Test your agent with a web call without requiring a phone number",
"phone_call": "Phone call",
"connecting_to_agent": "Connecting to agent...",
"call_active": "Call active",
"call_ended": "Call ended",
"call_error": "Call error",
"ready_to_start_call": "Ready to start call",
"failed_to_start_web_call_try_again": "Failed to start web call. Please try again.",
"call_encountered_error_try_again": "Call encountered an error. Please try again.",
"failed_initialize_web_call_microphone_permissions": "Failed to initialize web call. Please ensure microphone permissions are granted.",
"start_call_to_see_conversation": "Start a call to see the conversation",
"waiting_for_conversation": "Waiting for conversation...",
"ai_agent": "AI Agent",
"live": "Live",
"connecting": "Connecting...",
"requires_confirmation": "Requires confirmation",
"requires_confirmation_threshold": "Requires confirmation if booked with < {{time}} $t({{unit}}_timeUnit) notice",
"may_require_confirmation": "May require confirmation",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transcriptions are enabled now",
"transcription_stopped": "Transcriptions are stopped now",
"download_transcript": "Download Transcript",
"transcript": "Transcript",
"recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download",
"transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour",
"you_can_download_transcript_from_attachments": "You can also download transcript from attachments",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Hide organizer's email address from the booking screen, email notifications, and calendar events",
"you_and_conjunction": "You &",
"select_verified_email": "Select a verified email",
"add_verified_email": "Add verified email",
"add_verified_emails": "Add verified emails",
"custom_reply_to_email_title": "Custom 'Reply-To' email",
"custom_reply_to_email_description": "Use a different email address as the replyTo for confirmation emails instead of the organizer's email",
"email_survey_triggered_by_workflow": "This survey was triggered by a Workflow in Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Send another message",
"sending": "Sending",
"no_variables_found": "No variables found",
"credits_required": "Credits Required",
"web_call_credits_info": "Web calls consume credits. Make sure you have enough credits to start a call.",
"web_call_no_credits": "Web calls require credits. You have 0 credits remaining.",
"purchase_credits": "Purchase Credits",
"credits_required_tooltip": "This action requires credits to proceed",
"api_key_name_too_long": "Name must not exceed {{max}} characters",
"auto_charge_for_last_minute_cancellation": "Auto charge no-show fee for last minute cancellations",
"before_scheduled_start_time": "Before scheduled start time",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "¡Tu Organización se actualizó con éxito!",
"use_link_to_reset_password": "Usa el enlace a continuación para restablecer tu contraseña",
"hey_there": "Hola",
"there": "ahí",
"forgot_your_password_calcom": "¿Olvidaste tu contraseña? - {{appName}}",
"delete_webhook_confirmation_message": "¿Estás seguro de que deseas eliminar este webhook? Ya no recibirás datos de reuniones de {{appName}} en una URL especificada, en tiempo real, cuando se programe o cancele un evento.",
"confirm_delete_webhook": "Sí, eliminar webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Número de teléfono (para que nuestro agente llame)",
"buy_a_phone_number_or_import_one_you_already_have": "Compra un número de teléfono o importa uno que ya tengas en Twilio",
"test_agent": "Probar agente",
"test_web_call": "Probar llamada web",
"phone_numbers": "Números de teléfono",
"unsubscribe": "Dar de baja",
"delete_workflow_step": "Eliminar paso del flujo de trabajo",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplicación eliminada con éxito",
"error_removing_app": "Error al eliminar la aplicación",
"web_conference": "Conferencia web",
"web_call": "Llamada web",
"start_web_call": "Iniciar llamada web",
"end_call": "Finalizar llamada",
"test_your_agent_with_web_call": "Prueba tu agente con una llamada web sin necesidad de un número telefónico",
"phone_call": "Llamada telefónica",
"connecting_to_agent": "Conectando con el agente...",
"call_active": "Llamada activa",
"call_ended": "Llamada finalizada",
"call_error": "Error en la llamada",
"ready_to_start_call": "Listo para iniciar la llamada",
"failed_to_start_web_call_try_again": "No se pudo iniciar la llamada web. Por favor, inténtalo de nuevo.",
"call_encountered_error_try_again": "La llamada encontró un error. Por favor, inténtalo de nuevo.",
"failed_initialize_web_call_microphone_permissions": "No se pudo inicializar la llamada web. Por favor, asegúrate de otorgar permisos de micrófono.",
"start_call_to_see_conversation": "Inicia una llamada para ver la conversación",
"waiting_for_conversation": "Esperando conversación...",
"ai_agent": "Agente de IA",
"live": "En vivo",
"connecting": "Conectando...",
"requires_confirmation": "Requiere confirmación",
"requires_confirmation_threshold": "Requiere confirmación si se reserva con menos de {{time}} $t({{unit}}_timeUnit) de antelación",
"may_require_confirmation": "Puede requerir confirmación",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Las transcripciones están habilitadas ahora",
"transcription_stopped": "Las transcripciones se han detenido ahora",
"download_transcript": "Descargar transcripción",
"transcript": "Transcripción",
"recording_from_your_recent_call": "Una grabación de tu llamada reciente en {{appName}} está lista para descargar",
"transcript_from_previous_call": "La transcripción de tu llamada reciente en {{appName}} está lista para descargar. Los enlaces son válidos solo por 1 hora",
"you_can_download_transcript_from_attachments": "También puedes descargar la transcripción desde los archivos adjuntos",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Ocultar la dirección de correo electrónico del organizador en la pantalla de reserva, notificaciones por correo electrónico y eventos del calendario",
"you_and_conjunction": "Tú &",
"select_verified_email": "Seleccionar un correo electrónico verificado",
"add_verified_email": "Agregar correo electrónico verificado",
"add_verified_emails": "Agregar correos electrónicos verificados",
"custom_reply_to_email_title": "Correo 'Responder a' personalizado",
"custom_reply_to_email_description": "Usar una dirección de correo electrónico diferente como destinatario de respuesta para los correos de confirmación en lugar del correo del organizador",
"email_survey_triggered_by_workflow": "Esta encuesta se activó mediante un Workflow en Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Enviar otro mensaje",
"sending": "Enviando",
"no_variables_found": "No se encontraron variables",
"credits_required": "Créditos requeridos",
"web_call_credits_info": "Las llamadas web consumen créditos. Asegúrate de tener suficientes créditos para iniciar una llamada.",
"web_call_no_credits": "Las llamadas web requieren créditos. Te quedan 0 créditos.",
"purchase_credits": "Comprar créditos",
"credits_required_tooltip": "Esta acción requiere créditos para continuar",
"api_key_name_too_long": "El nombre no debe exceder los {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automáticamente la tarifa por no presentarse en caso de cancelaciones de último minuto",
"before_scheduled_start_time": "Antes de la hora de inicio programada",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Su Organization se actualizó con éxito.",
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
"hey_there": "Hola",
"there": "ahí",
"forgot_your_password_calcom": "¿Olvidaste tu contraseña? - {{appName}}",
"delete_webhook_confirmation_message": "¿Está seguro de que desea eliminar este webhook? Ya no recibirá datos de reuniones de {{appName}} en una URL específica, en tiempo real, cuando se programe o cancele un evento.",
"confirm_delete_webhook": "Sí, elimina este webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Número de teléfono (para que nuestro agente llame)",
"buy_a_phone_number_or_import_one_you_already_have": "Compra un número de teléfono o importa uno que ya tengas en twilio",
"test_agent": "Probar agente",
"test_web_call": "Probar llamada web",
"phone_numbers": "Números de teléfono",
"unsubscribe": "Dar de baja",
"delete_workflow_step": "Eliminar paso del flujo de trabajo",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplicación eliminada correctamente",
"error_removing_app": "Error al eliminar la aplicación",
"web_conference": "Conferencia web",
"web_call": "Llamada web",
"start_web_call": "Iniciar llamada web",
"end_call": "Finalizar llamada",
"test_your_agent_with_web_call": "Prueba tu agente con una llamada web sin necesidad de un número de teléfono",
"phone_call": "Llamada telefónica",
"connecting_to_agent": "Conectando con el agente...",
"call_active": "Llamada activa",
"call_ended": "Llamada finalizada",
"call_error": "Error en la llamada",
"ready_to_start_call": "Listo para iniciar la llamada",
"failed_to_start_web_call_try_again": "No se pudo iniciar la llamada web. Por favor, inténtalo de nuevo.",
"call_encountered_error_try_again": "La llamada encontró un error. Por favor, inténtalo de nuevo.",
"failed_initialize_web_call_microphone_permissions": "No se pudo inicializar la llamada web. Asegúrate de que los permisos del micrófono estén concedidos.",
"start_call_to_see_conversation": "Inicia una llamada para ver la conversación",
"waiting_for_conversation": "Esperando conversación...",
"ai_agent": "Agente de IA",
"live": "En vivo",
"connecting": "Conectando...",
"requires_confirmation": "Requiere confirmación",
"requires_confirmation_threshold": "Requiere confirmación si se reservó con < {{time}} $t({{unit}}_timeUnit) de aviso",
"may_require_confirmation": "Puede requerir confirmación",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Las transcripciones están habilitadas ahora",
"transcription_stopped": "Las transcripciones están detenidas ahora",
"download_transcript": "Descargar transcripción",
"transcript": "Transcripción",
"recording_from_your_recent_call": "Una grabación de su llamada reciente en {{appName}} está lista para descargar",
"transcript_from_previous_call": "La transcripción de su llamada reciente en {{appName}} está lista para descargar. Los enlaces son válidos solo por 1 hora.",
"you_can_download_transcript_from_attachments": "También puedes descargar la transcripción desde los archivos adjuntos",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Ocultar la dirección de correo electrónico del organizador en la pantalla de reserva, notificaciones por correo electrónico y eventos del calendario",
"you_and_conjunction": "Tú &",
"select_verified_email": "Seleccionar un correo electrónico verificado",
"add_verified_email": "Añadir correo electrónico verificado",
"add_verified_emails": "Añadir correos electrónicos verificados",
"custom_reply_to_email_title": "Correo electrónico personalizado para 'Responder a'",
"custom_reply_to_email_description": "Usar una dirección de correo electrónico diferente como destinatario de respuesta para los correos de confirmación en lugar del correo del organizador",
"email_survey_triggered_by_workflow": "Esta encuesta fue activada por un Workflow en Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Enviar otro mensaje",
"sending": "Enviando",
"no_variables_found": "No se encontraron variables",
"credits_required": "Créditos requeridos",
"web_call_credits_info": "Las llamadas web consumen créditos. Asegúrate de tener suficientes créditos para iniciar una llamada.",
"web_call_no_credits": "Las llamadas web requieren créditos. Te quedan 0 créditos.",
"purchase_credits": "Comprar créditos",
"credits_required_tooltip": "Esta acción requiere créditos para continuar",
"api_key_name_too_long": "El nombre no debe exceder los {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automáticamente la tarifa por no presentarse en caso de cancelaciones de último minuto",
"before_scheduled_start_time": "Antes de la hora de inicio programada",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Teie organisatsiooni uuendamine õnnestus!",
"use_link_to_reset_password": "Kasutage oma parooli lähtestamiseks allolevat linki",
"hey_there": "Tere",
"there": "seal",
"forgot_your_password_calcom": "Unustasite oma parooli? - {{appName}}",
"delete_webhook_confirmation_message": "Kas soovite kindlasti selle veebihaagi kustutada? Kui üritus on ajastatud või tühistatud, ei saa te enam reaalajas {{appName}} koosoleku andmeid määratud URL-il.",
"confirm_delete_webhook": "Jah, kustuta veebihaak",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefoninumber (millele meie agent helistab)",
"buy_a_phone_number_or_import_one_you_already_have": "Osta telefoninumber või impordi juba olemasolev Twilio number",
"test_agent": "Testi agenti",
"test_web_call": "Testi veebikõnet",
"phone_numbers": "Telefoninumbrid",
"unsubscribe": "Tühista tellimus",
"delete_workflow_step": "Kustuta töövoo samm",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Rakendus on edukalt eemaldatud",
"error_removing_app": "Viga rakenduse eemaldamisel",
"web_conference": "Veebikonverents",
"web_call": "Veebikõne",
"start_web_call": "Alusta veebikõnet",
"end_call": "Lõpeta kõne",
"test_your_agent_with_web_call": "Testi oma agenti veebikõnega ilma telefoninumbrit vajamata",
"phone_call": "Telefonikõne",
"connecting_to_agent": "Ühendamine agendiga...",
"call_active": "Kõne aktiivne",
"call_ended": "Kõne lõpetatud",
"call_error": "Kõne viga",
"ready_to_start_call": "Valmis kõnet alustama",
"failed_to_start_web_call_try_again": "Veebikõne alustamine ebaõnnestus. Palun proovi uuesti.",
"call_encountered_error_try_again": "Kõnes tekkis viga. Palun proovi uuesti.",
"failed_initialize_web_call_microphone_permissions": "Veebikõne initsialiseerimine ebaõnnestus. Palun veendu, et mikrofonilubad on antud.",
"start_call_to_see_conversation": "Alusta kõnet, et vestlust näha",
"waiting_for_conversation": "Ootan vestlust...",
"ai_agent": "Tehisintellekti agent",
"live": "Reaalajas",
"connecting": "Ühendamine...",
"requires_confirmation": "Nõuab kinnitust",
"requires_confirmation_threshold": "Nõuab kinnitust, kui broneering on < {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Võib nõuda kinnitust",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transkriptsioonid on nüüd lubatud",
"transcription_stopped": "Transkriptsioonid on nüüd peatatud",
"download_transcript": "Laadi alla transkript",
"transcript": "Transkript",
"recording_from_your_recent_call": "Teie hiljutise kõne salvestis rakenduses {{appName}} on allalaadimiseks valmis",
"transcript_from_previous_call": "Teie hiljutise kõne transkriptsioon rakenduses {{appName}} on allalaadimiseks valmis. Lingid kehtivad ainult 1 tund",
"you_can_download_transcript_from_attachments": "Transkriptsiooni saate alla laadida ka manustest",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Peida korraldaja e-posti aadress broneerimisekraanil, e-posti teavitustes ja kalendrisündmustes.",
"you_and_conjunction": "Sina &",
"select_verified_email": "Vali kinnitatud e-posti aadress",
"add_verified_email": "Lisa kinnitatud e-post",
"add_verified_emails": "Lisa kinnitatud e-posti aadressid",
"custom_reply_to_email_title": "Kohandatud 'Vasta-aadress' e-post",
"custom_reply_to_email_description": "Kasutage kinnituskirjade jaoks vastuse aadressina teist e-posti aadressi, mitte korraldaja e-posti aadressi.",
"email_survey_triggered_by_workflow": "See küsitlus käivitati Cali töövooga.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Saada uus sõnum",
"sending": "Saatmine",
"no_variables_found": "Muutujaid ei leitud",
"credits_required": "Vajalikud krediidid",
"web_call_credits_info": "Veebikõned tarbivad krediite. Veendu, et sul oleks piisavalt krediite kõne alustamiseks.",
"web_call_no_credits": "Veebikõned nõuavad krediite. Sul on jäänud 0 krediiti.",
"purchase_credits": "Osta krediite",
"credits_required_tooltip": "Selle toimingu jätkamiseks on vaja krediite.",
"api_key_name_too_long": "Nimi ei tohi ületada {{max}} tähemärki",
"auto_charge_for_last_minute_cancellation": "Viimase hetke tühistamiste eest puudumise tasu automaatne arvestamine",
"before_scheduled_start_time": "Enne planeeritud algusaega",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Zure erakundea arrakastaz eguneratu da!",
"use_link_to_reset_password": "Erabili beheko esteka pasahitza berrezartzeko",
"hey_there": "Kaixo,",
"there": "han",
"forgot_your_password_calcom": "Pasahitza ahaztu duzu? - {{appName}}",
"delete_webhook_confirmation_message": "Ziur zaude webhook hau ezabatu nahi duzula? Ez duzu gehiago {{appName}} bileraren daturik jasoko zehaztutako URLan denbora errealean, gertaera bat programatzen edo bertan behera uzten denean.",
"confirm_delete_webhook": "Bai, ezabatu webhooka",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefono zenbakia (gure agenteak deitzeko)",
"buy_a_phone_number_or_import_one_you_already_have": "Erosi telefono zenbaki bat edo inportatu dagoeneko twilio-n duzun bat",
"test_agent": "Probatu agentea",
"test_web_call": "Web deia probatu",
"phone_numbers": "Telefono zenbakiak",
"unsubscribe": "Harpidetza kendu",
"delete_workflow_step": "Ezabatu lan-fluxu urratsa",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplikazioa behar bezala kendu da",
"error_removing_app": "Errorea aplikazioa kentzean",
"web_conference": "Web konferentzia",
"web_call": "Web deia",
"start_web_call": "Hasi web deia",
"end_call": "Amaitu deia",
"test_your_agent_with_web_call": "Probatu zure agentea web dei batekin telefono zenbakirik behar izan gabe",
"phone_call": "Telefono deia",
"connecting_to_agent": "Agentearekiko konexioa egiten...",
"call_active": "Deia aktibo",
"call_ended": "Deia amaituta",
"call_error": "Dei errorea",
"ready_to_start_call": "Deia hasteko prest",
"failed_to_start_web_call_try_again": "Ezin izan da web deia hasi. Saiatu berriro.",
"call_encountered_error_try_again": "Deiak errore bat izan du. Saiatu berriro.",
"failed_initialize_web_call_microphone_permissions": "Ezin izan da web deia hasieratu. Ziurtatu mikrofonoaren baimenak emanda daudela.",
"start_call_to_see_conversation": "Hasi dei bat elkarrizketa ikusteko",
"waiting_for_conversation": "Elkarrizketaren zain...",
"ai_agent": "IA Agentea",
"live": "Zuzenean",
"connecting": "Konektatzen...",
"requires_confirmation": "Baieztapena behar du",
"requires_confirmation_threshold": "Baieztapena behar du < {{time}} $t({{unit}}_timeUnit) aurretiaz erreserbatuz gero",
"may_require_confirmation": "Baieztapena behar izan dezake",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transkripzioak aktibatuta daude orain",
"transcription_stopped": "Transkripzioak gelditu dira orain",
"download_transcript": "Deskargatu transkripzioa",
"transcript": "Transkripzioa",
"recording_from_your_recent_call": "{{appName}}-en egin berri duzun deiaren grabaketa deskargatzeko prest dago",
"transcript_from_previous_call": "{{appName}}-en egin berri duzun deiaren transkripzioa deskargatzeko prest dago. Estekak ordubetez bakarrik dira baliozkoak",
"you_can_download_transcript_from_attachments": "Transkripzioa eranskinetan ere deskarga dezakezu",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Ezkutatu antolatzailearen helbide elektronikoa erreserba pantailatik, mezu elektronikoetatik eta egutegiko ekitaldietatik",
"you_and_conjunction": "Zu &",
"select_verified_email": "Hautatu egiaztatutako helbide elektronikoa",
"add_verified_email": "Gehitu egiaztatutako helbide elektronikoa",
"add_verified_emails": "Gehitu egiaztatutako helbide elektronikoak",
"custom_reply_to_email_title": "'Erantzun' helbide elektroniko pertsonalizatua",
"custom_reply_to_email_description": "Erabili beste helbide elektroniko bat baieztapen mezuen erantzun-helbide gisa antolatzailearen helbidearen ordez",
"email_survey_triggered_by_workflow": "Galdeketa hau Cal-eko Workflow baten bidez aktibatu da.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Bidali beste mezu bat",
"sending": "Bidaltzen",
"no_variables_found": "Ez da aldagairik aurkitu",
"credits_required": "Kredituak beharrezkoak",
"web_call_credits_info": "Web deiek kredituak kontsumitzen dituzte. Ziurtatu kreditu nahikoa duzula dei bat hasteko.",
"web_call_no_credits": "Web deiek kredituak behar dituzte. 0 kreditu geratzen zaizkizu.",
"purchase_credits": "Erosi kredituak",
"credits_required_tooltip": "Ekintza honek kredituak behar ditu aurrera egiteko",
"api_key_name_too_long": "Izenak ezin ditu {{max}} karaktere baino gehiago izan",
"auto_charge_for_last_minute_cancellation": "Azken minutuko ezeztapenengatik ez agertzeko tasa automatikoki kobratu",
"before_scheduled_start_time": "Programatutako hasiera ordua baino lehen",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organisaatiosi päivitys onnistui!",
"use_link_to_reset_password": "Käytä alla olevaa linkkiä salasanan vaihtamiseen",
"hey_there": "Hei siellä",
"there": "siellä",
"forgot_your_password_calcom": "Unohditko salasanasi? - {{appName}}",
"delete_webhook_confirmation_message": "Haluatko varmasti poistaa tämän webhookin? Et enää vastaanota {{appName}}-kokoustietoja määritettyyn URL-osoitteeseen reaaliajassa, kun tapahtuma aikataulutetaan tai peruutetaan.",
"confirm_delete_webhook": "Kyllä, poista webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Puhelinnumero (johon agenttimme soittaa)",
"buy_a_phone_number_or_import_one_you_already_have": "Osta puhelinnumero tai tuo jo Twiliossa oleva numerosi",
"test_agent": "Testaa agentti",
"test_web_call": "Testaa verkkopuhelu",
"phone_numbers": "Puhelinnumerot",
"unsubscribe": "Peruuta tilaus",
"delete_workflow_step": "Poista työnkulun vaihe",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Sovellus poistettu onnistuneesti",
"error_removing_app": "Virhe sovelluksen poistamisessa",
"web_conference": "Verkkokokous",
"web_call": "Verkkopuhelu",
"start_web_call": "Aloita verkkopuhelu",
"end_call": "Lopeta puhelu",
"test_your_agent_with_web_call": "Testaa agenttisi verkkopuhelulla ilman puhelinnumeroa",
"phone_call": "Puhelinkeskustelu",
"connecting_to_agent": "Yhdistetään agenttiin...",
"call_active": "Puhelu aktiivinen",
"call_ended": "Puhelu päättyi",
"call_error": "Puheluvirhe",
"ready_to_start_call": "Valmis aloittamaan puhelun",
"failed_to_start_web_call_try_again": "Verkkopuhelun aloittaminen epäonnistui. Yritä uudelleen.",
"call_encountered_error_try_again": "Puhelussa tapahtui virhe. Yritä uudelleen.",
"failed_initialize_web_call_microphone_permissions": "Verkkopuhelun alustaminen epäonnistui. Varmista, että mikrofonin käyttöoikeudet on myönnetty.",
"start_call_to_see_conversation": "Aloita puhelu nähdäksesi keskustelun",
"waiting_for_conversation": "Odotetaan keskustelua...",
"ai_agent": "Tekoälyagentti",
"live": "Suora",
"connecting": "Yhdistetään...",
"requires_confirmation": "Vaatii vahvistuksen",
"requires_confirmation_threshold": "Vaatii vahvistuksen, jos varattu alle {{time}} $t({{unit}}_timeUnit) varoitusajalla",
"may_require_confirmation": "Saattaa vaatia vahvistuksen",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Litterointi on nyt käytössä",
"transcription_stopped": "Litterointi on nyt pysäytetty",
"download_transcript": "Lataa litterointi",
"transcript": "Litterointi",
"recording_from_your_recent_call": "Tallenne viimeisimmästä {{appName}}-puhelustasi on valmis ladattavaksi",
"transcript_from_previous_call": "Litterointi viimeisimmästä {{appName}}-puhelustasi on valmis ladattavaksi. Linkit ovat voimassa vain tunnin",
"you_can_download_transcript_from_attachments": "Voit myös ladata muistion liitteistä",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Piilota järjestäjän sähköpostiosoite varausnäytöltä, sähköposti-ilmoituksista ja kalenteritapahtumista",
"you_and_conjunction": "Sinä &",
"select_verified_email": "Valitse vahvistettu sähköposti",
"add_verified_email": "Lisää vahvistettu sähköposti",
"add_verified_emails": "Lisää vahvistettuja sähköposteja",
"custom_reply_to_email_title": "Mukautettu 'Vastaa'-sähköposti",
"custom_reply_to_email_description": "Käytä eri sähköpostiosoitetta vastausosoitteena vahvistussähköposteissa järjestäjän sähköpostin sijaan",
"email_survey_triggered_by_workflow": "Tämä kysely käynnistettiin Workflow-ohjelmalla Cal-palvelussa.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Lähetä toinen viesti",
"sending": "Lähetetään",
"no_variables_found": "Muuttujia ei löytynyt",
"credits_required": "Krediittejä vaaditaan",
"web_call_credits_info": "Verkkopuhelut kuluttavat krediittejä. Varmista, että sinulla on tarpeeksi krediittejä puhelun aloittamiseen.",
"web_call_no_credits": "Verkkopuhelut vaativat krediittejä. Sinulla on 0 krediittiä jäljellä.",
"purchase_credits": "Osta krediittejä",
"credits_required_tooltip": "Tämä toiminto vaatii krediittejä jatkaaksesi",
"api_key_name_too_long": "Nimi ei saa ylittää {{max}} merkkiä",
"auto_charge_for_last_minute_cancellation": "Veloita automaattisesti peruutusmaksu viime hetken peruutuksista",
"before_scheduled_start_time": "Ennen sovittua aloitusaikaa",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Votre organisation a été mise à niveau avec succès !",
"use_link_to_reset_password": "Utilisez le lien ci-dessous pour réinitialiser votre mot de passe",
"hey_there": "Bonjour",
"there": "là",
"forgot_your_password_calcom": "Mot de passe oublié ? - {{appName}}",
"delete_webhook_confirmation_message": "Voulez-vous vraiment supprimer ce webhook ? Vous ne recevrez plus les données de rendez-vous de {{appName}} à un lien spécifié, en temps réel, lorsqu'un événement est planifié ou annulé.",
"confirm_delete_webhook": "Oui, supprimer le webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Numéro de téléphone (pour que notre agent puisse appeler)",
"buy_a_phone_number_or_import_one_you_already_have": "Achetez un numéro de téléphone ou importez-en un que vous possédez déjà sur Twilio",
"test_agent": "Tester l'agent",
"test_web_call": "Tester l'appel web",
"phone_numbers": "Numéros de téléphone",
"unsubscribe": "Se désabonner",
"delete_workflow_step": "Supprimer l'étape du workflow",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Application supprimée avec succès",
"error_removing_app": "Erreur lors de la suppression de l'application",
"web_conference": "Visioconférence",
"web_call": "Appel web",
"start_web_call": "Démarrer l'appel web",
"end_call": "Terminer l'appel",
"test_your_agent_with_web_call": "Testez votre agent avec un appel web sans nécessiter de numéro de téléphone",
"phone_call": "Appel téléphonique",
"connecting_to_agent": "Connexion à l'agent...",
"call_active": "Appel en cours",
"call_ended": "Appel terminé",
"call_error": "Erreur d'appel",
"ready_to_start_call": "Prêt à démarrer l'appel",
"failed_to_start_web_call_try_again": "Échec du démarrage de l'appel web. Veuillez réessayer.",
"call_encountered_error_try_again": "L'appel a rencontré une erreur. Veuillez réessayer.",
"failed_initialize_web_call_microphone_permissions": "Échec d'initialisation de l'appel web. Veuillez vous assurer que les permissions du microphone sont accordées.",
"start_call_to_see_conversation": "Démarrez un appel pour voir la conversation",
"waiting_for_conversation": "En attente de conversation...",
"ai_agent": "Agent IA",
"live": "En direct",
"connecting": "Connexion en cours...",
"requires_confirmation": "Nécessite une confirmation",
"requires_confirmation_threshold": "Une confirmation est requise si réservé avec un préavis de moins de {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Peut nécessiter une confirmation",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Les transcriptions sont désormais activées",
"transcription_stopped": "Les transcriptions sont désormais arrêtées",
"download_transcript": "Télécharger la transcription",
"transcript": "Transcription",
"recording_from_your_recent_call": "Un enregistrement de votre appel récent sur {{appName}} est prêt à être téléchargé",
"transcript_from_previous_call": "La transcription de votre récent appel sur {{appName}} est prête à être téléchargée. Les liens ne sont valides que pendant 1 heure",
"you_can_download_transcript_from_attachments": "Vous pouvez également télécharger la transcription depuis les pièces jointes",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Masquer l'adresse e-mail de l'organisateur sur l'écran de réservation, les notifications par e-mail et les événements du calendrier",
"you_and_conjunction": "Vous &",
"select_verified_email": "Sélectionner un e-mail vérifié",
"add_verified_email": "Ajouter une adresse e-mail vérifiée",
"add_verified_emails": "Ajouter des adresses e-mail vérifiées",
"custom_reply_to_email_title": "E-mail 'Répondre à' personnalisé",
"custom_reply_to_email_description": "Utiliser une adresse e-mail différente comme destinataire de réponse pour les e-mails de confirmation au lieu de l'e-mail de l'organisateur",
"email_survey_triggered_by_workflow": "Ce questionnaire a été déclenché par un Workflow dans Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Envoyer un autre message",
"sending": "Envoi en cours",
"no_variables_found": "Aucune variable trouvée",
"credits_required": "Crédits requis",
"web_call_credits_info": "Les appels web consomment des crédits. Assurez-vous d'avoir suffisamment de crédits pour démarrer un appel.",
"web_call_no_credits": "Les appels web nécessitent des crédits. Il vous reste 0 crédit.",
"purchase_credits": "Acheter des crédits",
"credits_required_tooltip": "Cette action nécessite des crédits pour continuer",
"api_key_name_too_long": "Le nom ne doit pas dépasser {{max}} caractères",
"auto_charge_for_last_minute_cancellation": "Facturer automatiquement des frais de non-présentation pour les annulations de dernière minute",
"before_scheduled_start_time": "Avant l'heure de début prévue",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "ה-Organization שלך שודרג בהצלחה!",
"use_link_to_reset_password": "נא להשתמש בקישור הבא כדי לאפס את הסיסמה",
"hey_there": "היי שם",
"there": "שם",
"forgot_your_password_calcom": "שכחתה את הסיסמה? - {{appName}}",
"delete_webhook_confirmation_message": "בטוח שברצונך למחוק את ה-Webhook הזה? אם אירוע יבוטל או שייקבע עבורו מועד, לא תקבל יותר נתוני פגישות מ-{{appName}} בזמן אמת או בכתובת ה-URL שציינת.",
"confirm_delete_webhook": "כן, אני רוצה למחוק את ה-Webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "מספר טלפון (עבור הסוכן שלנו להתקשר אליו)",
"buy_a_phone_number_or_import_one_you_already_have": "קנה מספר טלפון או ייבא אחד שכבר יש לך ב-Twilio",
"test_agent": "בדוק סוכן",
"test_web_call": "בדיקת שיחת אינטרנט",
"phone_numbers": "מספרי טלפון",
"unsubscribe": "בטל מנוי",
"delete_workflow_step": "מחק שלב בתהליך העבודה",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "האפליקציה הוסרה בהצלחה",
"error_removing_app": "שגיאה בהסרת האפליקציה",
"web_conference": "שיחת ועידה מקוונת",
"web_call": "שיחת אינטרנט",
"start_web_call": "התחל שיחת אינטרנט",
"end_call": "סיים שיחה",
"test_your_agent_with_web_call": "בדוק את הסוכן שלך עם שיחת אינטרנט ללא צורך במספר טלפון",
"phone_call": "שיחת טלפון",
"connecting_to_agent": "מתחבר לסוכן...",
"call_active": "שיחה פעילה",
"call_ended": "השיחה הסתיימה",
"call_error": "שגיאת שיחה",
"ready_to_start_call": "מוכן להתחיל שיחה",
"failed_to_start_web_call_try_again": "נכשל בהתחלת שיחת אינטרנט. אנא נסה שוב.",
"call_encountered_error_try_again": "השיחה נתקלה בשגיאה. אנא נסה שוב.",
"failed_initialize_web_call_microphone_permissions": "נכשל באתחול שיחת אינטרנט. אנא ודא שהרשאות המיקרופון מאושרות.",
"start_call_to_see_conversation": "התחל שיחה כדי לראות את השיחה",
"waiting_for_conversation": "ממתין לשיחה...",
"ai_agent": "סוכן בינה מלאכותית",
"live": "חי",
"connecting": "מתחבר...",
"requires_confirmation": "נדרש אישור",
"requires_confirmation_threshold": "מחייב אישור אם ההזמנה בוצעה פחות מ-{{time}} $t({{unit}}_timeUnit) מראש",
"may_require_confirmation": "עשוי לחייב אישור",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "תמלולים פעילים כרגע",
"transcription_stopped": "תמלולים מופסקים כרגע",
"download_transcript": "הורדת תמלול",
"transcript": "תמליל",
"recording_from_your_recent_call": "הקלטה של שיחה שערכת לאחרונה ב-{{appName}} מוכנה להורדה",
"transcript_from_previous_call": "תמלול השיחה האחרונה שלך ב־{{appName}} זמין להורדה. קישורים תקפים למשך שעה בלבד",
"you_can_download_transcript_from_attachments": "ניתן גם להוריד את התמליל מהקבצים המצורפים",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "הסתר את כתובת האימייל של המארגן ממסך ההזמנה, התראות האימייל ואירועי היומן",
"you_and_conjunction": "את/ה &",
"select_verified_email": "בחר אימייל מאומת",
"add_verified_email": "הוסף אימייל מאומת",
"add_verified_emails": "הוסף אימיילים מאומתים",
"custom_reply_to_email_title": "אימייל 'השב-אל' מותאם אישית",
"custom_reply_to_email_description": "השתמש בכתובת אימייל אחרת כ-replyTo עבור אימיילי אישור במקום האימייל של המארגן",
"email_survey_triggered_by_workflow": "הסקר הזה הופעל על ידי Workflow ב-Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "שלח הודעה נוספת",
"sending": "שולח",
"no_variables_found": "לא נמצאו משתנים",
"credits_required": "נדרשים קרדיטים",
"web_call_credits_info": "שיחות אינטרנט צורכות קרדיטים. ודא שיש לך מספיק קרדיטים להתחלת שיחה.",
"web_call_no_credits": "שיחות אינטרנט דורשות קרדיטים. נותרו לך 0 קרדיטים.",
"purchase_credits": "רכוש קרדיטים",
"credits_required_tooltip": "פעולה זו דורשת קרדיטים כדי להמשיך",
"api_key_name_too_long": "השם לא יכול לעלות על {{max}} תווים",
"auto_charge_for_last_minute_cancellation": "חיוב אוטומטי של דמי אי-הופעה עבור ביטולים ברגע האחרון",
"before_scheduled_start_time": "לפני זמן ההתחלה המתוכנן",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Szervezete sikeresen frissítve!",
"use_link_to_reset_password": "Jelszó visszaállításához használja az alábbi linket",
"hey_there": "Szia",
"there": "ott",
"forgot_your_password_calcom": "Elfelejtette a jelszavát? - {{appName}}",
"delete_webhook_confirmation_message": "Biztosan törli ezt a webhookot? A továbbiakban nem kapja meg valós időben a(z) {{appName}} megbeszélés adatait egy megadott URL-címen, ha egy esemény ütemezett vagy lemondott.",
"confirm_delete_webhook": "Igen, webhook törlése",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefonszám (amit az ügynökünk hívni fog)",
"buy_a_phone_number_or_import_one_you_already_have": "Vásárolj telefonszámot vagy importálj egyet, amit már használsz a Twilio-n",
"test_agent": "Ügynök tesztelése",
"test_web_call": "Webes hívás tesztelése",
"phone_numbers": "Telefonszámok",
"unsubscribe": "Leiratkozás",
"delete_workflow_step": "Munkafolyamat lépés törlése",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Az alkalmazás sikeresen eltávolítva",
"error_removing_app": "Hiba az alkalmazás eltávolításakor",
"web_conference": "Online konferencia",
"web_call": "Webes hívás",
"start_web_call": "Webes hívás indítása",
"end_call": "Hívás befejezése",
"test_your_agent_with_web_call": "Tesztelje ügynökét webes hívással telefonszám megadása nélkül",
"phone_call": "Telefonhívás",
"connecting_to_agent": "Kapcsolódás az ügynökhöz...",
"call_active": "Hívás aktív",
"call_ended": "Hívás befejezve",
"call_error": "Hívási hiba",
"ready_to_start_call": "Készen áll a hívás indítására",
"failed_to_start_web_call_try_again": "Nem sikerült elindítani a webes hívást. Kérjük, próbálja újra.",
"call_encountered_error_try_again": "A hívás során hiba történt. Kérjük, próbálja újra.",
"failed_initialize_web_call_microphone_permissions": "Nem sikerült inicializálni a webes hívást. Kérjük, ellenőrizze, hogy a mikrofon-hozzáférés engedélyezve van.",
"start_call_to_see_conversation": "Indítson hívást a beszélgetés megtekintéséhez",
"waiting_for_conversation": "Várakozás a beszélgetésre...",
"ai_agent": "MI ügynök",
"live": "Élő",
"connecting": "Kapcsolódás...",
"requires_confirmation": "Megerősítést igényel",
"requires_confirmation_threshold": "Megerősítést igényel, ha < {{time}} $t({{unit}}_timeUnit) értesítéssel foglalt",
"may_require_confirmation": "Megerősítést igényelhet",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Az átírások most engedélyezve vannak",
"transcription_stopped": "Az átírások most le lett állítva",
"download_transcript": "Átirat letöltése",
"transcript": "Átirat",
"recording_from_your_recent_call": "A legutóbbi {{appName}} hívásáról készült felvétel letölthető",
"transcript_from_previous_call": "A(z) {{appName}} nevű legutóbbi hívásának átirata letölthető. A linkek csak 1 óráig érvényesek",
"you_can_download_transcript_from_attachments": "A jegyzőkönyvet letöltheti a csatolmányok közül",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Szervező e-mail címének elrejtése a foglalási képernyőn, e-mail értesítésekben és naptáreseményekben",
"you_and_conjunction": "Te &",
"select_verified_email": "Válasszon egy ellenőrzött e-mail címet",
"add_verified_email": "Hitelesített e-mail hozzáadása",
"add_verified_emails": "Hitelesített e-mailek hozzáadása",
"custom_reply_to_email_title": "Egyéni 'Válaszcím' e-mail",
"custom_reply_to_email_description": "Használjon másik e-mail címet válaszcímként a visszaigazoló e-mailekhez a szervező e-mail címe helyett",
"email_survey_triggered_by_workflow": "Ezt a kérdőívet egy Workflow indította a Cal-ban.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Másik üzenet küldése",
"sending": "Küldés folyamatban",
"no_variables_found": "Nem találhatók változók",
"credits_required": "Kreditek szükségesek",
"web_call_credits_info": "A webes hívások krediteket használnak. Győződjön meg róla, hogy elegendő kredittel rendelkezik a hívás indításához.",
"web_call_no_credits": "A webes hívásokhoz kreditek szükségesek. 0 kredit maradt.",
"purchase_credits": "Kreditek vásárlása",
"credits_required_tooltip": "Ehhez a művelethez kreditek szükségesek",
"api_key_name_too_long": "A név nem haladhatja meg a {{max}} karaktert",
"auto_charge_for_last_minute_cancellation": "Automatikus lemondási díj felszámolása az utolsó pillanatban történő lemondásokért",
"before_scheduled_start_time": "A tervezett kezdési idő előtt",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "È stato eseguito l'upgrade del piano Organization!",
"use_link_to_reset_password": "Usa il link qui sotto per reimpostare la tua password",
"hey_there": "Ciao",
"there": "lì",
"forgot_your_password_calcom": "Hai dimenticato la password? - {{appName}}",
"delete_webhook_confirmation_message": "Sei sicuro di voler eliminare questo webhook? Non riceverai più {{appName}} dati di riunione in un URL specificato, in tempo reale, quando un evento è programmato o annullato.",
"confirm_delete_webhook": "Sì, elimina webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Numero di telefono (che il nostro agente chiamerà)",
"buy_a_phone_number_or_import_one_you_already_have": "Acquista un numero di telefono o importane uno che hai già su Twilio",
"test_agent": "Testa l'agente",
"test_web_call": "Prova chiamata web",
"phone_numbers": "Numeri di telefono",
"unsubscribe": "Disiscriviti",
"delete_workflow_step": "Elimina fase del workflow",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "App rimossa correttamente",
"error_removing_app": "Errore durante la rimozione dell'app",
"web_conference": "Conferenza Web",
"web_call": "Chiamata web",
"start_web_call": "Inizia chiamata web",
"end_call": "Termina chiamata",
"test_your_agent_with_web_call": "Prova il tuo agente con una chiamata web senza richiedere un numero di telefono",
"phone_call": "Chiamata telefonica",
"connecting_to_agent": "Connessione all'agente in corso...",
"call_active": "Chiamata attiva",
"call_ended": "Chiamata terminata",
"call_error": "Errore nella chiamata",
"ready_to_start_call": "Pronto per iniziare la chiamata",
"failed_to_start_web_call_try_again": "Impossibile avviare la chiamata web. Riprova.",
"call_encountered_error_try_again": "La chiamata ha riscontrato un errore. Riprova.",
"failed_initialize_web_call_microphone_permissions": "Impossibile inizializzare la chiamata web. Assicurati che i permessi del microfono siano concessi.",
"start_call_to_see_conversation": "Inizia una chiamata per vedere la conversazione",
"waiting_for_conversation": "In attesa della conversazione...",
"ai_agent": "Agente IA",
"live": "In diretta",
"connecting": "Connessione in corso...",
"requires_confirmation": "Richiede conferma",
"requires_confirmation_threshold": "Richiede conferma se si prenota con un preavviso di < {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Può richiedere conferma",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Le trascrizioni sono attivate",
"transcription_stopped": "Le trascrizioni non sono attive",
"download_transcript": "Scarica trascrizione",
"transcript": "Trascrizione",
"recording_from_your_recent_call": "La registrazione della tua recente chiamata su {{appName}} è pronta per il download",
"transcript_from_previous_call": "La trascrizione dalla tua recente chiamata su {{appName}} è pronta per essere scaricata. I link sono validi solo per 1 ora",
"you_can_download_transcript_from_attachments": "Puoi anche scaricare la trascrizione dagli allegati",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Nascondi l'indirizzo email dell'organizzatore dalla schermata di prenotazione, dalle notifiche email e dagli eventi del calendario",
"you_and_conjunction": "Tu &",
"select_verified_email": "Seleziona un'email verificata",
"add_verified_email": "Aggiungi email verificata",
"add_verified_emails": "Aggiungi email verificate",
"custom_reply_to_email_title": "Email 'Rispondi a' personalizzata",
"custom_reply_to_email_description": "Usa un indirizzo email diverso come replyTo per le email di conferma invece dell'email dell'organizzatore",
"email_survey_triggered_by_workflow": "Questo sondaggio è stato attivato da un Workflow in Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Invia un altro messaggio",
"sending": "Invio in corso",
"no_variables_found": "Nessuna variabile trovata",
"credits_required": "Crediti richiesti",
"web_call_credits_info": "Le chiamate web consumano crediti. Assicurati di avere crediti sufficienti per iniziare una chiamata.",
"web_call_no_credits": "Le chiamate web richiedono crediti. Hai 0 crediti rimanenti.",
"purchase_credits": "Acquista crediti",
"credits_required_tooltip": "Questa azione richiede crediti per procedere",
"api_key_name_too_long": "Il nome non deve superare i {{max}} caratteri",
"auto_charge_for_last_minute_cancellation": "Addebita automaticamente la penale per cancellazioni dell'ultimo minuto",
"before_scheduled_start_time": "Prima dell'orario di inizio programmato",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "あなたの Organization プランは正常にアップグレードされました!",
"use_link_to_reset_password": "以下のリンクを使用してパスワードをリセットしてください",
"hey_there": "こんにちは",
"there": "そちら",
"forgot_your_password_calcom": "パスワードをお忘れですか? - {{appName}}",
"delete_webhook_confirmation_message": "この Webhook を削除してもよろしいですか? イベントがスケジュールまたはキャンセルされた場合、指定された URL で {{appName}} ミーティング データをリアルタイムで受信しなくなります。",
"confirm_delete_webhook": "はい、ウェブフックを削除します",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "電話番号(エージェントが通話するため)",
"buy_a_phone_number_or_import_one_you_already_have": "電話番号を購入するか、すでにTwilioで持っている番号をインポートする",
"test_agent": "エージェントをテスト",
"test_web_call": "Webコールのテスト",
"phone_numbers": "電話番号",
"unsubscribe": "登録解除",
"delete_workflow_step": "ワークフローステップを削除",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "アプリは正常に削除されました",
"error_removing_app": "アプリの削除中にエラーが発生しました",
"web_conference": "ウェブ会議",
"web_call": "Webコール",
"start_web_call": "Webコールを開始",
"end_call": "通話を終了",
"test_your_agent_with_web_call": "電話番号不要でWebコールでエージェントをテスト",
"phone_call": "電話",
"connecting_to_agent": "エージェントに接続中...",
"call_active": "通話中",
"call_ended": "通話終了",
"call_error": "通話エラー",
"ready_to_start_call": "通話開始の準備完了",
"failed_to_start_web_call_try_again": "Webコールの開始に失敗しました。もう一度お試しください。",
"call_encountered_error_try_again": "通話中にエラーが発生しました。もう一度お試しください。",
"failed_initialize_web_call_microphone_permissions": "Webコールの初期化に失敗しました。マイクの権限が許可されていることを確認してください。",
"start_call_to_see_conversation": "会話を表示するには通話を開始してください",
"waiting_for_conversation": "会話を待機中...",
"ai_agent": "AIエージェント",
"live": "ライブ",
"connecting": "接続中...",
"requires_confirmation": "確認が必要です",
"requires_confirmation_threshold": "< {{time}} $t({{unit}}_timeUnit) の通知で予約した場合には確認が必要です",
"may_require_confirmation": "確認が必要となる可能性があります",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "文字起こしが有効になりました",
"transcription_stopped": "文字起こしが停止しました",
"download_transcript": "トランスクリプトをダウンロード",
"transcript": "文字起こし",
"recording_from_your_recent_call": "{{appName}} での最近の通話のレコーディングがダウンロードできます",
"transcript_from_previous_call": "{{appName}}での最近の通話のトランスクリプトがダウンロード可能です。リンクは1時間のみ有効です。",
"you_can_download_transcript_from_attachments": "添付ファイルから議事録をダウンロードすることもできます",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "予約画面、メール通知、カレンダーイベントから主催者のメールアドレスを非表示にします",
"you_and_conjunction": "あなた &",
"select_verified_email": "確認済みメールを選択",
"add_verified_email": "認証済みメールアドレスを追加",
"add_verified_emails": "認証済みメールアドレスを追加",
"custom_reply_to_email_title": "カスタム「返信先」メール",
"custom_reply_to_email_description": "主催者のメールの代わりに、確認メールの返信先として別のメールアドレスを使用します",
"email_survey_triggered_by_workflow": "このアンケートはCalのワークフローによってトリガーされました。",
@@ -3619,6 +3642,11 @@
"send_another_message": "別のメッセージを送信する",
"sending": "送信中",
"no_variables_found": "変数が見つかりません",
"credits_required": "クレジットが必要です",
"web_call_credits_info": "Webコールはクレジットを消費します。通話を開始するのに十分なクレジットがあることを確認してください。",
"web_call_no_credits": "Webコールにはクレジットが必要です。残りクレジットは0です。",
"purchase_credits": "クレジットを購入",
"credits_required_tooltip": "この操作を進めるにはクレジットが必要です",
"api_key_name_too_long": "名前は{{max}}文字を超えてはいけません",
"auto_charge_for_last_minute_cancellation": "直前のキャンセルに対して自動的に無断キャンセル料を請求する",
"before_scheduled_start_time": "予定開始時刻より前",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organization របស់អ្នកត្រូវបានដំឡើងកំណែដោយជោគជ័យ!",
"use_link_to_reset_password": "ប្រើតំណខាងក្រោមដើម្បីកំណត់ពាក្យសម្ងាត់របស់អ្នកឡើងវិញ",
"hey_there": "សួស្តី",
"there": "នៅទីនោះ",
"forgot_your_password_calcom": "ភ្លេចពាក្យសម្ងាត់? - {{appName}}",
"delete_webhook_confirmation_message": "តើអ្នកប្រាកដថាចង់លុប webhook នេះទេ? អ្នកនឹងលែងទទួលបានទិន្នន័យប្រជុំ {{appName}} តាម URL ដែលបានបញ្ជាក់, ក្នុងពេលវេលាជាក់ស្តែង នៅពេលដែលព្រឹត្តិការណ៍មួយត្រូវបានកំណត់ពេល ឬលុបចោល។",
"confirm_delete_webhook": "បាទ/ចាស លុប webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "លេខទូរស័ព្ទ (សម្រាប់ភ្នាក់ងាររបស់យើងដើម្បីហៅ)",
"buy_a_phone_number_or_import_one_you_already_have": "ទិញលេខទូរស័ព្ទមួយឬនាំចូលលេខដែលអ្នកមានរួចហើយនៅលើ twilio",
"test_agent": "សាកល្បងភ្នាក់ងារ",
"test_web_call": "សាកល្បងការហៅទូរស័ព្ទតាមវេប",
"phone_numbers": "លេខទូរស័ព្ទ",
"unsubscribe": "ដកការជាវ",
"delete_workflow_step": "លុបជំហានលំហូរការងារ",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "កម្មវិធីត្រូវបានយកចេញដោយជោគជ័យ",
"error_removing_app": "មានកំហុសក្នុងការយកកម្មវិធីចេញ",
"web_conference": "សន្និសីទតាមអ៊ីនធឺណិត",
"web_call": "ការហៅទូរស័ព្ទតាមវេប",
"start_web_call": "ចាប់ផ្តើមការហៅទូរស័ព្ទតាមវេប",
"end_call": "បញ្ចប់ការហៅទូរស័ព្ទ",
"test_your_agent_with_web_call": "សាកល្បងភ្នាក់ងាររបស់អ្នកជាមួយការហៅទូរស័ព្ទតាមវេបដោយមិនចាំបាច់មានលេខទូរស័ព្ទ",
"phone_call": "ការហៅទូរស័ព្ទ",
"connecting_to_agent": "កំពុងភ្ជាប់ទៅភ្នាក់ងារ...",
"call_active": "ការហៅទូរស័ព្ទសកម្ម",
"call_ended": "ការហៅទូរស័ព្ទបានបញ្ចប់",
"call_error": "កំហុសក្នុងការហៅទូរស័ព្ទ",
"ready_to_start_call": "រួចរាល់ដើម្បីចាប់ផ្តើមការហៅទូរស័ព្ទ",
"failed_to_start_web_call_try_again": "បរាជ័យក្នុងការចាប់ផ្តើមការហៅទូរស័ព្ទតាមវេប។ សូមព្យាយាមម្តងទៀត។",
"call_encountered_error_try_again": "ការហៅទូរស័ព្ទជួបប្រទះកំហុស។ សូមព្យាយាមម្តងទៀត។",
"failed_initialize_web_call_microphone_permissions": "បរាជ័យក្នុងការចាប់ផ្តើមការហៅទូរស័ព្ទតាមវេប។ សូមប្រាកដថាការអនុញ្ញាតមីក្រូហ្វូនត្រូវបានផ្តល់។",
"start_call_to_see_conversation": "ចាប់ផ្តើមការហៅទូរស័ព្ទដើម្បីមើលការសន្ទនា",
"waiting_for_conversation": "កំពុងរង់ចាំការសន្ទនា...",
"ai_agent": "ភ្នាក់ងារ AI",
"live": "ផ្ទាល់",
"connecting": "កំពុងភ្ជាប់...",
"requires_confirmation": "ទាមទារការបញ្ជាក់",
"requires_confirmation_threshold": "ត្រូវការការបញ្ជាក់ប្រសិនបើបានកក់ជាមួយ < {{time}} $t({{unit}}_timeUnit) ការជូនដំណឹង",
"may_require_confirmation": "អាចត្រូវការការបញ្ជាក់",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "ការបកប្រែត្រូវបានបើកដំណើរការឥឡូវនេះ",
"transcription_stopped": "ការបកប្រែត្រូវបានបញ្ឈប់ឥឡូវនេះ",
"download_transcript": "ទាញយកការបកប្រែ",
"transcript": "កំណត់ត្រាការសន្ទនា",
"recording_from_your_recent_call": "ការថតពីការហៅថ្មីៗរបស់អ្នកនៅលើ {{appName}} ត្រូវបានត្រៀមរួចរាល់សម្រាប់ទាញយក",
"transcript_from_previous_call": "ការបកប្រែពីការហៅថ្មីៗរបស់អ្នកនៅលើ {{appName}} ត្រូវបានត្រៀមរួចរាល់សម្រាប់ទាញយក។ តំណភ្ជាប់មានសុពលភាពតែ 1 ម៉ោងប៉ុណ្ណោះ",
"you_can_download_transcript_from_attachments": "អ្នកក៏អាចទាញយកកំណត់ត្រាពីឯកសារភ្ជាប់បានដែរ",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "លាក់អាសយដ្ឋានអ៊ីមែលរបស់អ្នករៀបចំពីអេក្រង់ការកក់ ការជូនដំណឹងតាមអ៊ីមែល និងព្រឹត្តិការណ៍ប្រតិទិន",
"you_and_conjunction": "អ្នក &",
"select_verified_email": "ជ្រើសរើសអ៊ីមែលដែលបានផ្ទៀងផ្ទាត់",
"add_verified_email": "បន្ថែមអ៊ីមែលដែលបានផ្ទៀងផ្ទាត់",
"add_verified_emails": "បន្ថែមអ៊ីមែលដែលបានផ្ទៀងផ្ទាត់",
"custom_reply_to_email_title": "អ៊ីមែល 'ឆ្លើយតប' ផ្ទាល់ខ្លួន",
"custom_reply_to_email_description": "ប្រើអាសយដ្ឋានអ៊ីមែលផ្សេងជា replyTo សម្រាប់អ៊ីមែលបញ្ជាក់ជំនួសឱ្យអ៊ីមែលរបស់អ្នករៀបចំ",
"email_survey_triggered_by_workflow": "ការស្ទង់មតិនេះត្រូវបានបង្កើតឡើងដោយលំហូរការងារនៅក្នុង Cal។",
@@ -3619,6 +3642,11 @@
"send_another_message": "ផ្ញើសារមួយទៀត",
"sending": "កំពុងផ្ញើ",
"no_variables_found": "រកមិនឃើញអថេរទេ",
"credits_required": "តម្រូវឱ្យមានក្រេឌីត",
"web_call_credits_info": "ការហៅទូរស័ព្ទតាមវេបប្រើប្រាស់ក្រេឌីត។ សូមប្រាកដថាអ្នកមានក្រេឌីតគ្រប់គ្រាន់ដើម្បីចាប់ផ្តើមការហៅទូរស័ព្ទ។",
"web_call_no_credits": "ការហៅទូរស័ព្ទតាមវេបតម្រូវឱ្យមានក្រេឌីត។ អ្នកមានក្រេឌីតនៅសល់ 0។",
"purchase_credits": "ទិញក្រេឌីត",
"credits_required_tooltip": "សកម្មភាពនេះតម្រូវឱ្យមានក្រេឌីតដើម្បីបន្ត",
"api_key_name_too_long": "ឈ្មោះមិនត្រូវលើសពី {{max}} តួអក្សរទេ",
"auto_charge_for_last_minute_cancellation": "គិតថ្លៃសេវាដោយស្វ័យប្រវត្តិសម្រាប់ការលុបចោលនៅនាទីចុងក្រោយ",
"before_scheduled_start_time": "មុនពេលចាប់ផ្តើមដែលបានកំណត់",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organization이 성공적으로 업그레이드되었습니다!",
"use_link_to_reset_password": "비밀번호를 재설정하시려면 아래 링크를 사용하세요.",
"hey_there": "안녕하세요",
"there": "거기",
"forgot_your_password_calcom": "비밀번호를 잊어버리셨나요? - {{appName}}",
"delete_webhook_confirmation_message": "웹훅을 정말로 삭제하시겠습니까? 이벤트 일정이 잡혔거나 취소되었을 때 {{appName}}의 회의 정보를 더 이상 실시간으로 받아보실 수 없게 됩니다.",
"confirm_delete_webhook": "네, 웹훅을 삭제하겠습니다.",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "전화번호(에이전트가 전화할 번호)",
"buy_a_phone_number_or_import_one_you_already_have": "전화번호 구매 또는 이미 트윌리오에 있는 번호 가져오기",
"test_agent": "에이전트 테스트",
"test_web_call": "웹 통화 테스트",
"phone_numbers": "전화번호",
"unsubscribe": "구독 해지",
"delete_workflow_step": "워크플로우 단계 삭제",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "앱이 제거됨",
"error_removing_app": "앱 제거 중 오류 발생",
"web_conference": "웹 화상 회의",
"web_call": "웹 통화",
"start_web_call": "웹 통화 시작",
"end_call": "통화 종료",
"test_your_agent_with_web_call": "전화번호 없이 웹 통화로 에이전트 테스트하기",
"phone_call": "전화 통화",
"connecting_to_agent": "에이전트에 연결 중...",
"call_active": "통화 중",
"call_ended": "통화 종료됨",
"call_error": "통화 오류",
"ready_to_start_call": "통화 시작 준비 완료",
"failed_to_start_web_call_try_again": "웹 통화 시작에 실패했습니다. 다시 시도해 주세요.",
"call_encountered_error_try_again": "통화 중 오류가 발생했습니다. 다시 시도해 주세요.",
"failed_initialize_web_call_microphone_permissions": "웹 통화 초기화에 실패했습니다. 마이크 권한이 허용되어 있는지 확인해 주세요.",
"start_call_to_see_conversation": "대화를 보려면 통화를 시작하세요",
"waiting_for_conversation": "대화 대기 중...",
"ai_agent": "AI 에이전트",
"live": "실시간",
"connecting": "연결 중...",
"requires_confirmation": "확인 필수",
"requires_confirmation_threshold": "< {{time}} $t({{unit}}_timeUnit) 알림으로 예약한 경우 확인이 필요합니다",
"may_require_confirmation": "확인이 필요할 수 있음",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "전사 기능이 활성화되었습니다",
"transcription_stopped": "전사 기능이 중지되었습니다",
"download_transcript": "대화 기록 다운로드",
"transcript": "대화 내용",
"recording_from_your_recent_call": "{{appName}}에서 이루어진 최근 통화 녹음을 지금 바로 다운로드할 수 있습니다",
"transcript_from_previous_call": "{{appName}}에서 최근 통화한 대화 기록이 다운로드 준비되었습니다. 링크는 1시간 동안만 유효합니다.",
"you_can_download_transcript_from_attachments": "첨부 파일에서 대화 내용을 다운로드할 수 있습니다",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "예약 화면, 이메일 알림 및 캘린더 이벤트에서 주최자의 이메일 주소 숨기기",
"you_and_conjunction": "당신 &",
"select_verified_email": "인증된 이메일 선택",
"add_verified_email": "인증된 이메일 추가",
"add_verified_emails": "인증된 이메일 추가",
"custom_reply_to_email_title": "사용자 지정 '회신' 이메일",
"custom_reply_to_email_description": "주최자의 이메일 대신 확인 이메일의 회신 주소로 다른 이메일 주소 사용",
"email_survey_triggered_by_workflow": "이 설문은 Cal의 워크플로우에 의해 트리거되었습니다.",
@@ -3619,6 +3642,11 @@
"send_another_message": "다른 메시지 보내기",
"sending": "전송 중",
"no_variables_found": "변수를 찾을 수 없음",
"credits_required": "크레딧 필요",
"web_call_credits_info": "웹 통화는 크레딧을 소모합니다. 통화를 시작하기 전에 충분한 크레딧이 있는지 확인하세요.",
"web_call_no_credits": "웹 통화에는 크레딧이 필요합니다. 남은 크레딧이 0개입니다.",
"purchase_credits": "크레딧 구매",
"credits_required_tooltip": "이 작업을 진행하려면 크레딧이 필요합니다",
"api_key_name_too_long": "이름은 {{max}}자를 초과할 수 없습니다",
"auto_charge_for_last_minute_cancellation": "마지막 순간 취소에 대해 노쇼 수수료 자동 청구",
"before_scheduled_start_time": "예약된 시작 시간 전",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Uw organisatie is geüpgraded!",
"use_link_to_reset_password": "Gebruik de onderstaande link om uw wachtwoord te resetten",
"hey_there": "Hoi daar",
"there": "daar",
"forgot_your_password_calcom": "Wachtwoord vergeten? - {{appName}}",
"delete_webhook_confirmation_message": "Weet u zeker dat u deze webhook wilt verwijderen? U ontvangt dan niet langer {{appName}} vergadergegevens op een opgegeven URL, in real-time, wanneer een evenement is geboekt of geannuleerd.",
"confirm_delete_webhook": "Ja, webhook verwijderen",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefoonnummer (voor onze agent om te bellen)",
"buy_a_phone_number_or_import_one_you_already_have": "Koop een telefoonnummer of importeer er een die je al hebt bij Twilio",
"test_agent": "Test agent",
"test_web_call": "Test webgesprek",
"phone_numbers": "Telefoonnummers",
"unsubscribe": "Afmelden",
"delete_workflow_step": "Workflow-stap verwijderen",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "De app is verwijderd",
"error_removing_app": "Fout bij het verwijderen van de app",
"web_conference": "Webconferentie",
"web_call": "Webgesprek",
"start_web_call": "Start webgesprek",
"end_call": "Gesprek beëindigen",
"test_your_agent_with_web_call": "Test je agent met een webgesprek zonder dat een telefoonnummer nodig is",
"phone_call": "Telefoongesprek",
"connecting_to_agent": "Verbinding maken met agent...",
"call_active": "Gesprek actief",
"call_ended": "Gesprek beëindigd",
"call_error": "Gespreksfout",
"ready_to_start_call": "Klaar om gesprek te starten",
"failed_to_start_web_call_try_again": "Webgesprek starten mislukt. Probeer het opnieuw.",
"call_encountered_error_try_again": "Er is een fout opgetreden tijdens het gesprek. Probeer het opnieuw.",
"failed_initialize_web_call_microphone_permissions": "Initialiseren van webgesprek mislukt. Zorg ervoor dat microfoonrechten zijn toegestaan.",
"start_call_to_see_conversation": "Start een gesprek om de conversatie te zien",
"waiting_for_conversation": "Wachten op conversatie...",
"ai_agent": "AI-agent",
"live": "Live",
"connecting": "Verbinding maken...",
"requires_confirmation": "Vereist bevestiging",
"requires_confirmation_threshold": "Vereist bevestiging indien geboekt met < {{time}} $t({{unit}}_timeUnit) kennisgeving",
"may_require_confirmation": "Kan bevestiging vereisen",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transcripties zijn nu ingeschakeld",
"transcription_stopped": "Transcripties zijn nu gestopt",
"download_transcript": "Transcriptie downloaden",
"transcript": "Transcript",
"recording_from_your_recent_call": "Een opname van uw recente gesprek op {{appName}} is klaar om te downloaden",
"transcript_from_previous_call": "Transcriptie van uw recente gesprek op {{appName}} is klaar om te downloaden. Links zijn slechts 1 uur geldig",
"you_can_download_transcript_from_attachments": "Je kunt het transcript ook downloaden via de bijlagen",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Verberg het e-mailadres van de organisator op het boekingsscherm, in e-mailnotificaties en in agenda-afspraken",
"you_and_conjunction": "U &",
"select_verified_email": "Selecteer een geverifieerd e-mailadres",
"add_verified_email": "Geverifieerde e-mail toevoegen",
"add_verified_emails": "Geverifieerde e-mails toevoegen",
"custom_reply_to_email_title": "Aangepast 'Antwoord-aan' e-mailadres",
"custom_reply_to_email_description": "Gebruik een ander e-mailadres als antwoordadres voor bevestigingsmails in plaats van het e-mailadres van de organisator",
"email_survey_triggered_by_workflow": "Deze enquête is geactiveerd door een Workflow in Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Nog een bericht versturen",
"sending": "Bezig met verzenden",
"no_variables_found": "Geen variabelen gevonden",
"credits_required": "Credits vereist",
"web_call_credits_info": "Webgesprekken verbruiken credits. Zorg ervoor dat je voldoende credits hebt om een gesprek te starten.",
"web_call_no_credits": "Webgesprekken vereisen credits. Je hebt 0 credits over.",
"purchase_credits": "Credits kopen",
"credits_required_tooltip": "Deze actie vereist credits om verder te gaan",
"api_key_name_too_long": "Naam mag niet langer zijn dan {{max}} tekens",
"auto_charge_for_last_minute_cancellation": "Automatisch no-show kosten in rekening brengen voor last-minute annuleringen",
"before_scheduled_start_time": "Voor geplande starttijd",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organisasjonen din ble oppgradert!",
"use_link_to_reset_password": "Bruk lenken nedenfor for å tilbakestille passordet ditt",
"hey_there": "Hei der",
"there": "der",
"forgot_your_password_calcom": "Glemt passordet? - {{appName}}",
"delete_webhook_confirmation_message": "Er du sikker på at du vil slette denne webhooken? Du vil ikke lenger motta {{appName}} møtedata på en spesifisert URL, i sanntid, når en hendelse er planlagt eller avlyst.",
"confirm_delete_webhook": "Ja, slett webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefonnummer (for at agenten vår skal ringe)",
"buy_a_phone_number_or_import_one_you_already_have": "Kjøp et telefonnummer eller importer et du allerede har på twilio",
"test_agent": "Test agent",
"test_web_call": "Test nettanrop",
"phone_numbers": "Telefonnumre",
"unsubscribe": "Avslutt abonnement",
"delete_workflow_step": "Slett arbeidsflyttrinn",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Appen ble fjernet",
"error_removing_app": "Feil ved fjerning av app",
"web_conference": "Nett-konferanse",
"web_call": "Nettanrop",
"start_web_call": "Start nettanrop",
"end_call": "Avslutt samtale",
"test_your_agent_with_web_call": "Test agenten din med et nettanrop uten å kreve telefonnummer",
"phone_call": "Telefonsamtale",
"connecting_to_agent": "Kobler til agent...",
"call_active": "Samtale aktiv",
"call_ended": "Samtale avsluttet",
"call_error": "Samtalefeil",
"ready_to_start_call": "Klar til å starte samtale",
"failed_to_start_web_call_try_again": "Kunne ikke starte nettanrop. Vennligst prøv igjen.",
"call_encountered_error_try_again": "Samtalen støtte på en feil. Vennligst prøv igjen.",
"failed_initialize_web_call_microphone_permissions": "Kunne ikke initialisere nettanrop. Sørg for at mikrofontillatelser er gitt.",
"start_call_to_see_conversation": "Start en samtale for å se konversasjonen",
"waiting_for_conversation": "Venter på samtale...",
"ai_agent": "KI-agent",
"live": "Direkte",
"connecting": "Kobler til...",
"requires_confirmation": "Krever bekreftelse",
"requires_confirmation_threshold": "Krever bekreftelse hvis booket med < {{time}} $t({{unit}}_timeUnit) varsel",
"may_require_confirmation": "Kan kreve bekreftelse",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transkripsjoner er nå aktivert",
"transcription_stopped": "Transkripsjoner er nå stoppet",
"download_transcript": "Last ned transkripsjon",
"transcript": "Transkripsjon",
"recording_from_your_recent_call": "Et opptak fra din nylige samtale på {{appName}} er klart for nedlasting",
"transcript_from_previous_call": "Transkripsjon fra din nylige samtale på {{appName}} er klar for nedlasting. Lenker er kun gyldige i 1 time",
"you_can_download_transcript_from_attachments": "Du kan også laste ned transkripsjonen fra vedleggene",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Skjul arrangørens e-postadresse fra bestillingsskjermen, e-postvarsler og kalenderarrangementer",
"you_and_conjunction": "Du &",
"select_verified_email": "Velg en verifisert e-post",
"add_verified_email": "Legg til verifisert e-post",
"add_verified_emails": "Legg til verifiserte e-poster",
"custom_reply_to_email_title": "Egendefinert 'Svar-til' e-post",
"custom_reply_to_email_description": "Bruk en annen e-postadresse som svar-til for bekreftelsese-poster i stedet for arrangørens e-post",
"email_survey_triggered_by_workflow": "Denne undersøkelsen ble utløst av en arbeidsflyt i Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Send en ny melding",
"sending": "Sender",
"no_variables_found": "Ingen variabler funnet",
"credits_required": "Kreditter kreves",
"web_call_credits_info": "Nettanrop bruker kreditter. Sørg for at du har nok kreditter til å starte en samtale.",
"web_call_no_credits": "Nettanrop krever kreditter. Du har 0 kreditter igjen.",
"purchase_credits": "Kjøp kreditter",
"credits_required_tooltip": "Denne handlingen krever kreditter for å fortsette",
"api_key_name_too_long": "Navnet må ikke overstige {{max}} tegn",
"auto_charge_for_last_minute_cancellation": "Auto-belast gebyr for uteblivelse ved avbestillinger i siste liten",
"before_scheduled_start_time": "Før planlagt starttid",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Twój zespół z planu Organization przeszedł na wyższą wersję!",
"use_link_to_reset_password": "Użyj poniższego linku, aby zresetować swoje hasło",
"hey_there": "Cześć",
"there": "tam",
"forgot_your_password_calcom": "Nie pamiętasz swojego hasła? - {{appName}}",
"delete_webhook_confirmation_message": "Czy na pewno chcesz usunąć ten webhook? Nie będziesz już otrzymywać danych o spotkaniach w {{appName}} w określonym URL, w czasie rzeczywistym, gdy zdarzenie jest zaplanowane lub anulowane.",
"confirm_delete_webhook": "Tak, usuń webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Numer telefonu (dla naszego agenta do wykonania połączenia)",
"buy_a_phone_number_or_import_one_you_already_have": "Kup numer telefonu lub zaimportuj taki, który już posiadasz w Twilio",
"test_agent": "Przetestuj agenta",
"test_web_call": "Przetestuj połączenie internetowe",
"phone_numbers": "Numery telefonów",
"unsubscribe": "Wypisz się",
"delete_workflow_step": "Usuń krok przepływu pracy",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplikacja została usunięta",
"error_removing_app": "Błąd podczas usuwania aplikacji",
"web_conference": "Konferencja internetowa",
"web_call": "Połączenie internetowe",
"start_web_call": "Rozpocznij połączenie internetowe",
"end_call": "Zakończ połączenie",
"test_your_agent_with_web_call": "Przetestuj swojego agenta za pomocą połączenia internetowego bez potrzeby użycia numeru telefonu",
"phone_call": "Połączenie telefoniczne",
"connecting_to_agent": "Łączenie z agentem...",
"call_active": "Połączenie aktywne",
"call_ended": "Połączenie zakończone",
"call_error": "Błąd połączenia",
"ready_to_start_call": "Gotowy do rozpoczęcia połączenia",
"failed_to_start_web_call_try_again": "Nie udało się rozpocząć połączenia internetowego. Spróbuj ponownie.",
"call_encountered_error_try_again": "Wystąpił błąd podczas połączenia. Spróbuj ponownie.",
"failed_initialize_web_call_microphone_permissions": "Nie udało się zainicjować połączenia internetowego. Upewnij się, że przyznano uprawnienia do mikrofonu.",
"start_call_to_see_conversation": "Rozpocznij połączenie, aby zobaczyć rozmowę",
"waiting_for_conversation": "Oczekiwanie na rozmowę...",
"ai_agent": "Agent AI",
"live": "Na żywo",
"connecting": "Łączenie...",
"requires_confirmation": "Wymaga potwierdzenia",
"requires_confirmation_threshold": "Wymaga potwierdzenia w przypadku rezerwacji z wyprzedzeniem < {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Może wymagać potwierdzenia",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transkrypcje są teraz włączone",
"transcription_stopped": "Transkrypcje zostały zatrzymane",
"download_transcript": "Pobierz transkrypcję",
"transcript": "Transkrypcja",
"recording_from_your_recent_call": "Nagranie z ostatniego połączenia w aplikacji {{appName}} jest gotowe do pobrania.",
"transcript_from_previous_call": "Transkrypcja z Twojej ostatniej rozmowy na {{appName}} jest gotowa do pobrania. Linki są ważne tylko przez 1 godzinę",
"you_can_download_transcript_from_attachments": "Możesz również pobrać transkrypcję z załączników",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Ukryj adres e-mail organizatora na ekranie rezerwacji, w powiadomieniach e-mail i wydarzeniach w kalendarzu.",
"you_and_conjunction": "Ty &",
"select_verified_email": "Wybierz zweryfikowany e-mail",
"add_verified_email": "Dodaj zweryfikowany e-mail",
"add_verified_emails": "Dodaj zweryfikowane e-maile",
"custom_reply_to_email_title": "Niestandardowy e-mail 'Odpowiedz do'",
"custom_reply_to_email_description": "Użyj innego adresu e-mail jako 'replyTo' dla e-maili potwierdzających zamiast e-maila organizatora.",
"email_survey_triggered_by_workflow": "Ta ankieta została uruchomiona przez Workflow w Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Wyślij kolejną wiadomość",
"sending": "Wysyłanie",
"no_variables_found": "Nie znaleziono zmiennych",
"credits_required": "Wymagane kredyty",
"web_call_credits_info": "Połączenia internetowe zużywają kredyty. Upewnij się, że masz wystarczającą liczbę kredytów, aby rozpocząć połączenie.",
"web_call_no_credits": "Połączenia internetowe wymagają kredytów. Masz 0 pozostałych kredytów.",
"purchase_credits": "Kup kredyty",
"credits_required_tooltip": "Ta akcja wymaga kredytów, aby kontynuować",
"api_key_name_too_long": "Nazwa nie może przekraczać {{max}} znaków",
"auto_charge_for_last_minute_cancellation": "Automatyczne naliczanie opłaty za niepojawienie się w przypadku odwołania w ostatniej chwili",
"before_scheduled_start_time": "Przed zaplanowanym czasem rozpoczęcia",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Sua Organização foi atualizada com êxito!",
"use_link_to_reset_password": "Use o link abaixo para redefinir a sua senha",
"hey_there": "Olá",
"there": "lá",
"forgot_your_password_calcom": "Esqueceu sua senha? - {{appName}}",
"delete_webhook_confirmation_message": "Você tem certeza que quer remover este Webhook? Você não receberá mais dados de reuniões de {{appName}} na URL especificada, em tempo real, quando um evento for agendado ou cancelado.",
"confirm_delete_webhook": "Sim, remover o Webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Número de telefone (para nosso agente ligar)",
"buy_a_phone_number_or_import_one_you_already_have": "Compre um número de telefone ou importe um que você já tenha na Twilio",
"test_agent": "Testar agente",
"test_web_call": "Testar chamada web",
"phone_numbers": "Números de telefone",
"unsubscribe": "Cancelar inscrição",
"delete_workflow_step": "Excluir etapa do fluxo de trabalho",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplicativo removido com sucesso",
"error_removing_app": "Erro ao remover o aplicativo",
"web_conference": "Conferência via web",
"web_call": "Chamada web",
"start_web_call": "Iniciar chamada web",
"end_call": "Encerrar chamada",
"test_your_agent_with_web_call": "Teste seu agente com uma chamada web sem precisar de um número de telefone",
"phone_call": "Chamada telefônica",
"connecting_to_agent": "Conectando ao agente...",
"call_active": "Chamada ativa",
"call_ended": "Chamada encerrada",
"call_error": "Erro na chamada",
"ready_to_start_call": "Pronto para iniciar a chamada",
"failed_to_start_web_call_try_again": "Falha ao iniciar a chamada web. Por favor, tente novamente.",
"call_encountered_error_try_again": "A chamada encontrou um erro. Por favor, tente novamente.",
"failed_initialize_web_call_microphone_permissions": "Falha ao inicializar a chamada web. Verifique se as permissões do microfone estão concedidas.",
"start_call_to_see_conversation": "Inicie uma chamada para ver a conversa",
"waiting_for_conversation": "Aguardando conversa...",
"ai_agent": "Agente de IA",
"live": "Ao vivo",
"connecting": "Conectando...",
"requires_confirmation": "Requer confirmação",
"requires_confirmation_threshold": "Requer confirmação caso a reserva seja feita com menos de {{time}} $t({{unit}}_timeUnit) de antecedência",
"may_require_confirmation": "Pode exigir confirmação",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "As transcrições foram habilitadas",
"transcription_stopped": "As transcrições foram desabilitadas",
"download_transcript": "Baixar Transcrição",
"transcript": "Transcrição",
"recording_from_your_recent_call": "Uma gravação da sua chamada recente no {{appName}} está pronta para ser baixada",
"transcript_from_previous_call": "As transcrições da sua ligação recente no {{appName}} já estão prontas para serem baixadas. Os links são válidos por apenas 1 hora",
"you_can_download_transcript_from_attachments": "Você também pode baixar a transcrição dos anexos",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Ocultar o endereço de e-mail do organizador na tela de agendamento, notificações por e-mail e eventos do calendário",
"you_and_conjunction": "Você &",
"select_verified_email": "Selecione um e-mail verificado",
"add_verified_email": "Adicionar e-mail verificado",
"add_verified_emails": "Adicionar e-mails verificados",
"custom_reply_to_email_title": "E-mail personalizado para 'Responder a'",
"custom_reply_to_email_description": "Use um endereço de e-mail diferente como resposta para e-mails de confirmação em vez do e-mail do organizador",
"email_survey_triggered_by_workflow": "Esta pesquisa foi acionada por um Workflow no Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Enviar outra mensagem",
"sending": "Enviando",
"no_variables_found": "Nenhuma variável encontrada",
"credits_required": "Créditos necessários",
"web_call_credits_info": "Chamadas web consomem créditos. Certifique-se de ter créditos suficientes para iniciar uma chamada.",
"web_call_no_credits": "Chamadas web requerem créditos. Você tem 0 créditos restantes.",
"purchase_credits": "Comprar créditos",
"credits_required_tooltip": "Esta ação requer créditos para prosseguir",
"api_key_name_too_long": "O nome não deve exceder {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automaticamente taxa de não comparecimento para cancelamentos de última hora",
"before_scheduled_start_time": "Antes do horário de início agendado",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "A sua Organização foi atualizada com sucesso!",
"use_link_to_reset_password": "Use a ligação abaixo para redefinir a sua senha",
"hey_there": "Olá",
"there": "lá",
"forgot_your_password_calcom": "Esqueceu-se da sua senha? - {{appName}}",
"delete_webhook_confirmation_message": "De certeza que quer eliminar este Webhook? Não receberá mais dados de reuniões de {{appName}} do URL especificado, em tempo real, quando um evento for agendado ou cancelado.",
"confirm_delete_webhook": "Sim, eliminar o Webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Número de telefone (para nosso agente ligar)",
"buy_a_phone_number_or_import_one_you_already_have": "Compre um número de telefone ou importe um que você já tenha na Twilio",
"test_agent": "Testar agente",
"test_web_call": "Testar chamada web",
"phone_numbers": "Números de telefone",
"unsubscribe": "Cancelar inscrição",
"delete_workflow_step": "Excluir etapa do fluxo de trabalho",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplicação removida com sucesso",
"error_removing_app": "Erro ao remover a aplicação",
"web_conference": "Conferência web",
"web_call": "Chamada web",
"start_web_call": "Iniciar chamada web",
"end_call": "Terminar chamada",
"test_your_agent_with_web_call": "Teste o seu agente com uma chamada web sem necessidade de número de telefone",
"phone_call": "Chamada telefónica",
"connecting_to_agent": "A conectar ao agente...",
"call_active": "Chamada ativa",
"call_ended": "Chamada terminada",
"call_error": "Erro na chamada",
"ready_to_start_call": "Pronto para iniciar a chamada",
"failed_to_start_web_call_try_again": "Falha ao iniciar a chamada web. Por favor, tente novamente.",
"call_encountered_error_try_again": "A chamada encontrou um erro. Por favor, tente novamente.",
"failed_initialize_web_call_microphone_permissions": "Falha ao inicializar a chamada web. Certifique-se de que as permissões do microfone estão concedidas.",
"start_call_to_see_conversation": "Inicie uma chamada para ver a conversa",
"waiting_for_conversation": "A aguardar pela conversa...",
"ai_agent": "Agente de IA",
"live": "Ao vivo",
"connecting": "A conectar...",
"requires_confirmation": "Requer confirmação",
"requires_confirmation_threshold": "Requer confirmação se reservado com um aviso prévio inferior a {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Pode necessitar de confirmação",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "As transcrições estão agora ativadas",
"transcription_stopped": "As transcrições estão agora desativadas",
"download_transcript": "Transferir transcrição",
"transcript": "Transcrição",
"recording_from_your_recent_call": "A gravação da sua chamada recente em {{appName}} está pronta para descarregar",
"transcript_from_previous_call": "A transcrição da sua chamada recente em {{appName}} está pronta ser transferida. As ligações são válidas apenas durante 1 hora",
"you_can_download_transcript_from_attachments": "Você também pode baixar a transcrição dos anexos",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Ocultar o endereço de email do organizador na tela de agendamento, notificações por email e eventos do calendário",
"you_and_conjunction": "Você &",
"select_verified_email": "Selecione um email verificado",
"add_verified_email": "Adicionar e-mail verificado",
"add_verified_emails": "Adicionar e-mails verificados",
"custom_reply_to_email_title": "Email personalizado para 'Responder a'",
"custom_reply_to_email_description": "Use um endereço de email diferente como resposta para emails de confirmação em vez do email do organizador",
"email_survey_triggered_by_workflow": "Este questionário foi acionado por um Workflow no Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Enviar outra mensagem",
"sending": "Enviando",
"no_variables_found": "Nenhuma variável encontrada",
"credits_required": "Créditos necessários",
"web_call_credits_info": "As chamadas web consomem créditos. Certifique-se de que tem créditos suficientes para iniciar uma chamada.",
"web_call_no_credits": "As chamadas web requerem créditos. Tem 0 créditos restantes.",
"purchase_credits": "Comprar créditos",
"credits_required_tooltip": "Esta ação requer créditos para prosseguir",
"api_key_name_too_long": "O nome não deve exceder {{max}} caracteres",
"auto_charge_for_last_minute_cancellation": "Cobrar automaticamente taxa de não comparecimento para cancelamentos de última hora",
"before_scheduled_start_time": "Antes do horário de início agendado",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Upgrade-ul pentru Organization a fost realizat cu succes!",
"use_link_to_reset_password": "Utilizați linkul de mai jos pentru a vă reseta parola",
"hey_there": "Salut",
"there": "acolo",
"forgot_your_password_calcom": "Ți-ai uitat parola? - {{appName}}",
"delete_webhook_confirmation_message": "Ești sigur că vrei să ștergi acest webhook? Nu vei mai primi {{appName}} date de întâlnire la un URL specificat, în timp real, atunci când un eveniment este programat sau anulat.",
"confirm_delete_webhook": "Da, șterge webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Număr de telefon (pentru ca agentul nostru să sune)",
"buy_a_phone_number_or_import_one_you_already_have": "Cumpărați un număr de telefon sau importați unul pe care îl aveți deja pe Twilio",
"test_agent": "Testați agentul",
"test_web_call": "Testează apelul web",
"phone_numbers": "Numere de telefon",
"unsubscribe": "Dezabonați-vă",
"delete_workflow_step": "Ștergeți pasul fluxului de lucru",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplicație ștearsă cu succes",
"error_removing_app": "Eroare la ștergerea aplicației",
"web_conference": "Conferință web",
"web_call": "Apel web",
"start_web_call": "Pornește apelul web",
"end_call": "Încheie apelul",
"test_your_agent_with_web_call": "Testează agentul tău cu un apel web fără a necesita un număr de telefon",
"phone_call": "Apel telefonic",
"connecting_to_agent": "Se conectează la agent...",
"call_active": "Apel activ",
"call_ended": "Apel încheiat",
"call_error": "Eroare apel",
"ready_to_start_call": "Pregătit pentru a începe apelul",
"failed_to_start_web_call_try_again": "Nu s-a putut porni apelul web. Vă rugăm să încercați din nou.",
"call_encountered_error_try_again": "Apelul a întâmpinat o eroare. Vă rugăm să încercați din nou.",
"failed_initialize_web_call_microphone_permissions": "Nu s-a putut inițializa apelul web. Asigurați-vă că permisiunile pentru microfon sunt acordate.",
"start_call_to_see_conversation": "Porniți un apel pentru a vedea conversația",
"waiting_for_conversation": "Se așteaptă conversația...",
"ai_agent": "Agent AI",
"live": "În direct",
"connecting": "Se conectează...",
"requires_confirmation": "Necesită confirmare",
"requires_confirmation_threshold": "Necesită confirmare dacă este rezervat cu aviz de < {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Poate necesita confirmare",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Transcrierile sunt acum activate",
"transcription_stopped": "Transcrierile sunt acum oprite",
"download_transcript": "Descarcă transcrierea",
"transcript": "Transcriere",
"recording_from_your_recent_call": "O înregistrare a apelului dvs. recent de pe {{appName}} poate fi acum descărcată",
"transcript_from_previous_call": "Transcrierea de la apelul recent pe {{appName}} este gata pentru descărcare. Linkurile sunt valabile doar pentru 1 oră",
"you_can_download_transcript_from_attachments": "Puteți descărca transcrierea din atașamente",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Ascundeți adresa de email a organizatorului de pe ecranul de rezervare, notificările prin email și evenimentele din calendar",
"you_and_conjunction": "Tu &",
"select_verified_email": "Selectați un email verificat",
"add_verified_email": "Adaugă un email verificat",
"add_verified_emails": "Adaugă emailuri verificate",
"custom_reply_to_email_title": "Email personalizat 'Reply-To'",
"custom_reply_to_email_description": "Utilizați o altă adresă de email ca replyTo pentru emailurile de confirmare în locul emailului organizatorului",
"email_survey_triggered_by_workflow": "Acest sondaj a fost declanșat de un Workflow în Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Trimiteți un alt mesaj",
"sending": "Se trimite",
"no_variables_found": "Nu s-au găsit variabile",
"credits_required": "Credite necesare",
"web_call_credits_info": "Apelurile web consumă credite. Asigurați-vă că aveți suficiente credite pentru a începe un apel.",
"web_call_no_credits": "Apelurile web necesită credite. Aveți 0 credite rămase.",
"purchase_credits": "Cumpără credite",
"credits_required_tooltip": "Această acțiune necesită credite pentru a continua",
"api_key_name_too_long": "Numele nu trebuie să depășească {{max}} caractere",
"auto_charge_for_last_minute_cancellation": "Taxare automată a taxei de neprezentare pentru anulările de ultim moment",
"before_scheduled_start_time": "Înainte de ora programată de început",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Вы успешно перешли на тариф Organization!",
"use_link_to_reset_password": "Используйте ссылку ниже, чтобы сбросить пароль",
"hey_there": "Привет,",
"there": "там",
"forgot_your_password_calcom": "Забыли пароль? - {{appName}}",
"delete_webhook_confirmation_message": "Вы уверены, что хотите удалить этот вебхук? Вы больше не будете получать от {{appName}} данные о новых или отменённых встречах по указанному URL в реальном времени.",
"confirm_delete_webhook": "Да, удалить веб-хук",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Номер телефона (для звонка нашего агента)",
"buy_a_phone_number_or_import_one_you_already_have": "Купите номер телефона или импортируйте уже имеющийся у вас в Twilio",
"test_agent": "Тестовый агент",
"test_web_call": "Тестовый веб-звонок",
"phone_numbers": "Номера телефонов",
"unsubscribe": "Отписаться",
"delete_workflow_step": "Удалить шаг рабочего процесса",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Приложение успешно удалено",
"error_removing_app": "Ошибка при удалении приложения",
"web_conference": "Веб-конференция",
"web_call": "Веб-звонок",
"start_web_call": "Начать веб-звонок",
"end_call": "Завершить звонок",
"test_your_agent_with_web_call": "Протестируйте вашего агента с помощью веб-звонка без необходимости указывать номер телефона",
"phone_call": "Телефонный звонок",
"connecting_to_agent": "Подключение к агенту...",
"call_active": "Звонок активен",
"call_ended": "Звонок завершён",
"call_error": "Ошибка звонка",
"ready_to_start_call": "Готовы начать звонок",
"failed_to_start_web_call_try_again": "Не удалось начать веб-звонок. Пожалуйста, попробуйте ещё раз.",
"call_encountered_error_try_again": "Во время звонка произошла ошибка. Пожалуйста, попробуйте ещё раз.",
"failed_initialize_web_call_microphone_permissions": "Не удалось инициализировать веб-звонок. Убедитесь, что разрешения на использование микрофона предоставлены.",
"start_call_to_see_conversation": "Начните звонок, чтобы увидеть разговор",
"waiting_for_conversation": "Ожидание разговора...",
"ai_agent": "ИИ-агент",
"live": "В реальном времени",
"connecting": "Подключение...",
"requires_confirmation": "Требуется подтверждение",
"requires_confirmation_threshold": "Требуется подтверждение, если бронирование оформляется с уведомлением < {{time}} $t({{unit}}_timeUnit)",
"may_require_confirmation": "Может потребоваться подтверждение",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Транскрипции теперь включены",
"transcription_stopped": "Транскрипции теперь остановлены",
"download_transcript": "Скачать транскрипт",
"transcript": "Транскрипт",
"recording_from_your_recent_call": "Запись вашего недавнего созвона в {{appName}} готова для скачивания",
"transcript_from_previous_call": "Транскрипт вашего недавнего звонка в {{appName}} готов к скачиванию. Ссылки действительны только 1 час",
"you_can_download_transcript_from_attachments": "Вы также можете скачать расшифровку из вложений",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Скрыть адрес электронной почты организатора на экране бронирования, в email-уведомлениях и событиях календаря",
"you_and_conjunction": "Вы &",
"select_verified_email": "Выбрать подтвержденный email",
"add_verified_email": "Добавить подтверждённый email",
"add_verified_emails": "Добавить подтверждённые email-адреса",
"custom_reply_to_email_title": "Пользовательский email для ответа",
"custom_reply_to_email_description": "Используйте другой адрес электронной почты в качестве адреса для ответа в подтверждающих письмах вместо email организатора",
"email_survey_triggered_by_workflow": "Этот опрос был запущен рабочим процессом в Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Отправить ещё одно сообщение",
"sending": "Отправка",
"no_variables_found": "Переменные не найдены",
"credits_required": "Требуются кредиты",
"web_call_credits_info": "Веб-звонки расходуют кредиты. Убедитесь, что у вас достаточно кредитов для начала звонка.",
"web_call_no_credits": "Для веб-звонков требуются кредиты. У вас осталось 0 кредитов.",
"purchase_credits": "Купить кредиты",
"credits_required_tooltip": "Для выполнения этого действия требуются кредиты.",
"api_key_name_too_long": "Имя не должно превышать {{max}} символов",
"auto_charge_for_last_minute_cancellation": "Автоматическое списание платы за неявку при отмене в последний момент",
"before_scheduled_start_time": "До запланированного времени начала",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Vaša organizácia bola úspešne upgradovaná!",
"use_link_to_reset_password": "Použite nasledujúci odkaz na obnovenie hesla",
"hey_there": "Ahoj",
"there": "tam",
"forgot_your_password_calcom": "Zabudli ste heslo? - {{appName}}",
"delete_webhook_confirmation_message": "Naozaj chcete odstrániť tento webhook? Už nebudete dostávať údaje o stretnutiach {{appName}} v reálnom čase na určenú URL adresu pri naplánovaní alebo zrušení udalosti.",
"confirm_delete_webhook": "Áno, odstrániť webhook",
@@ -859,6 +860,7 @@
"phone_number_for_ai_call": "Telefónne číslo (pre volanie nášho agenta)",
"buy_a_phone_number_or_import_one_you_already_have": "Kúpte si telefónne číslo alebo importujte to, ktoré už máte na twilio",
"test_agent": "Testovať agenta",
"test_web_call": "Testovací webový hovor",
"phone_numbers": "Telefónne čísla",
"unsubscribe": "Odhlásiť",
"delete_workflow_step": "Odstrániť krok workflow",
@@ -1478,6 +1480,24 @@
"app_removed_successfully": "Aplikácia bola úspešne odstránená",
"error_removing_app": "Chyba pri odstraňovaní aplikácie",
"web_conference": "Webová konferencia",
"web_call": "Webový hovor",
"start_web_call": "Začať webový hovor",
"end_call": "Ukončiť hovor",
"test_your_agent_with_web_call": "Otestujte svojho agenta webovým hovorom bez potreby telefónneho čísla",
"phone_call": "Telefónny hovor",
"connecting_to_agent": "Pripájanie k agentovi...",
"call_active": "Hovor je aktívny",
"call_ended": "Hovor ukončený",
"call_error": "Chyba hovoru",
"ready_to_start_call": "Pripravené na začatie hovoru",
"failed_to_start_web_call_try_again": "Nepodarilo sa začať webový hovor. Skúste to znova.",
"call_encountered_error_try_again": "Počas hovoru sa vyskytla chyba. Skúste to znova.",
"failed_initialize_web_call_microphone_permissions": "Nepodarilo sa inicializovať webový hovor. Uistite sa, že sú povolené oprávnenia pre mikrofón.",
"start_call_to_see_conversation": "Začnite hovor pre zobrazenie konverzácie",
"waiting_for_conversation": "Čakanie na konverzáciu...",
"ai_agent": "AI Agent",
"live": "Naživo",
"connecting": "Pripájanie...",
"requires_confirmation": "Vyžaduje potvrdenie",
"requires_confirmation_threshold": "Vyžaduje potvrdenie, ak je rezervované s menej ako {{time}} $t({{unit}}_timeUnit) upozornením",
"may_require_confirmation": "Môže vyžadovať potvrdenie",
@@ -1713,6 +1733,7 @@
"transcription_enabled": "Prepisy sú teraz povolené",
"transcription_stopped": "Prepisy sú teraz zastavené",
"download_transcript": "Stiahnuť prepis",
"transcript": "Prepis",
"recording_from_your_recent_call": "Nahrávka z vášho nedávneho hovoru v {{appName}} je pripravená na stiahnutie",
"transcript_from_previous_call": "Prepis z vášho nedávneho hovoru v {{appName}} je pripravený na stiahnutie. Odkazy sú platné len 1 hodinu",
"you_can_download_transcript_from_attachments": "Prepis si môžete stiahnuť aj z príloh",
@@ -3344,6 +3365,8 @@
"hide_organizer_email_description": "Skryť e-mailovú adresu organizátora z obrazovky rezervácie, e-mailových upozornení a kalendárových udalostí",
"you_and_conjunction": "Vy &",
"select_verified_email": "Vyberte overený e-mail",
"add_verified_email": "Pridať overený e-mail",
"add_verified_emails": "Pridať overené e-maily",
"custom_reply_to_email_title": "Vlastný e-mail 'Odpovedať na'",
"custom_reply_to_email_description": "Použiť inú e-mailovú adresu ako adresu pre odpoveď v potvrdzovacích e-mailoch namiesto e-mailu organizátora",
"email_survey_triggered_by_workflow": "Tento prieskum bol aktivovaný pracovným postupom v Cal.",
@@ -3619,6 +3642,11 @@
"send_another_message": "Poslať ďalšiu správu",
"sending": "Odosielanie",
"no_variables_found": "Neboli nájdené žiadne premenné",
"credits_required": "Vyžadované kredity",
"web_call_credits_info": "Webové hovory spotrebúvajú kredity. Uistite sa, že máte dostatok kreditov na začatie hovoru.",
"web_call_no_credits": "Webové hovory vyžadujú kredity. Zostáva vám 0 kreditov.",
"purchase_credits": "Kúpiť kredity",
"credits_required_tooltip": "Táto akcia vyžaduje kredity na pokračovanie",
"api_key_name_too_long": "Názov nesmie presiahnuť {{max}} znakov",
"auto_charge_for_last_minute_cancellation": "Automaticky účtovať poplatok za neúčasť pri zrušení na poslednú chvíľu",
"before_scheduled_start_time": "Pred plánovaným časom začiatku",

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