Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55df0e3c9d | ||
|
|
730aab3e3d | ||
|
|
63740c02c7 | ||
|
|
c5942bcfcd | ||
|
|
c4547c51a2 | ||
|
|
cf89046ede | ||
|
|
d5d44fb64b | ||
|
|
4bb092c345 | ||
|
|
e1f7e94e36 | ||
|
|
2e9d5f7df1 | ||
|
|
8c943e8817 | ||
|
|
56821553fe | ||
|
|
b512ceb217 | ||
|
|
cfeb18de3f | ||
|
|
b407be933d | ||
|
|
4e1920471d | ||
|
|
6ee752aed8 | ||
|
|
a1922efab5 | ||
|
|
76332a759b | ||
|
|
dfdda8a5b8 | ||
|
|
286ee6b1bd | ||
|
|
ef8b0fa41c | ||
|
|
7fbeeae25b | ||
|
|
5cf0553199 | ||
|
|
4f114ef8d3 | ||
|
|
a9d6fbd14d | ||
|
|
3e1629add9 | ||
|
|
f1ac0642d1 | ||
|
|
4689775a1f | ||
|
|
4011733271 | ||
|
|
f41aa234d7 | ||
|
|
a44a3e5b84 | ||
|
|
98a075d062 | ||
|
|
bdc3cb9d6e | ||
|
|
616e4c4908 | ||
|
|
eaf695824b | ||
|
|
69a76f7fb7 | ||
|
|
d70fa46260 | ||
|
|
efba58a36a | ||
|
|
b3a28dca25 | ||
|
|
3b471f0e25 | ||
|
|
aac5fa3537 | ||
|
|
134f7e591e | ||
|
|
5bb2bc3ba4 | ||
|
|
bb6f5cca93 | ||
|
|
81bd8033a9 | ||
|
|
c2a44e9319 | ||
|
|
39917ebfa6 | ||
|
|
9ee06b410e | ||
|
|
76ced2d3b8 | ||
|
|
eaa1ad6578 | ||
|
|
f9095d298d | ||
|
|
fd87954048 | ||
|
|
be249a94de | ||
|
|
59582cf8f5 | ||
|
|
5c70060992 | ||
|
|
ae7f8f1653 | ||
|
|
4d0458b0e8 | ||
|
|
55e6682832 | ||
|
|
65448c017f | ||
|
|
e9fd23d0d3 | ||
|
|
badc857bcd | ||
|
|
7fe80dd3dd | ||
|
|
5feca13cfb | ||
|
|
f267cd9cd9 | ||
|
|
e8c2d4fa95 | ||
|
|
7e415b4542 |
@@ -7,7 +7,7 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@calcom/platform-libraries"],
|
||||
"ignore": ["@calcom/platform-libraries", "@calcom/api-v2"],
|
||||
"privatePackages": {
|
||||
"version": false,
|
||||
"tag": false
|
||||
|
||||
+82
-7
@@ -1,4 +1,3 @@
|
||||
// This configuration only applies to the package manager root.
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["./packages/config/eslint-preset.js"],
|
||||
@@ -7,29 +6,105 @@ module.exports = {
|
||||
"import/no-cycle": ["warn", { maxDepth: Infinity }],
|
||||
},
|
||||
overrides: [
|
||||
// WARN: features must not be imported by app-store or lib
|
||||
{
|
||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}", "packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
paths: ["@calcom/app-store"],
|
||||
patterns: ["@calcom/app-store/*"],
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/app-store or @calcom/lib.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// WARN: lib must not import app-store or features
|
||||
{
|
||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into app-store
|
||||
"**/app-store",
|
||||
"**/app-store/*",
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch alias imports
|
||||
"@calcom/app-store",
|
||||
"@calcom/app-store/*",
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "@calcom/lib should not import @calcom/app-store or @calcom/features.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// ERROR: app-store must not import trpc
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
allowTypeImports: false,
|
||||
patterns: [
|
||||
{
|
||||
group: ["@calcom/trpc/*", "@trpc/*"],
|
||||
message: "tRPC imports are blocked in packages/app-store. Move UI to apps/web/components/apps or introduce an API boundary.",
|
||||
group: [
|
||||
// Catch all relative paths into trpc
|
||||
"**/trpc",
|
||||
"**/trpc/*",
|
||||
// Catch alias imports
|
||||
"@calcom/trpc",
|
||||
"@calcom/trpc/*",
|
||||
"@trpc",
|
||||
"@trpc/*",
|
||||
],
|
||||
message:
|
||||
"@calcom/app-store must not import trpc. Move UI to apps/web/components/apps or introduce an API boundary.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ERROR: prisma must not import `features` package
|
||||
{
|
||||
files: ["packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/prisma.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -71,9 +71,9 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
yarn test:e2e
|
||||
yarn workspace @calcom/platform-libraries build && yarn test:e2e
|
||||
EXIT_CODE=$?
|
||||
echo "yarn test:e2e command exit code: $EXIT_CODE"
|
||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
ATOMS_E2E_API_URL: ${{ secrets.ATOMS_E2E_API_URL }}
|
||||
ATOMS_E2E_ORG_ID: ${{ secrets.ATOMS_E2E_ORG_ID }}
|
||||
ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED }}
|
||||
ATOMS_E2E_APPLE_ID: ${{ secrets.ATOMS_E2E_APPLE_ID }}
|
||||
ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE: ${{ secrets.ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@ jobs:
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn test
|
||||
yarn workspace @calcom/platform-libraries build && yarn test
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
@@ -1,20 +1,14 @@
|
||||
import { captureException as SentryCaptureException } from "@sentry/nextjs";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
export const captureErrors: NextMiddleware = async (_req, res, next) => {
|
||||
try {
|
||||
// Catch any errors that are thrown in remaining
|
||||
// middleware and the API route handler
|
||||
await next();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
SentryCaptureException(error);
|
||||
const redactedError = redactError(error);
|
||||
if (redactedError instanceof Error) {
|
||||
res.status(400).json({ message: redactedError.message, error: redactedError });
|
||||
return;
|
||||
}
|
||||
res.status(400).json({ message: "Something went wrong", error });
|
||||
res.status(500).json({ message: "Something went wrong" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
query: {},
|
||||
userId: testUserId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
@@ -43,7 +43,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
query: { apiKey: "test-key" },
|
||||
userId: testUserId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
|
||||
limit: 100,
|
||||
remaining: 99,
|
||||
@@ -51,6 +51,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
});
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
||||
@@ -74,15 +75,14 @@ describe("rateLimitApiKey middleware", () => {
|
||||
success: true,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
||||
@@ -96,10 +96,11 @@ describe("rateLimitApiKey middleware", () => {
|
||||
query: { apiKey: "test-key" },
|
||||
userId: testUserId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
@@ -121,6 +122,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
@@ -131,6 +133,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
vi.mocked(handleAutoLock).mockResolvedValueOnce(true);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(handleAutoLock).toHaveBeenCalledWith({
|
||||
@@ -158,6 +161,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
@@ -168,6 +172,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key."));
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(handleAutoLock).toHaveBeenCalledWith({
|
||||
@@ -195,6 +200,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
@@ -235,6 +241,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
export const eventTypeSelect = {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
position: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
scheduleId: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
recurringEvent: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
minimumBookingNotice: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
slotInterval: true,
|
||||
parentId: true,
|
||||
successRedirectUrl: true,
|
||||
description: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
customInputs: {
|
||||
select: { id: true, label: true, required: true, options: true, type: true, placeholder: true },
|
||||
},
|
||||
hashedLink: {
|
||||
select: { link: true },
|
||||
},
|
||||
team: {
|
||||
select: { slug: true },
|
||||
},
|
||||
hosts: {
|
||||
select: {
|
||||
userId: true,
|
||||
isFixed: true,
|
||||
scheduleId: true,
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
select: { username: true, id: true },
|
||||
},
|
||||
children: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.EventTypeSelect;
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
MIN_EVENT_DURATION_MINUTES,
|
||||
} from "@calcom/lib/constants";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema";
|
||||
import { HostSchema } from "@calcom/prisma/zod/modelSchema/HostSchema";
|
||||
|
||||
import { Frequency } from "~/lib/types";
|
||||
|
||||
import { jsonSchema } from "./shared/jsonSchema";
|
||||
import { schemaQueryUserId } from "./shared/queryUserId";
|
||||
import { timeZone } from "./shared/timeZone";
|
||||
|
||||
@@ -118,67 +117,3 @@ const schemaEventTypeEditParams = z
|
||||
.strict();
|
||||
|
||||
export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams);
|
||||
export const schemaEventTypeReadPublic = EventTypeSchema.pick({
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
position: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
scheduleId: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
recurringEvent: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
minimumBookingNotice: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
slotInterval: true,
|
||||
parentId: true,
|
||||
successRedirectUrl: true,
|
||||
description: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
children: z.array(childrenSchema).optional().default([]),
|
||||
hosts: z.array(hostSchema).optional().default([]),
|
||||
locations: z
|
||||
.array(
|
||||
z.object({
|
||||
link: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
hostPhoneNumber: z.string().optional(),
|
||||
type: z.any().optional(),
|
||||
})
|
||||
)
|
||||
.nullable(),
|
||||
metadata: jsonSchema.nullable(),
|
||||
customInputs: customInputSchema.array().optional(),
|
||||
link: z.string().optional(),
|
||||
hashedLink: z
|
||||
.array(z.object({ link: z.string() }))
|
||||
.optional()
|
||||
.default([]),
|
||||
bookingFields: eventTypeBookingFields.optional().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -102,8 +102,11 @@ export async function getHandler(req: NextApiRequest) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
attendees: true,
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
user: true,
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
payment: true,
|
||||
eventType: expand.includes("team") ? { include: { team: true } } : false,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
@@ -63,6 +63,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import {
|
||||
getTranscriptsAccessLinkFromRecordingId,
|
||||
checkIfRoomNameMatchesInRecording,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import {
|
||||
getCalendarCredentialsWithoutDelegation,
|
||||
getConnectedCalendars,
|
||||
} from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import {
|
||||
getCalendarCredentialsWithoutDelegation,
|
||||
getConnectedCalendars,
|
||||
} from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
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 { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
@@ -66,4 +65,4 @@ async function checkPermissions(req: NextApiRequest) {
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
export default deleteHandler;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
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 { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
@@ -49,15 +48,13 @@ export async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
customInputs: true,
|
||||
hashedLink: { select: { link: true } },
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
select: eventTypeSelect,
|
||||
});
|
||||
|
||||
if (!eventType) {
|
||||
throw new HttpError({ statusCode: 404, message: "Event type not found" });
|
||||
}
|
||||
|
||||
await checkPermissions(req, eventType);
|
||||
|
||||
const link = eventType ? getCalLink(eventType) : null;
|
||||
@@ -74,9 +71,12 @@ export async function getHandler(req: NextApiRequest) {
|
||||
eventType.scheduleId = user.defaultScheduleId;
|
||||
}
|
||||
|
||||
// TODO: eventType when not found should be a 404
|
||||
// but API consumers may depend on the {} behaviour.
|
||||
return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) };
|
||||
return {
|
||||
event_type: {
|
||||
...eventType,
|
||||
link,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type BaseEventTypeCheckPermissions = {
|
||||
@@ -100,4 +100,4 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
export default getHandler;
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
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 { Prisma } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import type { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type";
|
||||
import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaEventTypeEditBodyParams } from "~/lib/validations/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
import ensureOnlyMembersAsHosts from "~/pages/api/event-types/_utils/ensureOnlyMembersAsHosts";
|
||||
|
||||
@@ -235,8 +235,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
};
|
||||
}
|
||||
await checkPermissions(req, parsedBody);
|
||||
const eventType = await prisma.eventType.update({ where: { id }, data });
|
||||
return { event_type: schemaEventTypeReadPublic.parse(eventType) };
|
||||
const eventType = await prisma.eventType.update({ where: { id }, data, select: eventTypeSelect });
|
||||
return { event_type: eventType };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
||||
@@ -249,4 +249,4 @@ async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schema
|
||||
await checkTeamEventEditPermission(req, body);
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
export default patchHandler;
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
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 { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import { schemaQuerySlug } from "~/lib/validations/shared/querySlug";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
@@ -54,14 +54,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
userId: shouldUseUserId ? { in: userIds } : undefined,
|
||||
slug: slug, // slug will be undefined if not provided in query
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
hashedLink: { select: { link: true } },
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
select: eventTypeSelect,
|
||||
});
|
||||
// this really should return [], but backwards compatibility..
|
||||
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
|
||||
@@ -69,7 +62,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map(
|
||||
(eventType) => {
|
||||
const link = getCalLink(eventType);
|
||||
return schemaEventTypeReadPublic.parse({ ...eventType, link });
|
||||
return { ...eventType, link };
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
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 { Prisma } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type";
|
||||
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||
@@ -306,10 +307,10 @@ async function postHandler(req: NextApiRequest) {
|
||||
data.hosts = { createMany: { data: hosts } };
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
|
||||
const eventType = await prisma.eventType.create({ data, select: eventTypeSelect });
|
||||
|
||||
return {
|
||||
event_type: schemaEventTypeReadPublic.parse(eventType),
|
||||
event_type: eventType,
|
||||
message: "Event type created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export async function paymentById(
|
||||
if (safeQuery.success && method === "GET") {
|
||||
const userWithBookings = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { bookings: true },
|
||||
});
|
||||
await prisma.payment
|
||||
|
||||
@@ -75,7 +75,11 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = await prisma.schedule.findUniqueOrThrow({ where: { id }, include: { availability: true } });
|
||||
const data = await prisma.schedule.findUniqueOrThrow({
|
||||
where: { id },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { availability: true },
|
||||
});
|
||||
return { schedule: schemaSchedulePublic.parse(data) };
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
|
||||
/** Only OWNERS and ADMINS can edit teams */
|
||||
const _team = await prisma.team.findFirst({
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { members: true },
|
||||
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
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 { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
|
||||
const querySchema = z.object({
|
||||
teamId: z.coerce.number(),
|
||||
@@ -57,17 +57,11 @@ async function getHandler(req: NextApiRequest) {
|
||||
members: { some: { userId } },
|
||||
},
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
select: eventTypeSelect,
|
||||
};
|
||||
return {
|
||||
event_types: await prisma.eventType.findMany(args),
|
||||
};
|
||||
|
||||
const data = await prisma.eventType.findMany(args);
|
||||
return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
@@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
|
||||
const adminUserId = 1;
|
||||
const memberUserId = 10;
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => {
|
||||
vi.mock("@calcom/app-store/videoClient", () => {
|
||||
return {
|
||||
getRecordingsOfCalVideoByRoomName: vi.fn(),
|
||||
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import {
|
||||
getTranscriptsAccessLinkFromRecordingId,
|
||||
checkIfRoomNameMatchesInRecording,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
@@ -19,7 +19,7 @@ import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[reco
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => {
|
||||
vi.mock("@calcom/app-store/videoClient", () => {
|
||||
return {
|
||||
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
||||
checkIfRoomNameMatchesInRecording: vi.fn(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
@@ -16,7 +16,7 @@ import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get";
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => {
|
||||
vi.mock("@calcom/app-store/videoClient", () => {
|
||||
return {
|
||||
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/EventManager", () => ({
|
||||
vi.mock("@calcom/features/bookings/lib/EventManager", () => ({
|
||||
default: vi.fn().mockImplementation(() => ({
|
||||
reschedule: vi.fn().mockResolvedValue({
|
||||
results: [],
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
|
||||
describe("Error", async () => {
|
||||
test("Fails to remove event type if user is not OWNER/ADMIN of team associated with event type", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "DELETE",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -71,15 +71,18 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
// Assign userId to the request objects
|
||||
req.userId = memberUser;
|
||||
|
||||
await handler(req, res);
|
||||
expect(res.statusCode).toBe(403); // Check if the deletion was successful
|
||||
await expect(handler(req)).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
statusCode: 403,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", async () => {
|
||||
test("Removes event type if user is owner of team associated with event type", async () => {
|
||||
// Mocks for DELETE request
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "DELETE",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -90,8 +93,8 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
// Assign userId to the request objects
|
||||
req.userId = adminUser;
|
||||
|
||||
await handler(req, res);
|
||||
expect(res.statusCode).toBe(200); // Check if the deletion was successful
|
||||
const deletedEventType = await handler(req);
|
||||
expect(deletedEventType).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
|
||||
describe("GET /api/event-types/[id]", () => {
|
||||
describe("Errors", () => {
|
||||
test("Returns 403 if user not admin/team member/event owner", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -32,16 +32,40 @@ describe("GET /api/event-types/[id]", () => {
|
||||
);
|
||||
|
||||
req.userId = 333333;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
await expect(handler(req)).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
statusCode: 403,
|
||||
})
|
||||
);
|
||||
});
|
||||
test("Returns 404 if event type not found", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
id: 123456,
|
||||
},
|
||||
});
|
||||
|
||||
req.isSystemWideAdmin = true;
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
||||
|
||||
req.userId = 333333;
|
||||
|
||||
await expect(handler(req)).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
statusCode: 404,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", async () => {
|
||||
test("Returns event type if user is admin", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -57,17 +81,17 @@ describe("GET /api/event-types/[id]", () => {
|
||||
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = 333333;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.event_type.id).toEqual(eventTypeId);
|
||||
});
|
||||
|
||||
test("Returns event type if user is in team associated with event type", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const teamId = 9999;
|
||||
const userId = 333333;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -94,10 +118,10 @@ describe("GET /api/event-types/[id]", () => {
|
||||
|
||||
req.isSystemWideAdmin = false;
|
||||
req.userId = userId;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.event_type.id).toEqual(eventTypeId);
|
||||
expect(prismaMock.team.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: teamId,
|
||||
@@ -116,7 +140,7 @@ describe("GET /api/event-types/[id]", () => {
|
||||
test("Returns event type if user is the event type owner", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const userId = 333333;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -134,10 +158,10 @@ describe("GET /api/event-types/[id]", () => {
|
||||
|
||||
req.isSystemWideAdmin = false;
|
||||
req.userId = userId;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.event_type.id).toEqual(eventTypeId);
|
||||
expect(prismaMock.team.findFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@ RUN yarn workspace @calcom/api-v2 run generate-schemas
|
||||
RUN rm -rf apps/api/v2/node_modules
|
||||
RUN yarn install
|
||||
|
||||
RUN yarn workspace @calcom/platform-libraries run build
|
||||
|
||||
RUN yarn workspace @calcom/api-v2 run build
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.359",
|
||||
"@calcom/platform-libraries": "*",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -11,7 +11,12 @@ import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/se
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } 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 { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import {
|
||||
AuthOptionalUser,
|
||||
@@ -22,8 +27,8 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
||||
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 { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -101,6 +106,9 @@ export class BookingsController_2024_08_13 {
|
||||
|
||||
@Post("/")
|
||||
@UseGuards(OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Create a booking",
|
||||
description: `
|
||||
@@ -122,6 +130,9 @@ export class BookingsController_2024_08_13 {
|
||||
And 2 ways to book and event type belonging to a team:
|
||||
1. Provide \`eventTypeId\` in the request body.
|
||||
2. Provide \`eventTypeSlug\` and \`teamSlug\` and optionally \`organizationSlug\` if the team with the teamSlug is within an organization.
|
||||
|
||||
If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
`,
|
||||
})
|
||||
@ApiBody({
|
||||
@@ -161,7 +172,10 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Get("/:bookingUid")
|
||||
@UseGuards(BookingUidGuard)
|
||||
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get a booking",
|
||||
description: `\`:bookingUid\` can be
|
||||
@@ -170,10 +184,17 @@ export class BookingsController_2024_08_13 {
|
||||
|
||||
2. uid of one of the recurring booking recurrences
|
||||
|
||||
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).`,
|
||||
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
|
||||
|
||||
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
`,
|
||||
})
|
||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.getBooking(bookingUid);
|
||||
async getBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.getBooking(bookingUid, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -218,7 +239,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({ summary: "Get all bookings" })
|
||||
async getBookings(
|
||||
@Query() queryParams: GetBookingsInput_2024_08_13,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<GetBookingsOutput_2024_08_13> {
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
|
||||
@@ -236,7 +257,10 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reschedule")
|
||||
@UseGuards(BookingUidGuard)
|
||||
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Reschedule a booking",
|
||||
description: "Reschedule a booking or seated booking",
|
||||
@@ -248,17 +272,20 @@ export class BookingsController_2024_08_13 {
|
||||
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
|
||||
description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2).
|
||||
|
||||
If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.`,
|
||||
})
|
||||
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
|
||||
async rescheduleBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body(new RescheduleBookingInputPipe())
|
||||
body: RescheduleBookingInput,
|
||||
@Req() request: Request
|
||||
@Req() request: Request,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<RescheduleBookingOutput_2024_08_13> {
|
||||
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body);
|
||||
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body, user);
|
||||
await this.bookingsService.billRescheduledBooking(newBooking, bookingUid);
|
||||
|
||||
return {
|
||||
@@ -268,7 +295,10 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/cancel")
|
||||
@UseGuards(BookingUidGuard)
|
||||
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: "Cancel a booking",
|
||||
@@ -281,7 +311,11 @@ export class BookingsController_2024_08_13 {
|
||||
|
||||
\nCancelling recurring seated bookings:
|
||||
For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
|
||||
|
||||
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
`,
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
@@ -298,9 +332,10 @@ export class BookingsController_2024_08_13 {
|
||||
@Req() request: Request,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body(new CancelBookingInputPipe())
|
||||
body: CancelBookingInput
|
||||
body: CancelBookingInput,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<CancelBookingOutput_2024_08_13> {
|
||||
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body);
|
||||
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -342,7 +377,7 @@ export class BookingsController_2024_08_13 {
|
||||
})
|
||||
async reassignBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
|
||||
|
||||
@@ -392,7 +427,7 @@ export class BookingsController_2024_08_13 {
|
||||
})
|
||||
async confirmBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.confirmBooking(bookingUid, user);
|
||||
|
||||
@@ -414,7 +449,7 @@ export class BookingsController_2024_08_13 {
|
||||
async declineBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: DeclineBookingInput_2024_08_13,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.declineBooking(bookingUid, user, body.reason);
|
||||
|
||||
|
||||
+201
-53
@@ -437,61 +437,209 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should book an event type with attendees disabled", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
describe("book an event type with attendees disabled but auth provided", () => {
|
||||
let booking: CreateSeatedBookingOutput_2024_08_13;
|
||||
it("should book an event type with attendees disabled and no auth provided", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected seated booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
booking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected seated booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch booking and not have attendees because no auth provided", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${booking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(booking.start);
|
||||
expect(data.end).toEqual(booking.end);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("book an event type with attendees disabled but auth provided", () => {
|
||||
let booking: CreateSeatedBookingOutput_2024_08_13;
|
||||
|
||||
it("should book an event type with attendees disabled and owner auth provided", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", apiKeyString)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0].email).toEqual(emailAttendeeOne);
|
||||
expect(data.attendees[0].name).toEqual(nameAttendeeOne);
|
||||
expect(data.attendees[0].language).toEqual("it");
|
||||
expect(data.attendees[0].seatUid).toBeDefined();
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
booking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected seated booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch booking and have attendees", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${booking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", apiKeyString)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(booking.start);
|
||||
expect(data.end).toEqual(booking.end);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0].email).toEqual(emailAttendeeOne);
|
||||
expect(data.attendees[0].name).toEqual(nameAttendeeOne);
|
||||
expect(data.attendees[0].language).toEqual("it");
|
||||
expect(data.attendees[0].seatUid).toBeDefined();
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel seated booking", () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { getPagination } from "@/lib/pagination/pagination";
|
||||
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||
@@ -120,7 +121,10 @@ export class BookingsService_2024_08_13 {
|
||||
if (!eventType) {
|
||||
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
||||
}
|
||||
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
|
||||
const userIsEventTypeAdminOrOwner = authUser
|
||||
? await this.userIsEventTypeAdminOrOwner(authUser, eventType)
|
||||
: false;
|
||||
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser, userIsEventTypeAdminOrOwner);
|
||||
|
||||
if (eventType.schedulingType === "MANAGED") {
|
||||
throw new BadRequestException(
|
||||
@@ -140,13 +144,13 @@ export class BookingsService_2024_08_13 {
|
||||
await this.hasRequiredBookingFieldsResponses(body, eventType);
|
||||
|
||||
if (isRecurring && isSeated) {
|
||||
return await this.createRecurringSeatedBooking(request, body, eventType);
|
||||
return await this.createRecurringSeatedBooking(request, body, eventType, userIsEventTypeAdminOrOwner);
|
||||
}
|
||||
if (isRecurring && !isSeated) {
|
||||
return await this.createRecurringBooking(request, body, eventType);
|
||||
}
|
||||
if (isSeated) {
|
||||
return await this.createSeatedBooking(request, body, eventType);
|
||||
return await this.createSeatedBooking(request, body, eventType, userIsEventTypeAdminOrOwner);
|
||||
}
|
||||
|
||||
return await this.createRegularBooking(request, body, eventType);
|
||||
@@ -157,7 +161,8 @@ export class BookingsService_2024_08_13 {
|
||||
|
||||
async checkBookingRequiresAuthenticationSetting(
|
||||
eventType: EventTypeWithOwnerAndTeam,
|
||||
authUser: AuthOptionalUser
|
||||
authUser: AuthOptionalUser,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
if (!eventType.bookingRequiresAuthentication) return true;
|
||||
if (!authUser) {
|
||||
@@ -166,15 +171,23 @@ export class BookingsService_2024_08_13 {
|
||||
);
|
||||
}
|
||||
|
||||
if (!userIsEventTypeAdminOrOwner) {
|
||||
throw new ForbiddenException(
|
||||
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType) {
|
||||
const authUserId = authUser.id;
|
||||
const authUserRole = authUser.role;
|
||||
const eventTypeId = eventType.id;
|
||||
const teamId = eventType.teamId;
|
||||
const bookingUserId = eventType.owner?.id || null;
|
||||
const eventTypeOwnerId = eventType.userId || null;
|
||||
|
||||
if (authUserRole === "ADMIN") return;
|
||||
if (authUserRole === "ADMIN") return true;
|
||||
|
||||
if (bookingUserId === authUserId) return;
|
||||
if (eventTypeOwnerId === authUserId) return true;
|
||||
|
||||
if (eventTypeId) {
|
||||
const [isUserHost, isUserAssigned] = await Promise.all([
|
||||
@@ -182,7 +195,7 @@ export class BookingsService_2024_08_13 {
|
||||
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
|
||||
]);
|
||||
|
||||
if (isUserHost || isUserAssigned) return;
|
||||
if (isUserHost || isUserAssigned) return true;
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
@@ -190,19 +203,17 @@ export class BookingsService_2024_08_13 {
|
||||
authUserId,
|
||||
teamId
|
||||
);
|
||||
if (membership) return;
|
||||
if (membership) return true;
|
||||
}
|
||||
|
||||
if (
|
||||
bookingUserId &&
|
||||
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, bookingUserId))
|
||||
eventTypeOwnerId &&
|
||||
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId))
|
||||
) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ForbiddenException(
|
||||
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async getBookedEventType(body: CreateBookingInput) {
|
||||
@@ -476,7 +487,8 @@ export class BookingsService_2024_08_13 {
|
||||
async createRecurringSeatedBooking(
|
||||
request: Request,
|
||||
body: CreateRecurringBookingInput_2024_08_13,
|
||||
eventType: EventTypeWithOwnerAndTeam
|
||||
eventType: EventTypeWithOwnerAndTeam,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
|
||||
const bookings = await handleNewRecurringBooking({
|
||||
@@ -491,7 +503,8 @@ export class BookingsService_2024_08_13 {
|
||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||
});
|
||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
|
||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" })),
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
}
|
||||
|
||||
@@ -528,7 +541,8 @@ export class BookingsService_2024_08_13 {
|
||||
async createSeatedBooking(
|
||||
request: Request,
|
||||
body: CreateBookingInput_2024_08_13,
|
||||
eventType: EventTypeWithOwnerAndTeam
|
||||
eventType: EventTypeWithOwnerAndTeam,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
||||
try {
|
||||
@@ -554,7 +568,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputCreateSeatedBooking(databaseBooking, booking.seatReferenceUid || "");
|
||||
return this.outputService.getOutputCreateSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatReferenceUid || "",
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "booking_seats_full_error") {
|
||||
@@ -565,8 +583,12 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
async getBooking(uid: string) {
|
||||
async getBooking(uid: string, authUser: AuthOptionalUser) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
|
||||
const userIsEventTypeAdminOrOwner =
|
||||
authUser && booking?.eventType
|
||||
? await this.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
|
||||
: false;
|
||||
|
||||
if (booking) {
|
||||
const isRecurring = !!booking.recurringEventId;
|
||||
@@ -576,13 +598,12 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputRecurringBooking(booking);
|
||||
}
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBooking(
|
||||
booking,
|
||||
!!booking.eventType?.seatsShowAttendees
|
||||
);
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!booking.eventType?.seatsShowAttendees;
|
||||
return this.outputService.getOutputRecurringSeatedBooking(booking, showAttendees);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!booking.eventType?.seatsShowAttendees;
|
||||
return this.outputService.getOutputSeatedBooking(booking, showAttendees);
|
||||
}
|
||||
return this.outputService.getOutputBooking(booking);
|
||||
}
|
||||
@@ -594,10 +615,9 @@ export class BookingsService_2024_08_13 {
|
||||
const ids = recurringBooking.map((booking) => booking.id);
|
||||
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
|
||||
if (isRecurringSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBookings(
|
||||
ids,
|
||||
!!recurringBooking[0].eventType?.seatsShowAttendees
|
||||
);
|
||||
const showAttendees =
|
||||
userIsEventTypeAdminOrOwner || !!recurringBooking[0].eventType?.seatsShowAttendees;
|
||||
return this.outputService.getOutputRecurringSeatedBookings(ids, showAttendees);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputRecurringBookings(ids);
|
||||
@@ -663,19 +683,9 @@ export class BookingsService_2024_08_13 {
|
||||
if (isRecurring && !isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
|
||||
} else if (isRecurring && isSeated) {
|
||||
formattedBookings.push(
|
||||
this.outputService.getOutputRecurringSeatedBooking(
|
||||
formatted,
|
||||
!!formatted.eventType?.seatsShowAttendees
|
||||
)
|
||||
);
|
||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted, true));
|
||||
} else if (isSeated) {
|
||||
formattedBookings.push(
|
||||
await this.outputService.getOutputSeatedBooking(
|
||||
formatted,
|
||||
!!formatted.eventType?.seatsShowAttendees
|
||||
)
|
||||
);
|
||||
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted, true));
|
||||
} else {
|
||||
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
||||
}
|
||||
@@ -718,7 +728,12 @@ export class BookingsService_2024_08_13 {
|
||||
return queryParamsAttendeeEmail;
|
||||
}
|
||||
|
||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
||||
async rescheduleBooking(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: RescheduleBookingInput,
|
||||
authUser: AuthOptionalUser
|
||||
) {
|
||||
try {
|
||||
await this.canRescheduleBooking(bookingUid);
|
||||
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
||||
@@ -747,6 +762,10 @@ export class BookingsService_2024_08_13 {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
const userIsEventTypeAdminOrOwner =
|
||||
authUser && databaseBooking.eventType
|
||||
? await this.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
|
||||
: false;
|
||||
const isRecurring = !!databaseBooking.recurringEventId;
|
||||
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;
|
||||
|
||||
@@ -756,13 +775,15 @@ export class BookingsService_2024_08_13 {
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
booking?.seatReferenceUid || ""
|
||||
booking?.seatReferenceUid || "",
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputCreateSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatReferenceUid || ""
|
||||
booking.seatReferenceUid || "",
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
}
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
@@ -790,7 +811,12 @@ export class BookingsService_2024_08_13 {
|
||||
return booking;
|
||||
}
|
||||
|
||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
||||
async cancelBooking(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: CancelBookingInput,
|
||||
authUser: AuthOptionalUser
|
||||
) {
|
||||
if (this.inputService.isCancelSeatedBody(body)) {
|
||||
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
|
||||
|
||||
@@ -821,13 +847,13 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) {
|
||||
return this.getAllRecurringBookingsByIndividualUid(bookingUid);
|
||||
return this.getAllRecurringBookingsByIndividualUid(bookingUid, authUser);
|
||||
}
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
return this.getBooking(bookingUid, authUser);
|
||||
}
|
||||
|
||||
private async getAllRecurringBookingsByIndividualUid(bookingUid: string) {
|
||||
private async getAllRecurringBookingsByIndividualUid(bookingUid: string, authUser: AuthOptionalUser) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
const recurringBookingUid = booking?.recurringEventId;
|
||||
if (!recurringBookingUid) {
|
||||
@@ -836,7 +862,7 @@ export class BookingsService_2024_08_13 {
|
||||
);
|
||||
}
|
||||
|
||||
return await this.getBooking(recurringBookingUid);
|
||||
return await this.getBooking(recurringBookingUid, authUser);
|
||||
}
|
||||
|
||||
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
|
||||
@@ -1003,7 +1029,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
async confirmBooking(bookingUid: string, requestUser: UserWithProfile) {
|
||||
async confirmBooking(bookingUid: string, requestUser: ApiAuthGuardUser) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
@@ -1032,10 +1058,10 @@ export class BookingsService_2024_08_13 {
|
||||
},
|
||||
});
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
return this.getBooking(bookingUid, requestUser);
|
||||
}
|
||||
|
||||
async declineBooking(bookingUid: string, requestUser: UserWithProfile, reason?: string) {
|
||||
async declineBooking(bookingUid: string, requestUser: ApiAuthGuardUser, reason?: string) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
@@ -1065,7 +1091,7 @@ export class BookingsService_2024_08_13 {
|
||||
},
|
||||
});
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
return this.getBooking(bookingUid, requestUser);
|
||||
}
|
||||
|
||||
async getCalendarLinks(bookingUid: string): Promise<CalendarLink[]> {
|
||||
|
||||
@@ -229,7 +229,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail: databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
|
||||
cancelledByEmail:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
@@ -269,12 +270,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
async getOutputCreateSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
seatUid: string,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
||||
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
|
||||
databaseBooking,
|
||||
!!databaseBooking.eventType?.seatsShowAttendees
|
||||
);
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!databaseBooking.eventType?.seatsShowAttendees;
|
||||
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking, showAttendees);
|
||||
return { ...getSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
@@ -377,7 +377,10 @@ export class OutputBookingsService_2024_08_13 {
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
|
||||
async getOutputCreateRecurringSeatedBookings(bookings: { uid: string; seatUid: string }[]) {
|
||||
async getOutputCreateRecurringSeatedBookings(
|
||||
bookings: { uid: string; seatUid: string }[],
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of bookings) {
|
||||
@@ -386,7 +389,13 @@ export class OutputBookingsService_2024_08_13 {
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
transformed.push(this.getOutputCreateRecurringSeatedBooking(databaseBooking, booking.seatUid));
|
||||
transformed.push(
|
||||
this.getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatUid,
|
||||
userIsEventTypeAdminOrOwner
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
@@ -394,11 +403,13 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
seatUid: string,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
): CreateRecurringSeatedBookingOutput_2024_08_13 {
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!databaseBooking.eventType?.seatsShowAttendees;
|
||||
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
!!databaseBooking.eventType?.seatsShowAttendees
|
||||
showAttendees
|
||||
);
|
||||
return { ...getRecurringSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
|
||||
|
||||
import { ApiAuthGuardRequest } from "./api-auth.strategy";
|
||||
import { ApiAuthGuardRequest, ONLY_CLIENT_ID_PROVIDED_MESSAGE } from "./api-auth.strategy";
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
@@ -244,6 +244,29 @@ describe("ApiAuthStrategy", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if only OAuth ID is provided", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
[X_CAL_CLIENT_ID]: `${oAuthClient.id}gibberish`,
|
||||
},
|
||||
get: (key: string) => ({ origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
try {
|
||||
await strategy.authenticate(request);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
expect(error.getStatus()).toEqual(401);
|
||||
expect(error.message).toContain(ONLY_CLIENT_ID_PROVIDED_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if OAuth ID is invalid", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import type {
|
||||
import type { User, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Event Types Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
describe("User Authentication - User is Org Admin but not team admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
+1238
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -134,7 +134,7 @@ export async function getCRMData(
|
||||
|
||||
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
|
||||
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
|
||||
"@calcom/lib/server/getTeamMemberEmailFromCrm"
|
||||
"@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm"
|
||||
);
|
||||
const {
|
||||
email,
|
||||
|
||||
@@ -41,8 +41,8 @@ import type {
|
||||
import type { zodRoutes } from "@calcom/app-store/routing-forms/zod";
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import type { EventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer";
|
||||
import { areTheySiblingEntities } from "@calcom/lib/entityPermissionUtils.shared";
|
||||
import type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { buildEmptyQueryValue, raqbQueryValueUtils } from "@calcom/lib/raqb/raqbUtils";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useRedirectToLoginIfUnauthenticated } from "@calcom/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated";
|
||||
|
||||
export function RoutingFormAuthGuard({ children }: { children: React.ReactNode }) {
|
||||
useRedirectToLoginIfUnauthenticated();
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,7 +1,22 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import FormProvider from "./FormProvider";
|
||||
import { RoutingFormAuthGuard } from "./RoutingFormAuthGuard";
|
||||
|
||||
export default async function Layout({ children }: { children: ReactNode }) {
|
||||
return <FormProvider>{children}</FormProvider>;
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
params: { pages: string[] };
|
||||
}
|
||||
|
||||
export default async function Layout({ children, params }: LayoutProps) {
|
||||
const isPublic = params.pages?.[0] === "routing-link";
|
||||
if (isPublic) {
|
||||
return <FormProvider>{children}</FormProvider>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoutingFormAuthGuard>
|
||||
<FormProvider>{children}</FormProvider>
|
||||
</RoutingFormAuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import { z } from "zod";
|
||||
|
||||
import { EventTypeWebWrapper } from "@calcom/atoms/event-types/wrappers/EventTypeWebWrapper";
|
||||
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 { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -40,6 +44,95 @@ const getCachedEventType = unstable_cache(
|
||||
{ revalidate: 3600 } // Cache for 1 hour
|
||||
);
|
||||
|
||||
const getEventPermissions = async (userId: number, teamId: number | null) => {
|
||||
// Personal event - has all perms
|
||||
if (!teamId)
|
||||
return {
|
||||
eventTypes: {
|
||||
canRead: true,
|
||||
canCreate: true,
|
||||
canUpdate: true,
|
||||
canDelete: true,
|
||||
},
|
||||
workflows: {
|
||||
canRead: true,
|
||||
canCreate: true,
|
||||
canUpdate: true,
|
||||
canDelete: true,
|
||||
},
|
||||
};
|
||||
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) throw new Error("Membership not found");
|
||||
|
||||
const [eventTypePermissions, workflowPermissions] = await Promise.all([
|
||||
getResourcePermissions({
|
||||
userId,
|
||||
teamId,
|
||||
resource: Resource.EventType,
|
||||
userRole: membership.role,
|
||||
fallbackRoles: {
|
||||
read: {
|
||||
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
update: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
delete: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
create: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
},
|
||||
}),
|
||||
getResourcePermissions({
|
||||
userId,
|
||||
teamId,
|
||||
resource: Resource.Workflow,
|
||||
userRole: membership.role,
|
||||
fallbackRoles: {
|
||||
read: {
|
||||
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
update: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
delete: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
create: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
eventTypes: {
|
||||
canRead: eventTypePermissions.canRead,
|
||||
canCreate: eventTypePermissions.canCreate,
|
||||
canUpdate: eventTypePermissions.canEdit,
|
||||
canDelete: eventTypePermissions.canDelete,
|
||||
},
|
||||
workflows: {
|
||||
canRead: workflowPermissions.canRead,
|
||||
canCreate: workflowPermissions.canCreate,
|
||||
canUpdate: workflowPermissions.canEdit,
|
||||
canDelete: workflowPermissions.canDelete,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ServerPage = async ({ params }: PageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (!session?.user?.id) {
|
||||
@@ -59,7 +152,10 @@ const ServerPage = async ({ params }: PageProps) => {
|
||||
throw new Error("This event type does not exist");
|
||||
}
|
||||
|
||||
return <EventTypeWebWrapper data={data} id={eventTypeId} />;
|
||||
// Fetch permissions for the event type's team
|
||||
const permissions = await getEventPermissions(session.user.id, data.eventType.teamId);
|
||||
|
||||
return <EventTypeWebWrapper data={data} id={eventTypeId} permissions={permissions} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
@@ -3,7 +3,6 @@ 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";
|
||||
@@ -21,14 +20,5 @@ export async function checkInsightsPagePermission() {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
const hasPermission = await hasInsightsPermission({
|
||||
userId: session.user.id,
|
||||
organizationId: session.user.org?.id,
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { headers } from "next/headers";
|
||||
import Script from "next/script";
|
||||
|
||||
import { NavigationPermissionsProvider } from "@calcom/features/shell/permissions/NavigationPermissionsProvider";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
export default async function PageWrapperLayout({ children }: { children: React.ReactNode }) {
|
||||
@@ -23,7 +21,7 @@ export default async function PageWrapperLayout({ children }: { children: React.
|
||||
].filter((script): script is { id: string; script: string } => !!script.script);
|
||||
|
||||
return (
|
||||
<NavigationPermissionsProvider>
|
||||
<>
|
||||
<PageWrapper requiresLicense={false} nonce={nonce}>
|
||||
{children}
|
||||
{scripts.map((script) => (
|
||||
@@ -38,6 +36,6 @@ export default async function PageWrapperLayout({ children }: { children: React.
|
||||
/>
|
||||
))}
|
||||
</PageWrapper>
|
||||
</NavigationPermissionsProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||
<div className="col-span-2 mb-6 font-semibold">
|
||||
<Price
|
||||
currency={paymentAppData.currency}
|
||||
price={paymentAppData.price}
|
||||
price={props.payment?.amount ?? paymentAppData.price}
|
||||
displayAlternateSymbol={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-3
@@ -6,7 +6,6 @@ import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -26,11 +25,10 @@ const WebhooksViewServerWrapper = async () => {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
||||
const caller = await createRouterCaller(webhookRouter);
|
||||
const data = await caller.getByViewer();
|
||||
|
||||
return <WebhooksView data={data} isAdmin={isAdmin} />;
|
||||
return <WebhooksView data={data} />;
|
||||
};
|
||||
|
||||
export default WebhooksViewServerWrapper;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ const Page = async () => {
|
||||
<SettingsHeader
|
||||
title={t("billing")}
|
||||
description={t("manage_billing_description")}
|
||||
borderInShellHeader={true}>
|
||||
borderInShellHeader={false}>
|
||||
<BillingView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
|
||||
+6
-6
@@ -107,9 +107,9 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
|
||||
const { isAdvancedMode, permissions, color } = form.watch();
|
||||
|
||||
const filteredResources = useMemo(() => {
|
||||
const { filteredResources, scopedRegistry } = useMemo(() => {
|
||||
const scopedRegistry = getPermissionsForScope(scope);
|
||||
return Object.keys(scopedRegistry).filter((resource) =>
|
||||
const filteredResources = Object.keys(scopedRegistry).filter((resource) =>
|
||||
t(
|
||||
scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]]
|
||||
?.i18nKey || ""
|
||||
@@ -117,6 +117,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
);
|
||||
return { filteredResources, scopedRegistry };
|
||||
}, [searchQuery, t, scope]);
|
||||
|
||||
const createMutation = trpc.viewer.pbac.createRole.useMutation({
|
||||
@@ -212,7 +213,6 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
disabled={isSystemRole}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filteredResources.map((resource) => (
|
||||
<AdvancedPermissionGroup
|
||||
key={resource}
|
||||
@@ -222,7 +222,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
disabled={isSystemRole}
|
||||
scope={scope}
|
||||
/>
|
||||
))}
|
||||
))}{" "}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-muted rounded-xl p-1">
|
||||
@@ -237,7 +237,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-default border-subtle divide-subtle divide-y rounded-[10px] border">
|
||||
{Object.keys(getPermissionsForScope(scope)).map((resource) => (
|
||||
{Object.keys(scopedRegistry).map((resource) => (
|
||||
<SimplePermissionItem
|
||||
key={resource}
|
||||
resource={resource}
|
||||
@@ -247,7 +247,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
scope={scope}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>{" "}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+10
-4
@@ -1,7 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import type { Resource, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import {
|
||||
Scope,
|
||||
PERMISSION_REGISTRY,
|
||||
getPermissionsForScope,
|
||||
} from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { ToggleGroup } from "@calcom/ui/components/form";
|
||||
|
||||
@@ -21,11 +25,13 @@ export function SimplePermissionItem({
|
||||
permissions,
|
||||
onChange,
|
||||
disabled,
|
||||
scope,
|
||||
scope = Scope.Organization,
|
||||
}: SimplePermissionItemProps) {
|
||||
const { t } = useLocale();
|
||||
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope);
|
||||
const scopedRegistry = getPermissionsForScope(scope);
|
||||
|
||||
const registry = scopedRegistry || PERMISSION_REGISTRY;
|
||||
const isAllResources = resource === "*";
|
||||
const options = isAllResources
|
||||
? [
|
||||
@@ -41,7 +47,7 @@ export function SimplePermissionItem({
|
||||
return (
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-default text-sm font-medium leading-none">
|
||||
{t(PERMISSION_REGISTRY[resource as Resource]._resource?.i18nKey || resource)}
|
||||
{t(registry[resource as Resource]._resource?.i18nKey || resource)}
|
||||
</span>
|
||||
<ToggleGroup
|
||||
onValueChange={(val) => {
|
||||
|
||||
+17
-10
@@ -1,8 +1,5 @@
|
||||
import { CrudAction, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import {
|
||||
PERMISSION_REGISTRY,
|
||||
getPermissionsForScope,
|
||||
} from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getPermissionsForScope } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import {
|
||||
getTransitiveDependencies,
|
||||
getTransitiveDependents,
|
||||
@@ -52,7 +49,8 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
return permissions.includes("*.*") ? "all" : "none";
|
||||
}
|
||||
|
||||
const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY];
|
||||
const scopedRegistry = getPermissionsForScope(scope);
|
||||
const resourceConfig = scopedRegistry[resource as keyof typeof scopedRegistry];
|
||||
if (!resourceConfig) return "none";
|
||||
|
||||
// Check if global all permissions (*.*) is present
|
||||
@@ -111,7 +109,19 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
allResourcePerms = Object.keys(resourceConfig)
|
||||
.filter((action) => !action.startsWith("_"))
|
||||
.map((action) => `${resource}.${action}`);
|
||||
|
||||
// Add the resource permissions
|
||||
newPermissions.push(...allResourcePerms);
|
||||
|
||||
// Add all transitive dependencies for each permission
|
||||
allResourcePerms.forEach((perm) => {
|
||||
const dependencies = getTransitiveDependencies(perm, scope);
|
||||
dependencies.forEach((dep) => {
|
||||
if (!newPermissions.includes(dep)) {
|
||||
newPermissions.push(dep);
|
||||
}
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -132,15 +142,12 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
// First, remove *.* since we're modifying individual permissions
|
||||
let newPermissions = currentPermissions.filter((p) => p !== "*.*");
|
||||
|
||||
// Parse the permission to get resource and action
|
||||
const [resource, action] = permission.split(".");
|
||||
|
||||
if (enabled) {
|
||||
// Add the requested permission
|
||||
newPermissions.push(permission);
|
||||
|
||||
// Add all transitive dependencies
|
||||
const dependencies = getTransitiveDependencies(permission);
|
||||
const dependencies = getTransitiveDependencies(permission, scope);
|
||||
dependencies.forEach((dependency) => {
|
||||
if (!newPermissions.includes(dependency)) {
|
||||
newPermissions.push(dependency);
|
||||
@@ -151,7 +158,7 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
newPermissions = newPermissions.filter((p) => p !== permission);
|
||||
|
||||
// Remove all transitive dependents
|
||||
const dependents = getTransitiveDependents(permission);
|
||||
const dependents = getTransitiveDependents(permission, scope);
|
||||
dependents.forEach((dependent) => {
|
||||
newPermissions = newPermissions.filter((p) => p !== dependent);
|
||||
});
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { Resource, CrudAction, Scope } from "@calcom/features/pbac/domain/types/
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { RoleService } from "@calcom/features/pbac/services/role.service";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { validPassword } from "@calcom/features/auth/lib/validPassword";
|
||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid";
|
||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import {
|
||||
getAllTranscriptsAccessLinkFromMeetingId,
|
||||
submitBatchProcessorTranscriptionJob,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { generateVideoToken } from "@calcom/lib/videoTokens";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { getBooking } from "@calcom/web/lib/daily-webhook/getBooking";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient";
|
||||
import { verifyVideoToken } from "@calcom/lib/videoTokens";
|
||||
|
||||
import { GET } from "../route";
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => ({
|
||||
vi.mock("@calcom/app-store/videoClient", () => ({
|
||||
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient";
|
||||
import { verifyVideoToken } from "@calcom/lib/videoTokens";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@@ -7,9 +7,10 @@ import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { locationsResolver } from "@calcom/app-store/locations";
|
||||
import NoSSR from "@calcom/lib/components/NoSSR";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { LocationObject } from "@calcom/lib/location";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetaDataSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
@@ -188,7 +189,7 @@ const EventTypeGroup = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
|
||||
const ConfigureStepCardContent: FC<ConfigureStepCardProps> = (props) => {
|
||||
const { loading, formPortalRef, handleSetUpLater } = props;
|
||||
const { t } = useLocale();
|
||||
const { control, watch } = useFormContext<TEventTypesForm>();
|
||||
@@ -212,6 +213,7 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
|
||||
);
|
||||
|
||||
const [submit, setSubmit] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const allUpdated = updatedEventTypesStatus.every((item) => item.every((iitem) => iitem.updated));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -221,60 +223,73 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
|
||||
}
|
||||
}, [submit, allUpdated, mainForSubmitRef]);
|
||||
|
||||
return (
|
||||
formPortalRef?.current &&
|
||||
createPortal(
|
||||
<div className="mt-8">
|
||||
{fields.map((group, groupIndex) => (
|
||||
<div key={group.fieldId}>
|
||||
{eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && (
|
||||
<div className="mb-2 mt-4 flex items-center">
|
||||
<Avatar
|
||||
alt=""
|
||||
imageSrc={group.image} // if no image, use default avatar
|
||||
size="md"
|
||||
className="inline-flex justify-center"
|
||||
/>
|
||||
<p className="text-subtle block pl-2">{group.slug}</p>
|
||||
</div>
|
||||
)}
|
||||
<EventTypeGroup
|
||||
groupIndex={groupIndex}
|
||||
setUpdatedEventTypesStatus={setUpdatedEventTypesStatus}
|
||||
submitRefs={submitRefs}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<button form="outer-event-type-form" type="submit" className="hidden" ref={mainForSubmitRef}>
|
||||
Save
|
||||
</button>
|
||||
<Button
|
||||
className="text-md mt-6 w-full justify-center"
|
||||
type="button"
|
||||
data-testid="configure-step-save"
|
||||
onClick={() => {
|
||||
submitRefs.current.forEach((ref) => ref?.click());
|
||||
setSubmit(true);
|
||||
}}
|
||||
loading={loading}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
<Button
|
||||
color="minimal"
|
||||
data-testid="set-up-later"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleSetUpLater();
|
||||
}}
|
||||
className="mt-8 cursor-pointer px-4 py-2 font-sans text-sm font-medium">
|
||||
{t("set_up_later")}
|
||||
</Button>
|
||||
if (!formPortalRef?.current || !mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div className="mt-8">
|
||||
{fields.map((group, groupIndex) => (
|
||||
<div key={group.fieldId}>
|
||||
{eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && (
|
||||
<div className="mb-2 mt-4 flex items-center">
|
||||
<Avatar
|
||||
alt=""
|
||||
imageSrc={group.image} // if no image, use default avatar
|
||||
size="md"
|
||||
className="inline-flex justify-center"
|
||||
/>
|
||||
<p className="text-subtle block pl-2">{group.slug}</p>
|
||||
</div>
|
||||
)}
|
||||
<EventTypeGroup
|
||||
groupIndex={groupIndex}
|
||||
setUpdatedEventTypesStatus={setUpdatedEventTypesStatus}
|
||||
submitRefs={submitRefs}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
formPortalRef?.current
|
||||
)
|
||||
))}
|
||||
<button form="outer-event-type-form" type="submit" className="hidden" ref={mainForSubmitRef}>
|
||||
Save
|
||||
</button>
|
||||
<Button
|
||||
className="text-md mt-6 w-full justify-center"
|
||||
type="button"
|
||||
data-testid="configure-step-save"
|
||||
onClick={() => {
|
||||
submitRefs.current.forEach((ref) => ref?.click());
|
||||
setSubmit(true);
|
||||
}}
|
||||
loading={loading}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
<Button
|
||||
color="minimal"
|
||||
data-testid="set-up-later"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleSetUpLater();
|
||||
}}
|
||||
className="mt-8 cursor-pointer px-4 py-2 font-sans text-sm font-medium">
|
||||
{t("set_up_later")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>,
|
||||
formPortalRef.current
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
|
||||
return (
|
||||
<NoSSR>
|
||||
<ConfigureStepCardContent {...props} />
|
||||
</NoSSR>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, type FC } from "react";
|
||||
|
||||
import { EventTypeAppSettings } from "@calcom/app-store/_components/EventTypeAppSettingsInterface";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import useAppsData from "@calcom/lib/hooks/useAppsData";
|
||||
import useAppsData from "@calcom/features/apps/hooks/useAppsData";
|
||||
|
||||
import type { ConfigureStepCardProps } from "@components/apps/installation/ConfigureStepCard";
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
showToast(t("booking_confirmation_success"), "success");
|
||||
}
|
||||
utils.viewer.bookings.invalidate();
|
||||
utils.viewer.me.bookingUnconfirmedCount.invalidate();
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("booking_confirmation_failed"), "error");
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import type { TeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
import { UserAvatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ROUTING_FORM_QUEUED_RESPONSE_ID_QUERY_STRING,
|
||||
} from "@calcom/app-store/routing-forms/lib/constants";
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/lib/server/getTeamMemberEmailFromCrm";
|
||||
import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
vi.mock("@calcom/app-store/routing-forms/appBookingFormHandler", () => ({
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import type { LocationObject } from "@calcom/lib/location";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useCheckTeamBilling = (teamId?: number | null, isPlatformTeam?: boo
|
||||
return data.data;
|
||||
},
|
||||
enabled: !!teamId && !!isPlatformTeam,
|
||||
staleTime: 5000,
|
||||
});
|
||||
|
||||
return isTeamBilledAlready;
|
||||
|
||||
@@ -4,14 +4,12 @@ import { URLSearchParams } from "url";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getFullName } from "@calcom/features/form-builder/utils";
|
||||
import { determineReschedulePreventionRedirect } from "@calcom/features/bookings/lib/reschedule/determineReschedulePreventionRedirect";
|
||||
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { getSafe } from "@calcom/lib/getSafe";
|
||||
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
@@ -63,6 +61,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
allowReschedulingCancelledBookings: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
slug: true,
|
||||
},
|
||||
@@ -98,7 +97,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
notFound: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||
|
||||
const userRepo = new UserRepository(prisma);
|
||||
@@ -112,59 +110,36 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
profileEnrichedBookingUser: enrichedBookingUser,
|
||||
});
|
||||
|
||||
const isForcedRescheduleForCancelledBooking = allowRescheduleForCancelledBooking;
|
||||
// If booking is already REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
|
||||
// If the booking is CANCELLED and allowRescheduleForCancelledBooking is false, we redirect the user to the original event link.
|
||||
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
|
||||
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
|
||||
// This comes from query param and thus is considered forced
|
||||
const canBookThroughCancelledBookingRescheduleLink = booking.eventType?.allowReschedulingCancelledBookings;
|
||||
const isNonRescheduleableBooking =
|
||||
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
|
||||
|
||||
if (isDisabledRescheduling) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/booking/${uid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) {
|
||||
const canReschedule =
|
||||
booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink;
|
||||
return {
|
||||
redirect: {
|
||||
destination: canReschedule ? eventUrl : `/booking/${uid}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
|
||||
// TODO: Show something in UI to let user know that this booking is not rescheduleable
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
notFound: true;
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date();
|
||||
if (isBookingInPast && !eventType.allowReschedulingPastBookings) {
|
||||
const destinationUrlSearchParams = new URLSearchParams();
|
||||
const responses = bookingSeat ? getSafe<string>(bookingSeat.data, ["responses"]) : booking.responses;
|
||||
const name = getFullName(getSafe<string | { firstName: string; lastName?: string }>(responses, ["name"]));
|
||||
const email = getSafe<string>(responses, ["email"]);
|
||||
// Check if reschedule should be prevented based on booking status and event type settings
|
||||
const reschedulePreventionRedirectUrl = determineReschedulePreventionRedirect({
|
||||
booking: {
|
||||
uid,
|
||||
status: booking.status,
|
||||
endTime: booking.endTime,
|
||||
responses: booking.responses,
|
||||
eventType: {
|
||||
disableRescheduling: !!eventType?.disableRescheduling,
|
||||
allowReschedulingPastBookings: eventType.allowReschedulingPastBookings,
|
||||
allowBookingFromCancelledBookingReschedule: !!eventType.allowReschedulingCancelledBookings,
|
||||
teamId: eventType.team?.id ?? null,
|
||||
},
|
||||
},
|
||||
eventUrl,
|
||||
forceRescheduleForCancelledBooking: allowRescheduleForCancelledBooking,
|
||||
bookingSeat,
|
||||
});
|
||||
|
||||
if (name) destinationUrlSearchParams.set("name", name);
|
||||
if (email) destinationUrlSearchParams.set("email", email);
|
||||
|
||||
const searchParamsString = destinationUrlSearchParams.toString();
|
||||
if (reschedulePreventionRedirectUrl) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: searchParamsString ? `${eventUrl}?${searchParamsString}` : eventUrl,
|
||||
destination: reschedulePreventionRedirectUrl,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
|
||||
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
|
||||
"@calcom/lib/server/getTeamMemberEmailFromCrm"
|
||||
"@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm"
|
||||
);
|
||||
const {
|
||||
email,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
|
||||
import { getAppOnboardingUrl } from "@calcom/lib/apps/getAppOnboardingUrl";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { LocationObject } from "@calcom/lib/location";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-util
|
||||
import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||
import { shouldHideBrandingForEvent } from "@calcom/lib/hideBranding";
|
||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { parseEventTypeColor } from "@calcom/lib/isEventTypeColor";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -954,6 +954,24 @@ export const EventTypesCTA = ({ userEventGroupsData }: Omit<Props, "user">) => {
|
||||
userEventGroupsData?.profiles
|
||||
?.filter((profile) => !profile.readOnly)
|
||||
?.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
?.filter((profile) => {
|
||||
// For personal profiles (teamId is null), always allow creation
|
||||
if (!profile.teamId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For team profiles, check if user has eventType.create permission
|
||||
// This will be populated by the server-side PBAC check
|
||||
// Fallback to role-based check (admin/owner) if canCreateEventTypes is not set
|
||||
if (profile.canCreateEventTypes !== undefined) {
|
||||
return profile.canCreateEventTypes;
|
||||
}
|
||||
|
||||
// Fallback: allow admin and owner roles
|
||||
return (
|
||||
profile.membershipRole === MembershipRole.ADMIN || profile.membershipRole === MembershipRole.OWNER
|
||||
);
|
||||
})
|
||||
?.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
import {
|
||||
DataTableProvider,
|
||||
DataTableFilters,
|
||||
@@ -7,6 +9,7 @@ import {
|
||||
ColumnFilterType,
|
||||
type FilterableColumn,
|
||||
} from "@calcom/features/data-table";
|
||||
import { useDataTable } from "@calcom/features/data-table/hooks/useDataTable";
|
||||
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
||||
import {
|
||||
AverageEventDurationChart,
|
||||
@@ -27,11 +30,13 @@ import {
|
||||
TimezoneBadge,
|
||||
} from "@calcom/features/insights/components/booking";
|
||||
import { InsightsOrgTeamsProvider } from "@calcom/features/insights/context/InsightsOrgTeamsProvider";
|
||||
import { DateTargetSelector, type DateTarget } from "@calcom/features/insights/filters/DateTargetSelector";
|
||||
import { Download } from "@calcom/features/insights/filters/Download";
|
||||
import { OrgTeamsFilter } from "@calcom/features/insights/filters/OrgTeamsFilter";
|
||||
import { useInsightsBookings } from "@calcom/features/insights/hooks/useInsightsBookings";
|
||||
import { useInsightsOrgTeams } from "@calcom/features/insights/hooks/useInsightsOrgTeams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
|
||||
export default function InsightsPage({ timeZone }: { timeZone: string }) {
|
||||
return (
|
||||
@@ -49,10 +54,26 @@ const createdAtColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_R
|
||||
type: ColumnFilterType.DATE_RANGE,
|
||||
};
|
||||
|
||||
const startTimeColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_RANGE }> = {
|
||||
id: "startTime",
|
||||
title: "startTime",
|
||||
type: ColumnFilterType.DATE_RANGE,
|
||||
};
|
||||
|
||||
function InsightsPageContent() {
|
||||
const { t } = useLocale();
|
||||
const { table } = useInsightsBookings();
|
||||
const { isAll, teamId, userId } = useInsightsOrgTeams();
|
||||
const { removeFilter } = useDataTable();
|
||||
const [dateTarget, _setDateTarget] = useState<"startTime" | "createdAt">("startTime");
|
||||
|
||||
const setDateTarget = useCallback(
|
||||
(target: "startTime" | "createdAt") => {
|
||||
_setDateTarget(target);
|
||||
removeFilter(target === "startTime" ? "createdAt" : "startTime");
|
||||
},
|
||||
[_setDateTarget, removeFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -63,10 +84,16 @@ function InsightsPageContent() {
|
||||
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
|
||||
<DataTableFilters.ActiveFilters table={table} />
|
||||
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
|
||||
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
|
||||
<DataTableFilters.ClearFiltersButton exclude={["startTime", "createdAt"]} />
|
||||
<div className="grow" />
|
||||
<Download />
|
||||
<DateRangeFilter column={createdAtColumn} />
|
||||
<ButtonGroup combined>
|
||||
<DateRangeFilter
|
||||
column={dateTarget === "startTime" ? startTimeColumn : createdAtColumn}
|
||||
options={{ convertToTimeZone: true }}
|
||||
/>
|
||||
<DateTargetSelector value={dateTarget as DateTarget} onChange={setDateTarget} />
|
||||
</ButtonGroup>
|
||||
<TimezoneBadge />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -77,21 +77,26 @@ const BillingView = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-8 text-sm sm:space-y-8">
|
||||
<CtaRow title={t("view_and_manage_billing_details")} description={t("view_and_edit_billing_details")}>
|
||||
<Button color="primary" href={billingHref} target="_blank" EndIcon="external-link">
|
||||
<div className="bg-muted border-muted mt-5 rounded-xl border p-1">
|
||||
<div className="bg-default border-muted flex rounded-[10px] border px-5 py-4">
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<h3 className="text-emphasis text-sm font-semibold leading-none">{t("manage_billing")}</h3>
|
||||
<p className="text-subtle text-sm font-medium leading-tight">
|
||||
{t("view_and_manage_billing_details")}
|
||||
</p>
|
||||
</div>
|
||||
<Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link">
|
||||
{t("billing_portal")}
|
||||
</Button>
|
||||
</CtaRow>
|
||||
</div>
|
||||
<BillingCredits />
|
||||
<div className="border-subtle mt-6 space-y-6 rounded-lg border px-6 py-8 text-sm sm:space-y-8">
|
||||
<CtaRow title={t("need_anything_else")} description={t("further_billing_help")}>
|
||||
<Button color="secondary" onClick={onContactSupportClick}>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-5">
|
||||
<p className="text-subtle text-sm font-medium leading-tight">{t("need_help")}</p>
|
||||
<Button color="secondary" size="sm" onClick={onContactSupportClick}>
|
||||
{t("contact_support")}
|
||||
</Button>
|
||||
</CtaRow>
|
||||
</div>
|
||||
</div>
|
||||
<BillingCredits />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,17 +8,21 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { MemberInvitationModalWithoutMembers } from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
||||
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants";
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { TextField, Label, InputError } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ProgressBar } from "@calcom/ui/components/progress-bar";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton";
|
||||
|
||||
@@ -29,6 +33,39 @@ type MonthOption = {
|
||||
endDate: string;
|
||||
};
|
||||
|
||||
type CreditRowProps = {
|
||||
label: string;
|
||||
value: number;
|
||||
isBold?: boolean;
|
||||
underline?: "dashed" | "solid";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CreditRow = ({ label, value, isBold = false, underline, className = "" }: CreditRowProps) => {
|
||||
const numberFormatter = new Intl.NumberFormat();
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`my-1 flex justify-between`,
|
||||
underline === "dashed"
|
||||
? "border-subtle border-b border-dashed"
|
||||
: underline === "solid"
|
||||
? "border-subtle border-b border-solid"
|
||||
: "mt-1",
|
||||
className
|
||||
)}>
|
||||
<span
|
||||
className={classNames("text-sm", isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}>
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className={classNames(`text-sm`, isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}>
|
||||
{numberFormatter.format(value)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getMonthOptions = (): MonthOption[] => {
|
||||
const options: MonthOption[] = [];
|
||||
const minDate = dayjs.utc("2025-05-01");
|
||||
@@ -60,6 +97,7 @@ export default function BillingCredits() {
|
||||
const monthOptions = useMemo(() => getMonthOptions(), []);
|
||||
const [selectedMonth, setSelectedMonth] = useState<MonthOption>(monthOptions[0]);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const {
|
||||
@@ -71,6 +109,7 @@ export default function BillingCredits() {
|
||||
|
||||
const params = useParamsWithFallback();
|
||||
const orgId = session.data?.user?.org?.id;
|
||||
const orgSlug = session.data?.user?.org?.slug;
|
||||
|
||||
const parsedTeamId = Number(params.id);
|
||||
const teamId: number | undefined = Number.isFinite(parsedTeamId)
|
||||
@@ -132,117 +171,153 @@ export default function BillingCredits() {
|
||||
buyCreditsMutation.mutate({ quantity: data.quantity, teamId });
|
||||
};
|
||||
|
||||
const teamCreditsPercentageUsed =
|
||||
creditsData.credits.totalMonthlyCredits > 0
|
||||
? (creditsData.credits.totalRemainingMonthlyCredits / creditsData.credits.totalMonthlyCredits) * 100
|
||||
: 0;
|
||||
const totalCredits = creditsData.credits.totalMonthlyCredits ?? 0;
|
||||
const totalUsed = creditsData.credits.totalCreditsUsedThisMonth ?? 0;
|
||||
|
||||
const teamCreditsPercentageUsed = totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0;
|
||||
const numberFormatter = new Intl.NumberFormat();
|
||||
|
||||
return (
|
||||
<div className="border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 pb-6 text-sm sm:space-y-8">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">{t("credits")}</h2>
|
||||
<ServerTrans
|
||||
t={t}
|
||||
i18nKey="view_and_manage_credits_description"
|
||||
components={[
|
||||
<Link
|
||||
key="Credit System"
|
||||
className="underline underline-offset-2"
|
||||
target="_blank"
|
||||
href="https://cal.com/help/billing-and-usage/messaging-credits">
|
||||
Learn more
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
<div className="-mx-6 mt-6">
|
||||
<hr className="border-subtle" />
|
||||
<>
|
||||
<div className="bg-muted border-muted mt-5 rounded-xl border p-1">
|
||||
<div className="flex flex-col gap-1 px-4 py-5">
|
||||
<h2 className="text-default text-base font-semibold leading-none">{t("credits")}</h2>
|
||||
<p className="text-subtle text-sm font-medium leading-tight">{t("view_and_manage_credits")}</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
{creditsData.credits.totalMonthlyCredits > 0 ? (
|
||||
<div className="mb-4">
|
||||
<Label>{t("monthly_credits")}</Label>
|
||||
<ProgressBar
|
||||
color="green"
|
||||
percentageValue={teamCreditsPercentageUsed}
|
||||
label={`${Math.max(0, Math.round(teamCreditsPercentageUsed))}%`}
|
||||
/>
|
||||
<div className="text-subtle">
|
||||
<div>
|
||||
{t("total_credits", {
|
||||
totalCredits: creditsData.credits.totalMonthlyCredits,
|
||||
})}
|
||||
<div className="bg-default border-muted flex w-full rounded-[10px] border px-5 py-4">
|
||||
<div className="w-full">
|
||||
{totalCredits > 0 ? (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<CreditRow
|
||||
label={t("monthly_credits")}
|
||||
value={totalCredits}
|
||||
isBold={true}
|
||||
underline="dashed"
|
||||
/>
|
||||
<CreditRow label={t("credits_used")} value={totalUsed} underline="solid" />
|
||||
<CreditRow
|
||||
label={t("total_credits_remaining")}
|
||||
value={creditsData.credits.totalRemainingMonthlyCredits}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<ProgressBar color="green" percentageValue={100 - teamCreditsPercentageUsed} />
|
||||
</div>
|
||||
{/*750 credits per tip*/}
|
||||
<div className="mt-4 flex flex-1 items-center justify-between">
|
||||
<p className="text-subtle text-sm font-medium leading-tight">
|
||||
{orgSlug ? t("credits_per_tip_org") : t("credits_per_tip_teams")}
|
||||
</p>
|
||||
<Button onClick={() => setShowMemberInvitationModal(true)} size="sm" color="secondary">
|
||||
{t("add_members_no_ellipsis")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{t("remaining_credits", {
|
||||
remainingCredits: creditsData.credits.totalRemainingMonthlyCredits,
|
||||
})}
|
||||
<div className="-mx-5 mt-5">
|
||||
<hr className="border-subtle" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{/*Auto Top-Up goes here when we have it*/}
|
||||
{/*<div className="-mx-5 mt-5">
|
||||
<hr className="border-subtle" />
|
||||
</div>*/}
|
||||
{/*Additional Credits*/}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mt-4 flex">
|
||||
<div className="-mb-1 mr-auto w-full">
|
||||
<div className="flex justify-between">
|
||||
<Label>{t("additional_credits")}</Label>
|
||||
<div className="mb-2 flex items-center gap-1">
|
||||
<p className="text-sm font-semibold leading-none">
|
||||
<span className="text-subtle font-medium">{t("current_balance")}</span>{" "}
|
||||
{numberFormatter.format(creditsData.credits.additionalCredits)}
|
||||
</p>
|
||||
<Tooltip content={t("view_additional_credits_expense_tip")}>
|
||||
<Icon name="info" className="text-emphasis h-3 w-3" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
{...register("quantity", {
|
||||
required: t("error_required_field"),
|
||||
min: { value: 50, message: t("minimum_of_credits_required") },
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
label=""
|
||||
containerClassName="w-full -mt-1"
|
||||
size="sm"
|
||||
onChange={(e) => setValue("quantity", Number(e.target.value))}
|
||||
min={50}
|
||||
addOnSuffix={<>{t("credits")}</>}
|
||||
/>
|
||||
<Button color="secondary" target="_blank" size="sm" type="submit" data-testid="buy-credits">
|
||||
{t("buy")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Label>
|
||||
{creditsData.credits.totalMonthlyCredits ? t("additional_credits") : t("available_credits")}
|
||||
</Label>
|
||||
<div className="mt-2 text-sm">{creditsData.credits.additionalCredits}</div>
|
||||
<div className="-mx-6 mb-6 mt-6">
|
||||
<hr className="border-subtle mb-3 mt-3" />
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex">
|
||||
<div className="-mb-1 mr-auto">
|
||||
<Label>{t("buy_additional_credits")}</Label>
|
||||
<div className="flex flex-col">
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
{...register("quantity", {
|
||||
required: t("error_required_field"),
|
||||
min: { value: 50, message: t("minimum_of_credits_required") },
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
label=""
|
||||
containerClassName="w-60"
|
||||
onChange={(e) => setValue("quantity", Number(e.target.value))}
|
||||
min={50}
|
||||
addOnSuffix={<>{t("credits")}</>}
|
||||
/>
|
||||
{errors.quantity && <InputError message={errors.quantity.message ?? t("invalid_input")} />}
|
||||
</div>
|
||||
</form>
|
||||
<div className="-mx-5 mt-5">
|
||||
<hr className="border-subtle" />
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<Button
|
||||
color="primary"
|
||||
target="_blank"
|
||||
EndIcon="external-link"
|
||||
type="submit"
|
||||
data-testid="buy-credits">
|
||||
{t("buy_credits")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="-mx-6 mb-6 mt-6">
|
||||
<hr className="border-subtle mb-3 mt-3" />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="mr-auto">
|
||||
<Label className="mb-4">{t("download_expense_log")}</Label>
|
||||
<div className="mt-2 flex flex-col">
|
||||
<Select
|
||||
options={monthOptions}
|
||||
value={selectedMonth}
|
||||
onChange={(option) => option && setSelectedMonth(option)}
|
||||
/>
|
||||
{/*Download Expense Log*/}
|
||||
<div className="mt-4 flex">
|
||||
<div className="mr-auto w-full">
|
||||
<Label className="mb-4">{t("download_expense_log")}</Label>
|
||||
<div className="mr-2 mt-1">
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
innerClassNames={{
|
||||
control: "font-medium text-emphasis",
|
||||
}}
|
||||
options={monthOptions}
|
||||
value={selectedMonth}
|
||||
onChange={(option) => option && setSelectedMonth(option)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<Button onClick={handleDownload} loading={isDownloading} color="secondary" size="sm">
|
||||
{t("download")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<Button onClick={handleDownload} loading={isDownloading} StartIcon="file-down">
|
||||
{t("download")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/*Credit Worth Section*/}
|
||||
<div className="text-subtle px-5 py-4 text-sm font-medium leading-tight">
|
||||
<ServerTrans
|
||||
t={t}
|
||||
i18nKey="credit_worth_description"
|
||||
components={[
|
||||
<Link
|
||||
key="Credit System"
|
||||
className="underline underline-offset-2"
|
||||
target="_blank"
|
||||
href="https://cal.com/help/billing-and-usage/messaging-credits">
|
||||
Learn more
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{teamId && (
|
||||
<MemberInvitationModalWithoutMembers
|
||||
teamId={teamId}
|
||||
showMemberInvitationModal={showMemberInvitationModal}
|
||||
hideInvitationModal={() => setShowMemberInvitationModal(false)}
|
||||
onSettingsOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,77 +3,76 @@ import { SkeletonText, SkeletonContainer, SkeletonButton } from "@calcom/ui/comp
|
||||
export function BillingCreditsSkeleton() {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 text-sm sm:space-y-8">
|
||||
{/* Title and Description */}
|
||||
<div>
|
||||
<div>
|
||||
<SkeletonText className="mb-2 h-4 w-20" /> {/* Credits title */}
|
||||
</div>
|
||||
<div>
|
||||
<SkeletonText className="h-4 w-72" /> {/* Description */}
|
||||
</div>
|
||||
|
||||
<div className="-mx-6 mt-6">
|
||||
<hr className="border-subtle" />
|
||||
</div>
|
||||
{/* Monthly credits */}
|
||||
<div className="mt-[20px]">
|
||||
<SkeletonText className="mb-4 h-4 w-32" /> {/* Monthly credits label */}
|
||||
<div className="h-2 w-full rounded-md bg-gray-200" /> {/* Progress bar */}
|
||||
<div className="text-subtle mt-2">
|
||||
<div>
|
||||
<SkeletonText className="h-4 w-40" /> {/* Total credits */}
|
||||
<div className="bg-muted border-muted mt-5 rounded-xl p-1">
|
||||
<div className="flex flex-col gap-1 px-4 py-5">
|
||||
<SkeletonText className="h-4 w-20" /> {/* Credits title */}
|
||||
<SkeletonText className="h-4 w-48" /> {/* View and manage credits description */}
|
||||
</div>
|
||||
<div className="bg-default border-muted flex w-full rounded-[10px] px-5 py-4">
|
||||
<div className="w-full">
|
||||
{/* Credits section */}
|
||||
<div className="mb-4">
|
||||
{/* Monthly credits row */}
|
||||
<div className="mt-1 flex justify-between">
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-4 w-20" />
|
||||
</div>
|
||||
<div>
|
||||
<SkeletonText className="h-4 w-40" /> {/* Remaining credits */}
|
||||
{/* Additional credits row */}
|
||||
<div className="mt-1 flex justify-between">
|
||||
<SkeletonText className="h-4 w-36" />
|
||||
<SkeletonText className="h-4 w-20" />
|
||||
</div>
|
||||
{/* Remaining row */}
|
||||
<div className="mt-1 flex justify-between">
|
||||
<SkeletonText className="h-4 w-24" />
|
||||
<SkeletonText className="h-4 w-20" />
|
||||
</div>
|
||||
{/* Progress bar */}
|
||||
<div className="mt-4">
|
||||
<div className="h-2 w-full rounded-md bg-gray-200" />
|
||||
</div>
|
||||
{/* Credits per tip */}
|
||||
<div className="mt-4 flex flex-1 justify-between">
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonButton className="h-8 w-24 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-5 mt-5">
|
||||
<hr className="border-subtle" />
|
||||
</div>
|
||||
{/* Additional Credits form */}
|
||||
<div className="mt-4 flex">
|
||||
<div className="-mb-1 mr-auto w-full">
|
||||
<SkeletonText className="mb-2 h-4 w-32" /> {/* Additional credits label */}
|
||||
<div className="flex w-full">
|
||||
<SkeletonText className="h-9 w-full rounded-md" /> {/* Input field */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 mt-auto">
|
||||
<SkeletonButton className="h-9 w-12 rounded-md" /> {/* Buy button */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-5 mt-5">
|
||||
<hr className="border-subtle" />
|
||||
</div>
|
||||
{/* Download Expense Log */}
|
||||
<div className="mt-4 flex">
|
||||
<div className="mr-auto w-full">
|
||||
<SkeletonText className="mb-4 h-4 w-40" /> {/* Download expense log label */}
|
||||
<div className="mr-2 mt-1">
|
||||
<SkeletonText className="h-9 w-full rounded-md" /> {/* Select dropdown */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<SkeletonButton className="h-9 w-20 rounded-md" /> {/* Download button */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Additional credits */}
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
<SkeletonText className="mb-2 h-4 w-40" /> {/* Additional credits label */}
|
||||
</div>
|
||||
<div>
|
||||
<SkeletonText className="h-4 w-24" /> {/* Additional credits value */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-6 mb-6 mt-4">
|
||||
<hr className="border-subtle mb-3 mt-3" />
|
||||
</div>
|
||||
{/* Buy credits form */}
|
||||
<div className="flex">
|
||||
<div className="mr-auto">
|
||||
<div>
|
||||
<SkeletonText className="mb-2 h-4 w-[268px]" /> {/* Buy credits label */}
|
||||
</div>
|
||||
<div>
|
||||
<SkeletonText className="mt-2 h-8 w-[240px] rounded-md" /> {/* Input field */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto ">
|
||||
<SkeletonButton className="h-9 w-[120px] rounded-md" /> {/* Buy button */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-6 mb-6 mt-6">
|
||||
<hr className="border-subtle mb-3 mt-3" />
|
||||
</div>
|
||||
{/* Download expense log */}
|
||||
<div className="flex">
|
||||
<div className="mr-auto mt-1">
|
||||
<div>
|
||||
<SkeletonText className="mb-2 h-4 w-[156px]" /> {/* Label */}
|
||||
</div>
|
||||
<div>
|
||||
<SkeletonText className="mt-2 h-8 w-[156px] rounded-md" /> {/* Select */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto ">
|
||||
<SkeletonButton className="h-9 w-[109px] rounded-md" /> {/* Download button */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Credit Worth Section */}
|
||||
<div className="text-subtle px-5 py-4">
|
||||
<SkeletonText className="h-4 w-full" />
|
||||
<SkeletonText className="mt-1 h-4 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.7.0",
|
||||
"version": "5.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
"analyze:server": "BUNDLE_ANALYZE=server next build",
|
||||
"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": "turbo run copy-app-store-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",
|
||||
@@ -16,8 +16,9 @@
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||
"sentry:release": "NODE_OPTIONS='--max-old-space-size=6144' node scripts/create-sentry-release.js",
|
||||
"copy-static": "node scripts/copy-app-store-static.js",
|
||||
"build": "yarn copy-static && next build && yarn sentry:release",
|
||||
"copy-static": "turbo run copy-app-store-static",
|
||||
"copy-app-store-static": "node scripts/copy-app-store-static.js",
|
||||
"build": "turbo run copy-app-store-static && next build && yarn sentry:release",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Admin Users Management", () => {
|
||||
test("should add a new user successfully", async ({ page, users }) => {
|
||||
const adminUser = await users.create({
|
||||
role: "ADMIN",
|
||||
});
|
||||
|
||||
await adminUser.apiLogin();
|
||||
|
||||
await page.goto("/settings/admin/users/add");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
await page.fill('input[name="name"]', "Test User");
|
||||
await page.fill('input[name="username"]', "testuser");
|
||||
await page.fill('input[name="email"]', "testuser@example.com");
|
||||
await page.fill('input[name="bio"]', "Test user bio");
|
||||
|
||||
await page.locator('[data-testid="timezone-select"] div').first().click();
|
||||
|
||||
const responsePromise = page.waitForResponse((response) =>
|
||||
response.url().includes("/api/trpc/users/add")
|
||||
);
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
await page.waitForURL("/settings/admin/users");
|
||||
});
|
||||
|
||||
test("should edit an existing user successfully", async ({ page, users }) => {
|
||||
const adminUser = await users.create({
|
||||
role: "ADMIN",
|
||||
});
|
||||
|
||||
const userToEdit = await users.create({
|
||||
name: "Edit User",
|
||||
username: "edituser",
|
||||
});
|
||||
|
||||
await adminUser.apiLogin();
|
||||
|
||||
await page.goto(`/settings/admin/users/${userToEdit.id}/edit`);
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
await expect(page.locator('input[name="name"]')).toHaveValue("Edit User");
|
||||
|
||||
await page.fill('input[name="name"]', "Updated User");
|
||||
|
||||
const responsePromise = page.waitForResponse((response) =>
|
||||
response.url().includes("/api/trpc/users/update")
|
||||
);
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
await page.waitForURL("/settings/admin/users");
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,8 @@ test.describe("Duplicate API Calls Prevention", () => {
|
||||
const trpcCalls: string[] = [];
|
||||
const apiV2Calls: string[] = [];
|
||||
|
||||
// Intercept tRPC getSchedule calls - pattern matches trpc.viewer.slots.getSchedule.useQuery()
|
||||
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
|
||||
// Intercept tRPC getSchedule calls - pattern matches /api/trpc/slots/getSchedule
|
||||
await page.route("**/api/trpc/slots/getSchedule**", async (route) => {
|
||||
trpcCalls.push(route.request().url());
|
||||
await route.continue();
|
||||
});
|
||||
@@ -33,14 +33,10 @@ test.describe("Duplicate API Calls Prevention", () => {
|
||||
|
||||
const totalCalls = trpcCalls.length + apiV2Calls.length;
|
||||
|
||||
if (totalCalls === 0) {
|
||||
console.log("No API calls detected - environment may have issues preventing page load");
|
||||
expect(totalCalls).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||
}
|
||||
expect(totalCalls).toBeGreaterThan(0);
|
||||
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("should detect when schedule endpoints are called multiple times for team events", async ({
|
||||
@@ -65,7 +61,7 @@ test.describe("Duplicate API Calls Prevention", () => {
|
||||
const trpcCalls: string[] = [];
|
||||
const apiV2Calls: string[] = [];
|
||||
|
||||
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
|
||||
await page.route("**/api/trpc/slots/getSchedule**", async (route) => {
|
||||
trpcCalls.push(route.request().url());
|
||||
await route.continue();
|
||||
});
|
||||
@@ -81,14 +77,10 @@ test.describe("Duplicate API Calls Prevention", () => {
|
||||
|
||||
const totalCalls = trpcCalls.length + apiV2Calls.length;
|
||||
|
||||
if (totalCalls === 0) {
|
||||
console.log("No API calls detected - environment may have issues preventing page load");
|
||||
expect(totalCalls).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||
}
|
||||
expect(totalCalls).toBeGreaterThan(0);
|
||||
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("should detect when schedule endpoints are called multiple times for organization team events", async ({
|
||||
@@ -117,7 +109,7 @@ test.describe("Duplicate API Calls Prevention", () => {
|
||||
const trpcCalls: string[] = [];
|
||||
const apiV2Calls: string[] = [];
|
||||
|
||||
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
|
||||
await page.route("**/api/trpc/slots/getSchedule**", async (route) => {
|
||||
trpcCalls.push(route.request().url());
|
||||
await route.continue();
|
||||
});
|
||||
@@ -133,13 +125,9 @@ test.describe("Duplicate API Calls Prevention", () => {
|
||||
|
||||
const totalCalls = trpcCalls.length + apiV2Calls.length;
|
||||
|
||||
if (totalCalls === 0) {
|
||||
console.log("No API calls detected - environment may have issues preventing page load");
|
||||
expect(totalCalls).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||
}
|
||||
expect(totalCalls).toBeGreaterThan(0);
|
||||
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,6 +111,9 @@ export function createAppsFixture(page: Page) {
|
||||
},
|
||||
goToEventType: async (eventType: string) => {
|
||||
await page.getByRole("link", { name: eventType }).click();
|
||||
// fix the race condition
|
||||
await page.waitForSelector('[data-testid="event-title"]');
|
||||
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||
},
|
||||
goToAppsTab: async () => {
|
||||
await page.getByTestId("vertical-tab-apps").click();
|
||||
|
||||
@@ -584,6 +584,10 @@ export const createUsersFixture = (
|
||||
get: () => store.users,
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
const logoutBtn = page.getByTestId("logout-btn");
|
||||
await expect(logoutBtn).toHaveText("Go back to the login page");
|
||||
await page.reload();
|
||||
await expect(logoutBtn).toHaveText("Go back to the login page");
|
||||
},
|
||||
deleteAll: async () => {
|
||||
const ids = store.users.map((u) => u.id);
|
||||
@@ -680,6 +684,10 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
},
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
const logoutBtn = page.getByTestId("logout-btn");
|
||||
await expect(logoutBtn).toHaveText("Go back to the login page");
|
||||
await page.reload();
|
||||
await expect(logoutBtn).toHaveText("Go back to the login page");
|
||||
},
|
||||
getFirstTeamMembership: async () => {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { clearFilters, applySelectFilter } from "./filter-helpers";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { createAllPermissionsArray, enablePBACForTeam } from "./lib/test-helpers/pbac";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
@@ -268,8 +271,82 @@ test.describe("Insights", async () => {
|
||||
];
|
||||
|
||||
for (const title of expectedChartTitles) {
|
||||
const chartCard = page.locator("[data-testid='panel-card'] h2").filter({ hasText: title });
|
||||
const chartCard = page
|
||||
.locator("[data-testid='panel-card'] h2")
|
||||
.filter({ hasText: new RegExp(`^${title}$`) });
|
||||
await expect(chartCard).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("should be able to access insights page with custom role lacking insights.read permission", async ({
|
||||
page,
|
||||
users,
|
||||
}) => {
|
||||
const owner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isUnpublished: true,
|
||||
isOrg: true,
|
||||
});
|
||||
|
||||
const userOne = await users.create();
|
||||
const userTwo = await users.create();
|
||||
|
||||
const { teamOne } = await createTeamsAndMembership(userOne.id, userTwo.id);
|
||||
|
||||
const orgMembership = await owner.getOrgMembership();
|
||||
const orgId = orgMembership.team.id;
|
||||
|
||||
await enablePBACForTeam(orgId);
|
||||
await enablePBACForTeam(teamOne.id);
|
||||
|
||||
const permissions = createAllPermissionsArray().filter(
|
||||
({ resource, action }) => !(resource === "insights" && action === "read")
|
||||
);
|
||||
|
||||
const customRole = await prisma.role.create({
|
||||
data: {
|
||||
id: `e2e_no_insights_${orgId}_${Date.now()}`,
|
||||
name: "E2E Role Without Insights",
|
||||
description: "E2E role for testing - has all permissions except insights.read",
|
||||
color: "#dc2626",
|
||||
teamId: orgId,
|
||||
type: "CUSTOM",
|
||||
permissions: {
|
||||
create: permissions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.membership.update({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId: userOne.id,
|
||||
teamId: teamOne.id,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
customRoleId: customRole.id,
|
||||
},
|
||||
});
|
||||
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const isPBACEnabled = await featuresRepository.checkIfTeamHasFeature(orgId, "pbac");
|
||||
expect(isPBACEnabled).toBe(true);
|
||||
|
||||
const permissionService = new PermissionCheckService();
|
||||
const hasPermission = await permissionService.checkPermission({
|
||||
userId: userOne.id,
|
||||
teamId: teamOne.id,
|
||||
permission: "insights.read",
|
||||
fallbackRoles: [],
|
||||
});
|
||||
expect(hasPermission).toBe(false);
|
||||
|
||||
await userOne.apiLogin();
|
||||
await page.goto("/insights");
|
||||
|
||||
// Verify the user can access the insights page
|
||||
await page.locator('[data-testid^="insights-filters-"]').waitFor();
|
||||
expect(page.url()).toContain("/insights");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
// Create array of all permissions from PERMISSION_REGISTRY
|
||||
export const createAllPermissionsArray = () => {
|
||||
const allPermissions: { resource: string; action: string }[] = [];
|
||||
|
||||
Object.entries(PERMISSION_REGISTRY).forEach(([resource, resourceConfig]) => {
|
||||
if (resource === "*") {
|
||||
return;
|
||||
}
|
||||
Object.entries(resourceConfig).forEach(([action, _details]) => {
|
||||
allPermissions.push({ resource, action });
|
||||
});
|
||||
});
|
||||
|
||||
return allPermissions;
|
||||
};
|
||||
|
||||
export const enablePBACForTeam = async (teamId: number) => {
|
||||
await prisma.teamFeatures.create({
|
||||
data: {
|
||||
featureId: "pbac",
|
||||
teamId: teamId,
|
||||
assignedBy: "e2e",
|
||||
assignedAt: new Date(),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -775,10 +775,10 @@ test.describe("Out of office", () => {
|
||||
|
||||
//Default filter 'Last 7 Days' when DateRange Filter is selected
|
||||
await test.step("Default filter - 'Last 7 Days'", async () => {
|
||||
await addFilter(page, "dateRange");
|
||||
const entriesListRespPromise = page.waitForResponse(
|
||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||
);
|
||||
await addFilter(page, "dateRange");
|
||||
await entriesListRespPromise;
|
||||
|
||||
//1 OOO record should be visible for member3, end=currentDate-4days
|
||||
@@ -836,10 +836,10 @@ test.describe("Out of office", () => {
|
||||
|
||||
//Select 'Last 30 Days'
|
||||
await test.step("select 'Last 30 Days'", async () => {
|
||||
await addFilter(page, "dateRange");
|
||||
const entriesListRespPromise1 = page.waitForResponse(
|
||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||
);
|
||||
await addFilter(page, "dateRange");
|
||||
await entriesListRespPromise1;
|
||||
|
||||
const entriesListRespPromise2 = page.waitForResponse(
|
||||
|
||||
@@ -15,7 +15,7 @@ test.describe("Workflow Tab - Event Type", () => {
|
||||
test("Creating a new workflow", async ({ workflowPage }) => {
|
||||
const { createWorkflow, assertListCount } = workflowPage;
|
||||
|
||||
await createWorkflow({ name: "" });
|
||||
await createWorkflow({ name: "test workflow" });
|
||||
await assertListCount(3);
|
||||
});
|
||||
|
||||
|
||||
@@ -512,6 +512,10 @@
|
||||
<symbol class="lucide lucide-paperclip" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="paperclip">
|
||||
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" ></path>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-pause" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="pause">
|
||||
<rect x="14" y="4" width="4" height="16" rx="1" ></rect>
|
||||
<rect x="6" y="4" width="4" height="16" rx="1" ></rect>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-pencil" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="pencil">
|
||||
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" ></path>
|
||||
<path d="m15 5 4 4" ></path>
|
||||
@@ -533,6 +537,9 @@
|
||||
<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>
|
||||
<symbol class="lucide lucide-play" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="play">
|
||||
<polygon points="6 3 20 12 6 21 6 3" ></polygon>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-plus" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="plus">
|
||||
<path d="M5 12h14" ></path>
|
||||
<path d="M12 5v14" ></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@@ -16,7 +16,8 @@
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display_override": ["minimal-ui"],
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"start_url": "/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم",
|
||||
"verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني",
|
||||
"verify_email_button": "تأكيد البريد الإلكتروني",
|
||||
"cal_ai_assistant": "مساعد Cal AI",
|
||||
"cal_ai_assistant": "المساعد",
|
||||
"send_cal_video_transcription_emails": "إرسال رسائل النسخ النصي لفيديو Cal",
|
||||
"description_send_cal_video_transcription_emails": "إرسال رسائل بريد إلكتروني تحتوي على النسخ النصي لفيديو Cal بعد انتهاء الاجتماع. (يتطلب خطة مدفوعة)",
|
||||
"verify_email_change_description": "لقد طلبت تغيير البريد الإلكتروني المستخدم لحسابك في {{appName}}. الرجاء النقر على الزر أدناه لتأكيد بريدك الإلكتروني الجديد.",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "فشل التحقق من صحة سير العمل",
|
||||
"workflow_validation_empty_fields": "خطوة واحدة أو أكثر من خطوات سير العمل تحتوي على محتوى رسالة فارغ",
|
||||
"workflow_validation_unverified_contacts": "لم يتم التحقق من رقم هاتف واحد أو أكثر أو عناوين البريد الإلكتروني",
|
||||
"supercharge_your_workflows_with_cal_ai": "عزز سير عملك مع Cal.ai",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "وكلاء ذكاء اصطناعي واقعيون يقومون بحجز الاجتماعات وإرسال التذكيرات ومتابعة عملائك.",
|
||||
"phone_number_imported_successfully": "تم استيراد رقم الهاتف وربطه بالوكيل بنجاح",
|
||||
"phone_number_deleted_successfully": "تم حذف رقم الهاتف بنجاح",
|
||||
"delete_phone_number_confirmation": "هل أنت متأكد أنك تريد حذف رقم الهاتف هذا؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "تم إلغاء اشتراك رقم الهاتف بنجاح",
|
||||
"updating": "جاري التحديث",
|
||||
"round_robin": "الترتيب الدوري",
|
||||
"hi_how_are_you_doing": "مرحبًا، كيف حالك؟",
|
||||
"round_robin_description": "نقل الاجتماعات بشكل دوري بين أعضاء الفريق المتعددين.",
|
||||
"managed_event": "حدث تم إدارته",
|
||||
"username_placeholder": "اسم المستخدم",
|
||||
@@ -876,9 +879,9 @@
|
||||
"are_you_sure_you_want_to_delete_workflow_step": "هل أنت متأكد من أنك تريد حذف خطوة مسار العمل هذه؟",
|
||||
"do_you_still_want_to_unsubscribe": "هل ما زلت ترغب في إلغاء اشتراك رقم الهاتف من هذا الوكيل؟",
|
||||
"the_action_will_disconnect_phone_number": "سيؤدي هذا الإجراء إلى فصل رقم الهاتف عن الوكيل. لن يتمكن الوكيل من إجراء مكالمات حتى يتم توصيل رقم هاتف جديد.",
|
||||
"cal_ai_phone_numbers": "أرقام هواتف Cal AI",
|
||||
"cal_ai_phone_numbers": "أرقام الهواتف",
|
||||
"connect_phone_number": "توصيل رقم الهاتف",
|
||||
"cal_ai_phone_numbers_description": "إدارة أرقام هواتف الخاصة بك",
|
||||
"cal_ai_phone_numbers_description": "إدارة أرقام هواتفك",
|
||||
"import_number": "استيراد رقم",
|
||||
"this_action_will_also": "سيقوم هذا الإجراء أيضًا بـ:",
|
||||
"import_phone_number": "استيراد رقم الهاتف",
|
||||
@@ -886,7 +889,7 @@
|
||||
"cancel_your_phone_number_subscription": "إلغاء اشتراك رقم هاتفك",
|
||||
"delete_associated_phone_number": "حذف رقم الهاتف المرتبط",
|
||||
"unauthorized_create_workflow": "أنت غير مصرح لك بإنشاء مسار العمل هذا",
|
||||
"import_phone_number_description": "استيراد رقم هاتف Twilio الخاص بك لاستخدامه مع هاتف Cal AI",
|
||||
"import_phone_number_description": "استيراد رقم هاتف Twilio الخاص بك لاستخدامه مع الهاتف",
|
||||
"phone_number_cost": "${{price}}/شهريًا",
|
||||
"buy_new_number": "شراء رقم جديد",
|
||||
"buy_number_cost_x_per_month": "تكلفة شراء رقم هاتف هي ${{priceInDollars}} شهريًا. سيتم محاسبتك شهريًا على كل رقم هاتف نشط.",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "نعم، إلغاء الاشتراك",
|
||||
"cancel_phone_number_subscription_confirmation": "هل أنت متأكد من رغبتك في إلغاء اشتراك رقم الهاتف هذا؟ لا يمكن التراجع عن هذا الإجراء وستفقد إمكانية الوصول إلى رقم الهاتف هذا.",
|
||||
"add_members": "إضافة أعضاء...",
|
||||
"add_members_no_ellipsis": "إضافة أعضاء",
|
||||
"no_assigned_members": "لا يوجد أعضاء معينين",
|
||||
"assigned_to": "تم التعيين إلى",
|
||||
"you_must_be_logged_in_to": "يجب تسجيل الدخول إلى {{url}}",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "الفئات",
|
||||
"pricing": "التسعير",
|
||||
"learn_more": "معرفة المزيد",
|
||||
"try_now": "جرب الآن",
|
||||
"privacy_policy": "سياسة الخصوصية",
|
||||
"terms_of_service": "شروط الخدمة",
|
||||
"remove": "إزالة",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "إرسال رسالة عبر WhatsApp إلى أحد الحاضرين",
|
||||
"workflows": "سير العمل",
|
||||
"new_workflow_btn": "سير عمل جديد",
|
||||
"how_would_you_like_to_start": "كيف ترغب في البدء؟",
|
||||
"add_new_workflow": "إضافة سير عمل جديد",
|
||||
"reschedule_event_trigger": "متى يتم إعادة جدولة الحدث",
|
||||
"trigger": "مشغل",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "مدة الحدث",
|
||||
"event_time_info": "وقت بدء الحدث",
|
||||
"event_type_not_found": "لم يتم العثور على نوع الحدث",
|
||||
"number_to_call_variable": "رقم للاتصال",
|
||||
"number_to_call_info": "رقم هاتف المستخدم الذي تتصل به",
|
||||
"location_variable": "الموقع",
|
||||
"location_info": "موقع الحدث",
|
||||
"additional_notes_variable": "ملاحظات إضافية",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "رابط الفريق",
|
||||
"team_members": "أعضاء الفريق",
|
||||
"more": "المزيد",
|
||||
"cal_ai_workflows": "سير عمل Cal.ai",
|
||||
"and_count_more": "و {{count}} أكثر",
|
||||
"more_page_footer": "نعتبر تطبيق الهاتف المحمول امتداداً لتطبيق الويب، لكن يرجى العودة لتطبيق الويب إذا كنت تقوم بأي إجراءات معقدة.",
|
||||
"workflow_example_1": "إرسال رسائل قصيرة للحضور للتذكير قبل 24 ساعة من بدء الحدث",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "إرسال رسائل قصيرة للحضور للتذكير قبل ساعة من بدء الحدث",
|
||||
"workflow_example_5": "إرسال رسالة قصيرة مخصصة للحضور عند إعادة جدولة الحدث",
|
||||
"workflow_example_6": "إرسال بريد إلكتروني مخصص للمضيف عندما يتم حجز حدث جديد",
|
||||
"send_sms_reminder": "إرسال تذكير عبر الرسائل القصيرة",
|
||||
"send_sms_reminder_description": "قبل 24 ساعة من بدء الحدث",
|
||||
"follow_up_with_no_shows": "متابعة المتغيبين",
|
||||
"follow_up_with_no_shows_description": "بعد 30 دقيقة من انتهاء الحدث",
|
||||
"remind_attendees_to_bring_id": "تذكير الحاضرين بإحضار الهوية",
|
||||
"remind_attendees_to_bring_id_description": "قبل يوم واحد من بدء الحدث",
|
||||
"email_to_remind_booking": "تذكير بالبريد الإلكتروني",
|
||||
"email_to_remind_booking_description": "قبل ساعة واحدة من بدء الحدث",
|
||||
"custom_sms_reminder": "تذكير مخصص عبر الرسائل القصيرة",
|
||||
"custom_sms_reminder_description": "عند جدولة الحدث",
|
||||
"custom_email_reminder": "تذكير مخصص بالبريد الإلكتروني",
|
||||
"custom_email_reminder_description": "إعادة جدولة الحدث للمضيف",
|
||||
"count_managed_to_limit": "تضمين عدد الحجوزات من أنواع الفعاليات المُدارة",
|
||||
"welcome_to_cal_header": "مرحبًا بك في {{appName}}!",
|
||||
"edit_form_later_subtitle": "ستتمكن من تعديل هذا لاحقًا.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "إظهار في صفحة الحجز",
|
||||
"visit_cancelled_booking": "يمكنك زيارة صفحة الحجز الملغى",
|
||||
"get_started_zapier_templates": "البدء في استخدام قوالب Zapier",
|
||||
"standard_templates": "القوالب القياسية",
|
||||
"cal_ai_templates": "قوالب Cal.ai",
|
||||
"team_is_unpublished": "لم يُنشر {{team}}",
|
||||
"org_is_unpublished_description": "رابط هذه المنظمة غير متاح حاليًا. يرجى الاتصال بمالك المنظمة أو طلب النشر منه.",
|
||||
"team_is_unpublished_description": "رابط هذا {{entity}} غير متاح حاليًا. يرجى الاتصال بمالك {{entity}} أو طلب نشره منه.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "تعذر تنفيذ عملية الدفع من البطاقة.",
|
||||
"insights": "رؤى",
|
||||
"routing_forms": "نماذج التوجيه",
|
||||
"testing_workflow_info_message": "عند اختبار سير العمل هذا، يجب أن تدرك أنه لا يمكن جدولة رسائل البريد الإلكتروني والرسائل القصيرة إلا قبل ساعة واحدة على الأقل",
|
||||
"insights_no_data_found_for_filter": "لم يتم العثور على بيانات لعامل التصفية المحدد أو التواريخ المحددة.",
|
||||
"acknowledge_booking_no_show_fee": "أقر بأنه إذا لم أحضر هذا الحدث فسيتم سحب رسوم عدم حضور من بطاقتي تبلغ {{amount, currency}}.",
|
||||
"days": "أيام",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "الفريق: {{teamName}}",
|
||||
"insights_user_filter": "المستخدم: {{userName}}",
|
||||
"insights_subtitle": "عرض Insights الحجز عبر أحداثك",
|
||||
"call_history": "سجل المكالمات",
|
||||
"call_history_subtitle": "عرض سجل المكالمات عبر مكالمات Cal.ai الخاصة بك",
|
||||
"location_options": "خيارات الموقع {{locationCount}}",
|
||||
"channel_type": "نوع القناة",
|
||||
"end_reason": "سبب الإنهاء",
|
||||
"session_status": "حالة الجلسة",
|
||||
"user_sentiment": "شعور المستخدم",
|
||||
"time_header": "الوقت",
|
||||
"from_header": "من",
|
||||
"custom_plan": "الخطة المخصصة",
|
||||
"email_embed": "البريد الإلكتروني المضمن",
|
||||
"add_times_to_your_email": "حدّد بعض الفترات المتاحة وأدرجها في رسالتك الإلكترونية",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "الحساب مرتبط بالفعل",
|
||||
"send_email": "إرسال بريد إلكتروني",
|
||||
"cal_ai_phone_call_action": "الاتصال بالحاضر باستخدام وكيل Cal.ai الصوتي",
|
||||
"call_to_confirm_booking": "اتصال لتأكيد الحجز",
|
||||
"cal_ai_phone_call_action_description": "قبل ساعتين من بدء الحدث",
|
||||
"cal_ai_agent_configuration": "إعدادات وكيل Cal.ai",
|
||||
"choose_at_least_one_event_type_test_call": "يرجى اختيار نوع حدث واحد على الأقل لإجراء مكالمة اختبارية.",
|
||||
"mark_as_no_show": "وضع علامة عدم الحضور",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "تأكيد تغيير البريد الإلكتروني",
|
||||
"buy_credits": "شراء رصيد",
|
||||
"credits": "الرصيد",
|
||||
"view_and_manage_credits": "عرض وإدارة الرصيد",
|
||||
"credits_used": "الرصيد المستخدم",
|
||||
"total_credits_remaining": "الرصيد المتبقي الإجمالي",
|
||||
"credits_per_tip_org": "تحصل على 1000 رصيد شهريًا لكل عضو في الفريق",
|
||||
"credits_per_tip_teams": "تحصل على 750 رصيد شهريًا لكل عضو في الفريق",
|
||||
"view_and_manage_credits": "عرض وإدارة الرصيد لإرسال رسائل SMS",
|
||||
"view_and_manage_credits_description": "عرض وإدارة الرصيد لإرسال رسائل SMS. قيمة الرصيد الواحد هي 1¢ (دولار أمريكي). <0>معرفة المزيد</0>",
|
||||
"credit_worth_description": "الرصيد الواحد يساوي 1 سنت (دولار أمريكي). <0>تعرف على المزيد</0>",
|
||||
"buy_additional_credits": "شراء رصيد إضافي (0.01 دولار لكل رصيد)",
|
||||
"view_additional_credits_expense_tip": "يمكنك عرض إنفاق الرصيد الإضافي في سجل النفقات الخاص بك",
|
||||
"overview": "نظرة عامة",
|
||||
"organization_slug_taken": "الاسم المختصر للمؤسسة مستخدم بالفعل",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "لا يمكنك إنشاء مؤسسة لأنك بالفعل جزء من مؤسسة",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "لقد نفد رصيد فريق Cal.com الخاص بك {{teamName}}. نتيجة لذلك، يتم الآن إرسال رسائل SMS عبر البريد الإلكتروني بدلاً من ذلك. لاستئناف إرسال SMS، يرجى شراء رصيد إضافي.",
|
||||
"credit_limit_reached_message_user": "لقد نفدت رصيد حسابك على Cal.com. نتيجة لذلك، يتم الآن إرسال رسائل SMS عبر البريد الإلكتروني بدلاً من ذلك. لاستئناف إرسال رسائل SMS، يرجى شراء رصيد إضافي.",
|
||||
"current_credit_balance": "الرصيد الحالي: {{balance}} رصيد",
|
||||
"current_balance": "الرصيد الحالي:",
|
||||
"notification_about_your_booking": "إشعار بخصوص حجزك",
|
||||
"monthly_credits": "الرصيد الشهري",
|
||||
"total_credits": "إجمالي الرصيد: {{totalCredits}}",
|
||||
"remaining_credits": "الرصيد المتبقي: {{remainingCredits}}",
|
||||
"remaining": "المتبقي",
|
||||
"total": "الإجمالي",
|
||||
"additional_credits": "رصيد إضافي",
|
||||
"routing_form_next_in_queue": "{{count}} التالي في قائمة الانتظار",
|
||||
"routing_form_select_members_to_email": "إرسال ردود البريد الإلكتروني إلى",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "عرض سير العمل الحالي وإعداداته",
|
||||
"pbac_desc_update_workflows": "تعديل وتغيير إعدادات سير العمل",
|
||||
"pbac_desc_delete_workflows": "إزالة سير العمل من النظام",
|
||||
"pbac_resource_webhook": "ويب هوك",
|
||||
"pbac_desc_create_webhooks": "إنشاء ويب هوكس",
|
||||
"pbac_desc_view_webhooks": "عرض ويب هوكس",
|
||||
"pbac_desc_update_webhooks": "تحديث الويب هوك",
|
||||
"pbac_desc_delete_webhooks": "حذف الويب هوك",
|
||||
"pbac_desc_manage_workflows": "وصول كامل لإدارة جميع سير العمل",
|
||||
"pbac_desc_create_event_types": "إنشاء أنواع الأحداث",
|
||||
"pbac_desc_view_event_types": "عرض أنواع الأحداث",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "اسم حدث المشغل (مثل BOOKING_CREATED، BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "وقت إنشاء الويب هوك",
|
||||
"webhook_type": "رابط نوع الحدث",
|
||||
"set_up_agent": "إعداد الوكيل",
|
||||
"webhook_title": "اسم نوع الحدث",
|
||||
"webhook_start_time": "وقت بدء الحدث",
|
||||
"webhook_end_time": "وقت انتهاء الحدث",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "زيارة",
|
||||
"location_custom_label_input_label": "تسمية مخصصة على صفحة الحجز",
|
||||
"meeting_link": "رابط الاجتماع",
|
||||
"session_outcome": "نتيجة الجلسة",
|
||||
"call_created": "تم إنشاء المكالمة",
|
||||
"voicemail": "البريد الصوتي",
|
||||
"my_bookings": "حجوزاتي",
|
||||
"phone": "الهاتف",
|
||||
"free": "مجاني",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "اسم المستخدم",
|
||||
"expand_panel": "توسيع اللوحة",
|
||||
"collapse_panel": "طي اللوحة",
|
||||
"email_verification_required": "التحقق من البريد الإلكتروني مطلوب لهذا النوع من الأحداث",
|
||||
"invalid_verification_code": "تم تقديم رمز تحقق غير صالح",
|
||||
"you_have_one_team": "لديك فريق واحد",
|
||||
"consider_consolidating_one_team_org": "فكر في إنشاء مؤسسة لتوحيد الفواتير وأدوات الإدارة والتحليلات عبر فريقك.",
|
||||
"consider_consolidating_multi_team_org": "فكر في إنشاء مؤسسة لتوحيد الفواتير وأدوات الإدارة والتحليلات عبر فرقك.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "قبل وقت البدء المحدد",
|
||||
"cancel_booking_acknowledge_no_show_fee": "أقر بأنه عند إلغاء الحجز قبل {{timeValue}} {{timeUnit}} من وقت البدء سيتم فرض رسوم التخلف عن الحضور بقيمة {{amount, currency}}",
|
||||
"contact_organizer": "إذا كانت لديك أي أسئلة، يرجى التواصل مع المنظم.",
|
||||
"booking_time_option": "وقت الحجز",
|
||||
"booking_time_option_description": "عندما يتم جدولة الحجز (من البداية إلى النهاية)",
|
||||
"created_at_option": "تم الإنشاء في",
|
||||
"created_at_option_description": "عندما تم إنشاء الحجز في الأصل",
|
||||
"call_details": "تفاصيل المكالمة",
|
||||
"call_id": "معرف المكالمة",
|
||||
"call_information": "معلومات المكالمة",
|
||||
"sentiment": "المشاعر",
|
||||
"disconnect_reason": "سبب الانقطاع",
|
||||
"call_summary": "ملخص المكالمة",
|
||||
"transcription": "النسخ",
|
||||
"event_details": "تفاصيل الحدث",
|
||||
"agent": "الوكيل",
|
||||
"no_transcript_available": "لا يوجد نص متاح",
|
||||
"testing_sms_workflow_info_message": "عند اختبار سير العمل هذا، انتبه إلى أنه يجب جدولة الرسائل النصية القصيرة قبل 15 دقيقة على الأقل",
|
||||
"start_from_scratch_title": "البدء من الصفر",
|
||||
"start_from_scratch_description": "إنشاء سير العمل الخاص بك من الصفر.",
|
||||
"cal_ai_template_title": "قالب Cal.ai",
|
||||
"cal_ai_template_description": "وكلاء الذكاء الاصطناعي الذين يحجزون الاجتماعات، ويرسلون التذكيرات، ويتابعون!",
|
||||
"voice": "الصوت",
|
||||
"select_voice": "اختر الصوت",
|
||||
"select_voice_for_agent": "اختر صوتًا لوكيلك",
|
||||
"choose_a_voice_for_your_agent": "اختر صوتًا لوكيلك",
|
||||
"trait": "سمة",
|
||||
"voice_id": "معرّف الصوت",
|
||||
"use_voice": "استخدم الصوت",
|
||||
"current_voice": "الصوت الحالي",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "Ən yaxşı e-poçt və təqvim çatdırılmasını təmin etmək üçün e-poçt ünvanınızı təsdiqləyin",
|
||||
"verify_email_email_header": "E-poçt ünvanınızı təsdiqləyin",
|
||||
"verify_email_button": "E-poçtu təsdiqlə",
|
||||
"cal_ai_assistant": "Köməkçisi",
|
||||
"cal_ai_assistant": "Köməkçi",
|
||||
"send_cal_video_transcription_emails": "Cal Video transkript e-poçtlarını göndər",
|
||||
"description_send_cal_video_transcription_emails": "Görüş bitdikdən sonra Cal Video transkripsiyası olan e-poçtları göndərin. (Ödənişli plan tələb olunur)",
|
||||
"verify_email_change_description": "Son zamanlarda {{appName}} hesabınıza daxil olmaq üçün istifadə etdiyiniz e-poçt ünvanını dəyişdirmək istədiyinizi bildirdiniz. Yeni e-poçt ünvanınızı təsdiqləmək üçün aşağıdakı düyməni basın.",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "İş axını doğrulaması uğursuz oldu",
|
||||
"workflow_validation_empty_fields": "Bir və ya daha çox iş axını addımında boş mesaj məzmunu var",
|
||||
"workflow_validation_unverified_contacts": "Bir və ya daha çox telefon nömrəsi və ya e-poçt ünvanı təsdiqlənməyib",
|
||||
"supercharge_your_workflows_with_cal_ai": "Cal.ai ilə İş Axınlarınızı Gücləndirin",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "Görüşləri təyin edən, xatırlatmalar göndərən və müştərilərinizlə əlaqə saxlayan canlı AI agentləri.",
|
||||
"phone_number_imported_successfully": "Telefon nömrəsi uğurla idxal edildi və agentə bağlandı",
|
||||
"phone_number_deleted_successfully": "Telefon nömrəsi uğurla silindi",
|
||||
"delete_phone_number_confirmation": "Bu telefon nömrəsini silmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "Telefon nömrəsi abunəliyi uğurla ləğv edildi",
|
||||
"updating": "Yenilənir",
|
||||
"round_robin": "Round Robin",
|
||||
"hi_how_are_you_doing": "Salam, necəsən?",
|
||||
"round_robin_description": "Görüşləri bir neçə komanda üzvü arasında dövr edin.",
|
||||
"managed_event": "İdarə olunan Tədbir",
|
||||
"username_placeholder": "istifadəçi adı",
|
||||
@@ -878,7 +881,7 @@
|
||||
"the_action_will_disconnect_phone_number": "Bu əməliyyat telefon nömrəsini agentdən ayıracaq. Yeni telefon nömrəsi qoşulana qədər agent zəng edə bilməyəcək.",
|
||||
"cal_ai_phone_numbers": "Telefon Nömrələri",
|
||||
"connect_phone_number": "Telefon Nömrəsini Qoş",
|
||||
"cal_ai_phone_numbers_description": "Telefon Nömrələrinizi idarə edin",
|
||||
"cal_ai_phone_numbers_description": "Telefon Nömrələrinizi İdarə edin",
|
||||
"import_number": "Nömrəni İdxal et",
|
||||
"this_action_will_also": "Bu əməliyyat həmçinin:",
|
||||
"import_phone_number": "Telefon Nömrəsini İdxal et",
|
||||
@@ -886,7 +889,7 @@
|
||||
"cancel_your_phone_number_subscription": "Telefon nömrəsi abunəliyinizi ləğv edin",
|
||||
"delete_associated_phone_number": "Əlaqəli telefon nömrəsini silin",
|
||||
"unauthorized_create_workflow": "Bu iş axınını yaratmaq üçün səlahiyyətiniz yoxdur",
|
||||
"import_phone_number_description": "Phone ilə istifadə etmək üçün Twilio telefon nömrənizi idxal edin",
|
||||
"import_phone_number_description": "Telefon ilə istifadə etmək üçün Twilio telefon nömrənizi idxal edin",
|
||||
"phone_number_cost": "${{price}}/ay",
|
||||
"buy_new_number": "Yeni Nömrə Alın",
|
||||
"buy_number_cost_x_per_month": "Telefon nömrəsi almaq ayda ${{priceInDollars}} məbləğində başa gəlir. Hər aktiv telefon nömrəsi üçün aylıq ödəniş edəcəksiniz.",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "Bəli, Abunəliyi Ləğv Et",
|
||||
"cancel_phone_number_subscription_confirmation": "Bu telefon nömrəsi abunəliyini ləğv etmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz və bu telefon nömrəsinə girişinizi itirəcəksiniz.",
|
||||
"add_members": "Üzvlər əlavə et...",
|
||||
"add_members_no_ellipsis": "Üzvləri əlavə et",
|
||||
"no_assigned_members": "Təyin edilmiş üzvlər yoxdur",
|
||||
"assigned_to": "Təyin edilmiş",
|
||||
"you_must_be_logged_in_to": "{{url}}-ə daxil olmalısınız",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "Kateqoriyalar",
|
||||
"pricing": "Qiymətlər",
|
||||
"learn_more": "Daha çox öyrən",
|
||||
"try_now": "İndi cəhd edin",
|
||||
"privacy_policy": "Məxfilik Siyasəti",
|
||||
"terms_of_service": "Xidmət Şərtləri",
|
||||
"remove": "Sil",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "iştirakçıya WhatsApp mesajı göndərin",
|
||||
"workflows": "İş axınları",
|
||||
"new_workflow_btn": "Yeni İş Axını",
|
||||
"how_would_you_like_to_start": "Necə başlamaq istəyirsiniz?",
|
||||
"add_new_workflow": "Yeni iş axını əlavə et",
|
||||
"reschedule_event_trigger": "hadisə yenidən planlaşdırıldıqda",
|
||||
"trigger": "Tətik",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "Tədbirin müddəti",
|
||||
"event_time_info": "Tədbirin başlama vaxtı",
|
||||
"event_type_not_found": "EventType tapılmadı",
|
||||
"number_to_call_variable": "Zəng ediləcək nömrə",
|
||||
"number_to_call_info": "Zəng etdiyiniz istifadəçinin telefon nömrəsi",
|
||||
"location_variable": "Məkan",
|
||||
"location_info": "Tədbirin yeri",
|
||||
"additional_notes_variable": "Əlavə qeydlər",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "Komanda URL",
|
||||
"team_members": "Komanda üzvləri",
|
||||
"more": "Daha çox",
|
||||
"cal_ai_workflows": "Cal.ai İş Axınları",
|
||||
"and_count_more": "və {{count}} daha",
|
||||
"more_page_footer": "Mobil tətbiqi veb tətbiqinin bir uzantısı kimi görürük. Hər hansı mürəkkəb əməliyyatları yerinə yetirirsinizsə, lütfən, veb tətbiqinə qayıdın.",
|
||||
"workflow_example_1": "Tədbir başlamazdan 24 saat əvvəl iştirakçıya SMS xatırlatma göndərin",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "Tədbir başlamazdan 1 saat əvvəl iştirakçıya e-poçt xatırlatma göndərin",
|
||||
"workflow_example_5": "Tədbir təxirə salındıqda ev sahibinə xüsusi e-poçt göndərin",
|
||||
"workflow_example_6": "Yeni tədbir sifariş edildikdə ev sahibinə xüsusi SMS göndərin",
|
||||
"send_sms_reminder": "SMS xatırlatması göndər",
|
||||
"send_sms_reminder_description": "Tədbir başlamazdan 24 saat əvvəl",
|
||||
"follow_up_with_no_shows": "Gəlməyənlərlə əlaqə saxla",
|
||||
"follow_up_with_no_shows_description": "Tədbir bitdikdən 30 dəqiqə sonra",
|
||||
"remind_attendees_to_bring_id": "İştirakçılara şəxsiyyət vəsiqəsi gətirməyi xatırlat",
|
||||
"remind_attendees_to_bring_id_description": "Tədbir başlamazdan 1 gün əvvəl",
|
||||
"email_to_remind_booking": "E-poçt Xatırlatması",
|
||||
"email_to_remind_booking_description": "Tədbir başlamazdan 1 saat əvvəl",
|
||||
"custom_sms_reminder": "Fərdi SMS Xatırlatması",
|
||||
"custom_sms_reminder_description": "Tədbir planlaşdırıldıqda",
|
||||
"custom_email_reminder": "Fərdi E-poçt Xatırlatması",
|
||||
"custom_email_reminder_description": "Tədbir ev sahibi üçün yenidən planlaşdırıldıqda",
|
||||
"count_managed_to_limit": "İdarə olunan tədbir növlərindən rezervasiya saylarını daxil edin",
|
||||
"welcome_to_cal_header": "{{appName}}-a xoş gəlmisiniz!",
|
||||
"edit_form_later_subtitle": "Bunu sonra redaktə edə biləcəksiniz.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "Rezervasiya səhifəsində göstər",
|
||||
"visit_cancelled_booking": "Ləğv edilmiş rezervasiya səhifəsinə baxa bilərsiniz",
|
||||
"get_started_zapier_templates": "Zapier şablonları ilə başlayın",
|
||||
"standard_templates": "Standart Şablonlar",
|
||||
"cal_ai_templates": "Cal.ai Şablonları",
|
||||
"team_is_unpublished": "{{team}} yayımlanmayıb",
|
||||
"org_is_unpublished_description": "Bu təşkilat bağlantısı hazırda mövcud deyil. Zəhmət olmasa təşkilat sahibinə müraciət edin və ya onu yayımlamasını xahiş edin.",
|
||||
"team_is_unpublished_description": "Bu komanda bağlantısı hazırda mövcud deyil. Zəhmət olmasa komanda sahibinə müraciət edin və ya onu yayımlamasını xahiş edin.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "Ödəniş üçün kartdan pul çıxıla bilmədi.",
|
||||
"insights": "Görüşlər",
|
||||
"routing_forms": "Marşrutlaşdırma Formları",
|
||||
"testing_workflow_info_message": "Bu iş axınını sınaqdan keçirərkən, e-poçtların və SMS-lərin ən azı 1 saat əvvəl planlaşdırıla biləcəyini unutmayın",
|
||||
"insights_no_data_found_for_filter": "Seçilmiş filtr və ya tarixlər üçün məlumat tapılmadı.",
|
||||
"acknowledge_booking_no_show_fee": "Bu tədbirə qatılmasam, kartıma {{amount, currency}} gəlməmə haqqı tətbiq olunacağını qəbul edirəm.",
|
||||
"days": "günlər",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "Komanda: {{teamName}}",
|
||||
"insights_user_filter": "İstifadəçi: {{userName}}",
|
||||
"insights_subtitle": "Tədbirləriniz üzrə rezervasiya məlumatlarını görün",
|
||||
"call_history": "Zəng Tarixçəsi",
|
||||
"call_history_subtitle": "Cal.ai zəngləriniz üzrə zəng tarixçəsinə baxın",
|
||||
"location_options": "{{locationCount}} məkan seçimi",
|
||||
"channel_type": "Kanal Növü",
|
||||
"end_reason": "Bitmə Səbəbi",
|
||||
"session_status": "Sessiya Statusu",
|
||||
"user_sentiment": "İstifadəçi Münasibəti",
|
||||
"time_header": "Vaxt",
|
||||
"from_header": "Kimdən",
|
||||
"custom_plan": "Xüsusi Plan",
|
||||
"email_embed": "Email Əlavəsi",
|
||||
"add_times_to_your_email": "Mövcud vaxtları seçin və Email-ə əlavə edin",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "Hesab artıq əlaqələndirilib",
|
||||
"send_email": "E-poçt göndərin",
|
||||
"cal_ai_phone_call_action": "Cal.ai Səs Agenti vasitəsilə iştirakçıya zəng edin",
|
||||
"call_to_confirm_booking": "Rezervasiyanı təsdiqləmək üçün zəng edin",
|
||||
"cal_ai_phone_call_action_description": "Tədbir başlamazdan 2 saat əvvəl",
|
||||
"cal_ai_agent_configuration": "Cal.ai Agent Konfiqurasiyası",
|
||||
"choose_at_least_one_event_type_test_call": "Test zəngi etmək üçün ən azı bir tədbir növü seçin.",
|
||||
"mark_as_no_show": "Gəlməyən kimi işarələyin",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "E-poçt dəyişikliyini təsdiqlə",
|
||||
"buy_credits": "Kredit alın",
|
||||
"credits": "Kreditlər",
|
||||
"view_and_manage_credits": "Kreditləri görüntüləyin və idarə edin",
|
||||
"credits_used": "İstifadə edilmiş kreditlər",
|
||||
"total_credits_remaining": "Ümumi qalan",
|
||||
"credits_per_tip_org": "Hər ay, hər komanda üzvü üçün 1000 kredit əldə edirsiniz",
|
||||
"credits_per_tip_teams": "Hər ay, hər komanda üzvü üçün 750 kredit əldə edirsiniz",
|
||||
"view_and_manage_credits": "SMS mesajları göndərmək üçün kreditlərə baxın və idarə edin",
|
||||
"view_and_manage_credits_description": "SMS mesajları göndərmək üçün kreditləri görüntüləyin və idarə edin. Bir kredit 1¢ (USD) dəyərindədir. <0>Daha ətraflı</0>",
|
||||
"credit_worth_description": "Bir kredit 1¢ (USD) dəyərindədir. <0>Daha ətraflı öyrənin</0>",
|
||||
"buy_additional_credits": "Əlavə kreditlər alın (kredit başına $0.01)",
|
||||
"view_additional_credits_expense_tip": "Əlavə kredit xərclərini xərc jurnalınızda görə bilərsiniz",
|
||||
"overview": "Ümumi baxış",
|
||||
"organization_slug_taken": "Təşkilat slaqı artıq istifadə olunub",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Siz artıq bir təşkilatın üzvü olduğunuz üçün yeni təşkilat yarada bilməzsiniz",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "Cal.com {{teamName}} komandanızın kreditləri bitdi. Nəticədə, SMS mesajları indi e-poçt vasitəsilə göndərilir. SMS göndərməyə davam etmək üçün, zəhmət olmasa əlavə kreditlər alın.",
|
||||
"credit_limit_reached_message_user": "Cal.com hesabınızın kreditləri bitib. Nəticə olaraq, SMS mesajları artıq e-poçt vasitəsilə göndərilir. SMS göndərməyə davam etmək üçün, zəhmət olmasa əlavə kreditlər alın.",
|
||||
"current_credit_balance": "Cari balans: {{balance}} kredit",
|
||||
"current_balance": "Cari balans:",
|
||||
"notification_about_your_booking": "Rezervasiyanız haqqında bildiriş",
|
||||
"monthly_credits": "Aylıq kreditlər",
|
||||
"total_credits": "Ümumi kreditlər: {{totalCredits}}",
|
||||
"remaining_credits": "Qalan kreditlər: {{remainingCredits}}",
|
||||
"remaining": "Qalan",
|
||||
"total": "Ümumi",
|
||||
"additional_credits": "Əlavə kreditlər",
|
||||
"routing_form_next_in_queue": "Növbədə {{count}} növbəti",
|
||||
"routing_form_select_members_to_email": "E-poçt cavablarını göndərmək üçün üzvləri seçin",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "Mövcud iş axınlarına və onların konfiqurasiyalarına baxış",
|
||||
"pbac_desc_update_workflows": "İş axını parametrlərini redaktə etmək və dəyişdirmək",
|
||||
"pbac_desc_delete_workflows": "İş axınlarını sistemdən silmək",
|
||||
"pbac_resource_webhook": "Webhook",
|
||||
"pbac_desc_create_webhooks": "Webhook yaradın",
|
||||
"pbac_desc_view_webhooks": "Webhook-lara baxın",
|
||||
"pbac_desc_update_webhooks": "Webhook-ləri yeniləyin",
|
||||
"pbac_desc_delete_webhooks": "Webhook-ləri silin",
|
||||
"pbac_desc_manage_workflows": "Bütün iş axınlarına tam idarəetmə girişi",
|
||||
"pbac_desc_create_event_types": "Tədbir növləri yaratmaq",
|
||||
"pbac_desc_view_event_types": "Tədbir növlərinə baxmaq",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "Tətikləyici hadisənin adı (məs., BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "Webhook yaradılma vaxtı",
|
||||
"webhook_type": "Tədbir növünün slaqı",
|
||||
"set_up_agent": "Agent qurun",
|
||||
"webhook_title": "Tədbir növünün adı",
|
||||
"webhook_start_time": "Tədbirin başlama vaxtı",
|
||||
"webhook_end_time": "Tədbirin bitmə vaxtı",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "Ziyarət et",
|
||||
"location_custom_label_input_label": "Rezervasiya səhifəsində xüsusi etiket",
|
||||
"meeting_link": "Görüş linki",
|
||||
"session_outcome": "Sessiya nəticəsi",
|
||||
"call_created": "Zəng yaradıldı",
|
||||
"voicemail": "Səsli poçt",
|
||||
"my_bookings": "Mənim Rezervasiyalarım",
|
||||
"phone": "Telefon",
|
||||
"free": "Pulsuz",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "İstifadəçi Adı",
|
||||
"expand_panel": "Paneli Genişlət",
|
||||
"collapse_panel": "Paneli Yığ",
|
||||
"email_verification_required": "Bu tədbir növü üçün e-poçt doğrulaması tələb olunur",
|
||||
"invalid_verification_code": "Yanlış doğrulama kodu təqdim edilib",
|
||||
"you_have_one_team": "Bir komandanız var",
|
||||
"consider_consolidating_one_team_org": "Komandanız üzrə ödənişləri, admin alətlərini və analitikanı birləşdirmək üçün təşkilat qurmağı nəzərdən keçirin.",
|
||||
"consider_consolidating_multi_team_org": "Komandalarınız üzrə ödənişləri, admin alətlərini və analitikanı birləşdirmək üçün təşkilat qurmağı nəzərdən keçirin.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "Planlaşdırılmış başlama vaxtından əvvəl",
|
||||
"cancel_booking_acknowledge_no_show_fee": "Təsdiq edirəm ki, rezervasiyanı başlama vaxtından {{timeValue}} {{timeUnit}} ərzində ləğv etməklə mənə {{amount, currency}} məbləğində gəlməmə haqqı tutulacaq",
|
||||
"contact_organizer": "Hər hansı sualınız varsa, təşkilatçı ilə əlaqə saxlayın.",
|
||||
"booking_time_option": "Rezervasiya vaxtı",
|
||||
"booking_time_option_description": "Rezervasiyanın planlaşdırıldığı vaxt (başlanğıcdan sona)",
|
||||
"created_at_option": "Yaradılma tarixi",
|
||||
"created_at_option_description": "Rezervasiyanın ilkin yaradıldığı vaxt",
|
||||
"call_details": "Zəng detalları",
|
||||
"call_id": "Zəng ID",
|
||||
"call_information": "Zəng məlumatı",
|
||||
"sentiment": "Əhval-ruhiyyə",
|
||||
"disconnect_reason": "Bağlantının kəsilmə səbəbi",
|
||||
"call_summary": "Zəng xülasəsi",
|
||||
"transcription": "Transkripsiya",
|
||||
"event_details": "Tədbir detalları",
|
||||
"agent": "Agent",
|
||||
"no_transcript_available": "Transkript mövcud deyil",
|
||||
"testing_sms_workflow_info_message": "Bu iş axınını sınaqdan keçirərkən, SMS-lərin ən azı 15 dəqiqə əvvəlcədən planlaşdırılmalı olduğunu nəzərə alın",
|
||||
"start_from_scratch_title": "Sıfırdan başlayın",
|
||||
"start_from_scratch_description": "Öz iş axınınızı sıfırdan yaradın.",
|
||||
"cal_ai_template_title": "Cal.ai şablonu",
|
||||
"cal_ai_template_description": "Görüşləri təyin edən, xatırlatmalar göndərən və davam etdirən AI agentləri!",
|
||||
"voice": "Səs",
|
||||
"select_voice": "Səs seçin",
|
||||
"select_voice_for_agent": "Agentiniz üçün səs seçin",
|
||||
"choose_a_voice_for_your_agent": "Agentiniz üçün səs seçin",
|
||||
"trait": "Xüsusiyyət",
|
||||
"voice_id": "Səs ID",
|
||||
"use_voice": "Səsdən istifadə edin",
|
||||
"current_voice": "Cari səs",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "Проверете имейл адреса си, за да гарантирате най-добрата доставка на имейли и календари",
|
||||
"verify_email_email_header": "Потвърдете вашия имейл адрес",
|
||||
"verify_email_button": "Потвърди имейл",
|
||||
"cal_ai_assistant": "асистент",
|
||||
"cal_ai_assistant": "Асистент",
|
||||
"send_cal_video_transcription_emails": "Изпращане на имейли с транскрипция на Cal Video",
|
||||
"description_send_cal_video_transcription_emails": "Изпращане на имейли с транскрипцията на Cal Video след края на срещата. (Изисква платен план)",
|
||||
"verify_email_change_description": "Наскоро поискахте да промените имейл адреса, който използвате за вход във вашия {{appName}} акаунт. Моля, кликнете върху бутона по-долу, за да потвърдите новия си имейл адрес.",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "Валидацията на работния процес е неуспешна",
|
||||
"workflow_validation_empty_fields": "Една или повече стъпки от работния процес имат празно съдържание на съобщението",
|
||||
"workflow_validation_unverified_contacts": "Един или повече телефонни номера или имейл адреси не са потвърдени",
|
||||
"supercharge_your_workflows_with_cal_ai": "Подсилете вашите работни процеси с Cal.ai",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "Реалистични AI агенти, които резервират срещи, изпращат напомняния и проследяват вашите клиенти.",
|
||||
"phone_number_imported_successfully": "Телефонният номер е импортиран и свързан с агента успешно",
|
||||
"phone_number_deleted_successfully": "Телефонният номер е изтрит успешно",
|
||||
"delete_phone_number_confirmation": "Сигурни ли сте, че искате да изтриете този телефонен номер? Това действие не може да бъде отменено.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "Абонаментът за телефонен номер е отменен успешно",
|
||||
"updating": "Обновяване",
|
||||
"round_robin": "Round Robin",
|
||||
"hi_how_are_you_doing": "Здравейте, как сте?",
|
||||
"round_robin_description": "Разпределяйте срещи между множество членове на екипа.",
|
||||
"managed_event": "Управлявано събитие",
|
||||
"username_placeholder": "потребителско име",
|
||||
@@ -876,7 +879,7 @@
|
||||
"are_you_sure_you_want_to_delete_workflow_step": "Сигурни ли сте, че искате да изтриете тази стъпка от работния процес?",
|
||||
"do_you_still_want_to_unsubscribe": "Все още ли искате да прекратите абонамента за този телефонен номер от този агент?",
|
||||
"the_action_will_disconnect_phone_number": "Това действие ще прекъсне връзката на телефонния номер с агента. Агентът няма да може да осъществява обаждания, докато не бъде свързан нов телефонен номер.",
|
||||
"cal_ai_phone_numbers": "телефонни номера",
|
||||
"cal_ai_phone_numbers": "Телефонни номера",
|
||||
"connect_phone_number": "Свързване на телефонен номер",
|
||||
"cal_ai_phone_numbers_description": "Управлявайте вашите телефонни номера",
|
||||
"import_number": "Импортиране на номер",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "Да, отказвам абонамента",
|
||||
"cancel_phone_number_subscription_confirmation": "Сигурни ли сте, че искате да прекратите абонамента за този телефонен номер? Това действие не може да бъде отменено и ще загубите достъп до този телефонен номер.",
|
||||
"add_members": "Добави членове...",
|
||||
"add_members_no_ellipsis": "Добави членове",
|
||||
"no_assigned_members": "Няма назначени членове",
|
||||
"assigned_to": "Назначено на",
|
||||
"you_must_be_logged_in_to": "Трябва да сте влезли в профила си, за да {{url}}",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "Категории",
|
||||
"pricing": "Цени",
|
||||
"learn_more": "Научи повече",
|
||||
"try_now": "Пробвайте сега",
|
||||
"privacy_policy": "Политика за поверителност",
|
||||
"terms_of_service": "Общи условия",
|
||||
"remove": "Премахни",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "изпращане на WhatsApp съобщение до участника",
|
||||
"workflows": "Работни процеси",
|
||||
"new_workflow_btn": "Нов работен процес",
|
||||
"how_would_you_like_to_start": "Как бихте искали да започнете?",
|
||||
"add_new_workflow": "Добавяне на нов работен процес",
|
||||
"reschedule_event_trigger": "когато събитието е пренасрочено",
|
||||
"trigger": "Тригер",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "Продължителността на събитието",
|
||||
"event_time_info": "Началният час на събитието",
|
||||
"event_type_not_found": "Типът събитие не е намерен",
|
||||
"number_to_call_variable": "Номер за обаждане",
|
||||
"number_to_call_info": "Телефонният номер на потребителя, на когото се обаждате",
|
||||
"location_variable": "Локация",
|
||||
"location_info": "Локацията на събитието",
|
||||
"additional_notes_variable": "Допълнителни бележки",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "URL на екипа",
|
||||
"team_members": "Членове на екипа",
|
||||
"more": "Още",
|
||||
"cal_ai_workflows": "Cal.ai работни процеси",
|
||||
"and_count_more": "и още {{count}}",
|
||||
"more_page_footer": "Разглеждаме мобилното приложение като разширение на уеб приложението. Ако извършвате сложни действия, моля, върнете се към уеб приложението.",
|
||||
"workflow_example_1": "Изпрати SMS напомняне 24 часа преди началото на събитието до участника",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "Изпрати имейл напомняне 1 час преди началото на събитията до участника",
|
||||
"workflow_example_5": "Изпрати персонализиран имейл до домакина, когато събитието е пренасрочено",
|
||||
"workflow_example_6": "Изпрати персонализиран SMS до домакина, когато е резервирано ново събитие",
|
||||
"send_sms_reminder": "Изпращане на SMS напомняне",
|
||||
"send_sms_reminder_description": "24 часа преди началото на събитието",
|
||||
"follow_up_with_no_shows": "Проследяване на неявилите се",
|
||||
"follow_up_with_no_shows_description": "30 минути след края на събитието",
|
||||
"remind_attendees_to_bring_id": "Напомняне на участниците да носят лична карта",
|
||||
"remind_attendees_to_bring_id_description": "1 ден преди началото на събитието",
|
||||
"email_to_remind_booking": "Имейл напомняне",
|
||||
"email_to_remind_booking_description": "1 час преди началото на събитието",
|
||||
"custom_sms_reminder": "Персонализирано SMS напомняне",
|
||||
"custom_sms_reminder_description": "Когато събитието е планирано",
|
||||
"custom_email_reminder": "Персонализирано имейл напомняне",
|
||||
"custom_email_reminder_description": "Събитието е пренасрочено за домакина",
|
||||
"count_managed_to_limit": "Включване на броя резервации от управляваните типове събития",
|
||||
"welcome_to_cal_header": "Добре дошли в {{appName}}!",
|
||||
"edit_form_later_subtitle": "Ще можете да редактирате това по-късно.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "Покажи на страницата за резервации",
|
||||
"visit_cancelled_booking": "Можете да посетите страницата с отменената резервация",
|
||||
"get_started_zapier_templates": "Започнете със Zapier шаблони",
|
||||
"standard_templates": "Стандартни шаблони",
|
||||
"cal_ai_templates": "Cal.ai шаблони",
|
||||
"team_is_unpublished": "{{team}} не е публикуван",
|
||||
"org_is_unpublished_description": "Тази връзка към организацията в момента не е достъпна. Моля, свържете се със собственика на организацията или го помолете да я публикува.",
|
||||
"team_is_unpublished_description": "Тази връзка към екипа в момента не е достъпна. Моля, свържете се със собственика на екипа или го помолете да я публикува.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "Не можа да се извърши плащане с картата.",
|
||||
"insights": "Анализи",
|
||||
"routing_forms": "Формуляри за маршрутизиране",
|
||||
"testing_workflow_info_message": "Когато тествате този работен процес, имайте предвид, че имейлите и SMS съобщенията могат да бъдат планирани само поне 1 час предварително",
|
||||
"insights_no_data_found_for_filter": "Не са намерени данни за избрания филтър или избраните дати.",
|
||||
"acknowledge_booking_no_show_fee": "Потвърждавам, че ако не присъствам на това събитие, ще бъде начислена такса за неявяване в размер на {{amount, currency}} на моята карта.",
|
||||
"days": "дни",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "Екип: {{teamName}}",
|
||||
"insights_user_filter": "Потребител: {{userName}}",
|
||||
"insights_subtitle": "Преглед на статистики за резервации за всички ваши събития",
|
||||
"call_history": "История на обажданията",
|
||||
"call_history_subtitle": "Преглед на историята на обажданията през вашите Cal.ai разговори",
|
||||
"location_options": "{{locationCount}} опции за локация",
|
||||
"channel_type": "Тип канал",
|
||||
"end_reason": "Причина за край",
|
||||
"session_status": "Статус на сесията",
|
||||
"user_sentiment": "Настроение на потребителя",
|
||||
"time_header": "Време",
|
||||
"from_header": "От",
|
||||
"custom_plan": "Персонализиран план",
|
||||
"email_embed": "Вграждане в имейл",
|
||||
"add_times_to_your_email": "Изберете няколко свободни часа и ги вградете във вашия имейл",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "Профилът вече е свързан",
|
||||
"send_email": "Изпрати имейл",
|
||||
"cal_ai_phone_call_action": "Обаждане до участника чрез Cal.ai гласов агент",
|
||||
"call_to_confirm_booking": "Обаждане за потвърждаване на резервация",
|
||||
"cal_ai_phone_call_action_description": "2 часа преди началото на събитието",
|
||||
"cal_ai_agent_configuration": "Конфигурация на Cal.ai агент",
|
||||
"choose_at_least_one_event_type_test_call": "Моля, изберете поне един тип събитие, за да направите тестово обаждане.",
|
||||
"mark_as_no_show": "Маркирай като неявил се",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "Потвърди промяната на имейл",
|
||||
"buy_credits": "Купете кредити",
|
||||
"credits": "Кредити",
|
||||
"view_and_manage_credits": "Преглед и управление на кредити",
|
||||
"credits_used": "Използвани кредити",
|
||||
"total_credits_remaining": "Общо оставащи",
|
||||
"credits_per_tip_org": "Получавате 1000 кредита на месец за всеки член на екипа",
|
||||
"credits_per_tip_teams": "Получавате 750 кредита на месец за всеки член на екипа",
|
||||
"view_and_manage_credits": "Преглед и управление на кредити за изпращане на SMS съобщения",
|
||||
"view_and_manage_credits_description": "Преглед и управление на кредити за изпращане на SMS съобщения. Един кредит струва 1¢ (USD). <0>Научете повече</0>",
|
||||
"credit_worth_description": "Един кредит струва 1¢ (USD). <0>Научете повече</0>",
|
||||
"buy_additional_credits": "Купете допълнителни кредити ($0.01 на кредит)",
|
||||
"view_additional_credits_expense_tip": "Можете да видите допълнителните разходи за кредити в дневника на разходите си",
|
||||
"overview": "Общ преглед",
|
||||
"organization_slug_taken": "Слъгът на организацията вече е зает",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Не можете да създадете организация, тъй като вече сте част от организация",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "Вашият Cal.com екип {{teamName}} е изчерпал кредитите си. В резултат на това SMS съобщенията сега се изпращат чрез имейл. За да възобновите изпращането на SMS, моля, закупете допълнителни кредити.",
|
||||
"credit_limit_reached_message_user": "Кредитите на вашия Cal.com акаунт са изчерпани. В резултат на това SMS съобщенията сега се изпращат чрез имейл. За да възобновите изпращането на SMS, моля, закупете допълнителни кредити.",
|
||||
"current_credit_balance": "Текущ баланс: {{balance}} кредита",
|
||||
"current_balance": "Текущ баланс:",
|
||||
"notification_about_your_booking": "Известие за вашата резервация",
|
||||
"monthly_credits": "Месечни кредити",
|
||||
"total_credits": "Общо кредити: {{totalCredits}}",
|
||||
"remaining_credits": "Оставащи кредити: {{remainingCredits}}",
|
||||
"remaining": "Оставащи",
|
||||
"total": "Общо",
|
||||
"additional_credits": "Допълнителни кредити",
|
||||
"routing_form_next_in_queue": "{{count}} следващи на опашката",
|
||||
"routing_form_select_members_to_email": "Изпращане на имейл отговори до",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "Преглед на съществуващите работни процеси и техните конфигурации",
|
||||
"pbac_desc_update_workflows": "Редактиране и модифициране на настройките на работните процеси",
|
||||
"pbac_desc_delete_workflows": "Премахване на работни процеси от системата",
|
||||
"pbac_resource_webhook": "Уебхук",
|
||||
"pbac_desc_create_webhooks": "Създаване на уебхуци",
|
||||
"pbac_desc_view_webhooks": "Преглед на уебхуци",
|
||||
"pbac_desc_update_webhooks": "Актуализиране на уебхукове",
|
||||
"pbac_desc_delete_webhooks": "Изтриване на уебхукове",
|
||||
"pbac_desc_manage_workflows": "Пълен достъп за управление на всички работни процеси",
|
||||
"pbac_desc_create_event_types": "Създаване на типове събития",
|
||||
"pbac_desc_view_event_types": "Преглед на типове събития",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "Името на задействащото събитие (напр. BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "Времето на уебхука",
|
||||
"webhook_type": "Слъгът на типа събитие",
|
||||
"set_up_agent": "Настройка на агент",
|
||||
"webhook_title": "Името на типа събитие",
|
||||
"webhook_start_time": "Началното време на събитието",
|
||||
"webhook_end_time": "Крайното време на събитието",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "Посети",
|
||||
"location_custom_label_input_label": "Персонализиран етикет на страницата за резервация",
|
||||
"meeting_link": "Линк за среща",
|
||||
"session_outcome": "Резултат от сесията",
|
||||
"call_created": "Създадено обаждане",
|
||||
"voicemail": "Гласова поща",
|
||||
"my_bookings": "Моите резервации",
|
||||
"phone": "Телефон",
|
||||
"free": "Безплатно",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "Име на потребителя",
|
||||
"expand_panel": "Разгъни панела",
|
||||
"collapse_panel": "Свий панела",
|
||||
"email_verification_required": "Изисква се имейл верификация за този тип събитие",
|
||||
"invalid_verification_code": "Предоставен е невалиден код за верификация",
|
||||
"you_have_one_team": "Имате един екип",
|
||||
"consider_consolidating_one_team_org": "Помислете за създаване на организация, за да обедините таксуването, административните инструменти и анализите за вашия екип.",
|
||||
"consider_consolidating_multi_team_org": "Помислете за създаване на организация, за да обедините таксуването, административните инструменти и анализите за вашите екипи.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "Преди планираното начално време",
|
||||
"cancel_booking_acknowledge_no_show_fee": "Потвърждавам, че при отмяна на резервацията в рамките на {{timeValue}} {{timeUnit}} преди началния час ще ми бъде начислена такса за неявяване в размер на {{amount, currency}}",
|
||||
"contact_organizer": "Ако имате въпроси, моля, свържете се с организатора.",
|
||||
"booking_time_option": "Време на резервация",
|
||||
"booking_time_option_description": "Когато резервацията е планирана (от начало до край)",
|
||||
"created_at_option": "Създадено на",
|
||||
"created_at_option_description": "Когато резервацията е първоначално създадена",
|
||||
"call_details": "Детайли на обаждането",
|
||||
"call_id": "ID на обаждането",
|
||||
"call_information": "Информация за обаждането",
|
||||
"sentiment": "Настроение",
|
||||
"disconnect_reason": "Причина за прекъсване",
|
||||
"call_summary": "Обобщение на обаждането",
|
||||
"transcription": "Транскрипция",
|
||||
"event_details": "Детайли на събитието",
|
||||
"agent": "Агент",
|
||||
"no_transcript_available": "Няма наличен транскрипт",
|
||||
"testing_sms_workflow_info_message": "Когато тествате този работен процес, имайте предвид, че SMS трябва да бъдат планирани поне 15 минути предварително",
|
||||
"start_from_scratch_title": "Започнете от нулата",
|
||||
"start_from_scratch_description": "Създайте свой собствен работен процес от нулата.",
|
||||
"cal_ai_template_title": "Cal.ai шаблон",
|
||||
"cal_ai_template_description": "AI агенти, които резервират срещи, изпращат напомняния и проследяват!",
|
||||
"voice": "Глас",
|
||||
"select_voice": "Избери глас",
|
||||
"select_voice_for_agent": "Избери глас за твоя агент",
|
||||
"choose_a_voice_for_your_agent": "Избери глас за твоя агент",
|
||||
"trait": "Характеристика",
|
||||
"voice_id": "ID на глас",
|
||||
"use_voice": "Използвай глас",
|
||||
"current_voice": "Текущ глас",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "সেরা ইমেল এবং ক্যালেন্ডার ডেলিভারিবিলিটি নিশ্চিত করতে আপনার ইমেল ঠিকানা যাচাই করুন। ",
|
||||
"verify_email_email_header": "আপনার ইমেল ঠিকানা যাচাই করুন",
|
||||
"verify_email_button": "ইমেল যাচাই করুন",
|
||||
"cal_ai_assistant": "ক্যাল এআই সহকারী",
|
||||
"cal_ai_assistant": "সহকারী",
|
||||
"send_cal_video_transcription_emails": "ক্যাল ভিডিও ট্রান্সক্রিপশন ইমেল পাঠান",
|
||||
"description_send_cal_video_transcription_emails": "মিটিং শেষ হওয়ার পর ক্যাল ভিডিওর ট্রান্সক্রিপশন সহ ইমেল পাঠান। (একটি পেইড প্ল্যান প্রয়োজন)",
|
||||
"verify_email_change_description": "আপনি সম্প্রতি আপনার লগ ইন করতে ব্যবহার করা ইমেল ঠিকানা পরিবর্তন করার জন্য অনুরোধ করেছেন {{appName}} অ্যাকাউন্ট। ",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "ওয়ার্কফ্লো যাচাইকরণ ব্যর্থ হয়েছে",
|
||||
"workflow_validation_empty_fields": "এক বা একাধিক ওয়ার্কফ্লো ধাপে খালি বার্তা বিষয়বস্তু রয়েছে",
|
||||
"workflow_validation_unverified_contacts": "এক বা একাধিক ফোন নম্বর বা ইমেল ঠিকানা যাচাই করা হয়নি",
|
||||
"supercharge_your_workflows_with_cal_ai": "Cal.ai দিয়ে আপনার ওয়ার্কফ্লো সুপারচার্জ করুন",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "জীবন্ত এআই এজেন্ট যারা মিটিং বুক করে, রিমাইন্ডার পাঠায়, এবং আপনার গ্রাহকদের সাথে ফলোআপ করে।",
|
||||
"phone_number_imported_successfully": "ফোন নম্বর সফলভাবে আমদানি করা হয়েছে এবং এজেন্টের সাথে সংযুক্ত করা হয়েছে",
|
||||
"phone_number_deleted_successfully": "ফোন নম্বর সফলভাবে মুছে ফেলা হয়েছে",
|
||||
"delete_phone_number_confirmation": "আপনি কি নিশ্চিত যে আপনি এই ফোন নম্বরটি মুছতে চান? এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "ফোন নম্বর সাবস্ক্রিপশন সফলভাবে বাতিল করা হয়েছে",
|
||||
"updating": "আপডেট করা হচ্ছে",
|
||||
"round_robin": "রাউন্ড রবিন",
|
||||
"hi_how_are_you_doing": "হাই, আপনি কেমন আছেন?",
|
||||
"round_robin_description": "একাধিক দলের সদস্যের মধ্যে চক্র সভা।",
|
||||
"managed_event": "পরিচালিত ইভেন্ট",
|
||||
"username_placeholder": "ব্যবহারকারীর নাম",
|
||||
@@ -876,7 +879,7 @@
|
||||
"are_you_sure_you_want_to_delete_workflow_step": "আপনি কি নিশ্চিত যে আপনি এই ওয়ার্কফ্লো ধাপটি মুছতে চান?",
|
||||
"do_you_still_want_to_unsubscribe": "আপনি কি এখনও এই এজেন্ট থেকে ফোন নম্বরটি আনসাবস্ক্রাইব করতে চান?",
|
||||
"the_action_will_disconnect_phone_number": "এই কাজটি এজেন্ট থেকে ফোন নম্বরটি বিচ্ছিন্ন করবে। নতুন ফোন নম্বর সংযোগ না করা পর্যন্ত এজেন্ট কল করতে সক্ষম হবে না।",
|
||||
"cal_ai_phone_numbers": "ফোন নম্বর",
|
||||
"cal_ai_phone_numbers": "ফোন নম্বরসমূহ",
|
||||
"connect_phone_number": "ফোন নম্বর সংযোগ করুন",
|
||||
"cal_ai_phone_numbers_description": "আপনার ফোন নম্বরগুলি পরিচালনা করুন",
|
||||
"import_number": "নম্বর আমদানি করুন",
|
||||
@@ -886,7 +889,7 @@
|
||||
"cancel_your_phone_number_subscription": "আপনার ফোন নম্বর সাবস্ক্রিপশন বাতিল করুন",
|
||||
"delete_associated_phone_number": "সংযুক্ত ফোন নম্বরটি মুছুন",
|
||||
"unauthorized_create_workflow": "আপনি এই ওয়ার্কফ্লো তৈরি করার জন্য অনুমোদিত নন",
|
||||
"import_phone_number_description": "ফোনের সাথে ব্যবহার করার জন্য আপনার Twilio ফোন নম্বর আমদানি করুন",
|
||||
"import_phone_number_description": "ফোনের সাথে ব্যবহার করার জন্য আপনার Twilio ফোন নম্বর ইম্পোর্ট করুন",
|
||||
"phone_number_cost": "${{price}}/মাস",
|
||||
"buy_new_number": "নতুন নম্বর কিনুন",
|
||||
"buy_number_cost_x_per_month": "একটি ফোন নম্বর কেনার খরচ মাসে ${{priceInDollars}}। প্রতিটি সক্রিয় ফোন নম্বরের জন্য আপনাকে মাসিক চার্জ করা হবে।",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "হ্যাঁ, সাবস্ক্রিপশন বাতিল করুন",
|
||||
"cancel_phone_number_subscription_confirmation": "আপনি কি নিশ্চিত যে আপনি এই ফোন নম্বর সাবস্ক্রিপশন বাতিল করতে চান? এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না এবং আপনি এই ফোন নম্বরে অ্যাক্সেস হারাবেন।",
|
||||
"add_members": "সদস্য যোগ করুন ...",
|
||||
"add_members_no_ellipsis": "সদস্য যোগ করুন",
|
||||
"no_assigned_members": "কোনও নির্ধারিত সদস্য নেই",
|
||||
"assigned_to": "নির্ধারিত",
|
||||
"you_must_be_logged_in_to": "আপনি অবশ্যই লগ ইন করতে হবে {{url}}",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "বিভাগ",
|
||||
"pricing": "মূল্য নির্ধারণ",
|
||||
"learn_more": "আরও শিখুন",
|
||||
"try_now": "এখনই চেষ্টা করুন",
|
||||
"privacy_policy": "গোপনীয়তা নীতি",
|
||||
"terms_of_service": "পরিষেবার শর্তাদি",
|
||||
"remove": "সরান",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "অংশগ্রহণকারীদের কাছে হোয়াটসঅ্যাপ বার্তা প্রেরণ করুন",
|
||||
"workflows": "কর্মপ্রবাহ",
|
||||
"new_workflow_btn": "নতুন কর্মপ্রবাহ",
|
||||
"how_would_you_like_to_start": "আপনি কীভাবে শুরু করতে চান?",
|
||||
"add_new_workflow": "একটি নতুন ওয়ার্কফ্লো যুক্ত করুন",
|
||||
"reschedule_event_trigger": "যখন ইভেন্টটি পুনরায় নির্ধারণ করা হয়",
|
||||
"trigger": "ট্রিগার",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "ইভেন্টের সময়কাল",
|
||||
"event_time_info": "ইভেন্ট শুরু সময়",
|
||||
"event_type_not_found": "ইভেন্টটাইপ পাওয়া যায় নি",
|
||||
"number_to_call_variable": "কল করার নম্বর",
|
||||
"number_to_call_info": "আপনি যে ব্যবহারকারীকে কল করছেন তার ফোন নম্বর",
|
||||
"location_variable": "অবস্থান",
|
||||
"location_info": "ইভেন্টের অবস্থান",
|
||||
"additional_notes_variable": "অতিরিক্ত নোট",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "টিম ইউআরএল",
|
||||
"team_members": "দলের সদস্য",
|
||||
"more": "আরও",
|
||||
"cal_ai_workflows": "Cal.ai ওয়ার্কফ্লো",
|
||||
"and_count_more": "এবং আরও {{count}}টি",
|
||||
"more_page_footer": "আমরা মোবাইল অ্যাপ্লিকেশনটিকে ওয়েব অ্যাপ্লিকেশনটির এক্সটেনশন হিসাবে দেখি। ",
|
||||
"workflow_example_1": "ইভেন্টটি অংশ নিতে শুরু করার 24 ঘন্টা আগে এসএমএস অনুস্মারক প্রেরণ করুন",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "ইভেন্টগুলি উপস্থিত হতে শুরু করার 1 ঘন্টা আগে ইমেল অনুস্মারক প্রেরণ করুন",
|
||||
"workflow_example_5": "ইভেন্টটি হোস্টে পুনরায় নির্ধারণ করা হলে কাস্টম ইমেল প্রেরণ করুন",
|
||||
"workflow_example_6": "হোস্ট করার জন্য নতুন ইভেন্ট বুক করা হলে কাস্টম এসএমএস প্রেরণ করুন",
|
||||
"send_sms_reminder": "এসএমএস রিমাইন্ডার পাঠান",
|
||||
"send_sms_reminder_description": "ইভেন্ট শুরু হওয়ার ২৪ ঘন্টা আগে",
|
||||
"follow_up_with_no_shows": "অনুপস্থিতদের সাথে ফলোআপ করুন",
|
||||
"follow_up_with_no_shows_description": "ইভেন্ট শেষ হওয়ার ৩০ মিনিট পরে",
|
||||
"remind_attendees_to_bring_id": "অংশগ্রহণকারীদের আইডি আনতে মনে করিয়ে দিন",
|
||||
"remind_attendees_to_bring_id_description": "ইভেন্ট শুরু হওয়ার ১ দিন আগে",
|
||||
"email_to_remind_booking": "ইমেইল রিমাইন্ডার",
|
||||
"email_to_remind_booking_description": "ইভেন্ট শুরু হওয়ার ১ ঘন্টা আগে",
|
||||
"custom_sms_reminder": "কাস্টম এসএমএস রিমাইন্ডার",
|
||||
"custom_sms_reminder_description": "যখন ইভেন্ট শিডিউল করা হয়",
|
||||
"custom_email_reminder": "কাস্টম ইমেইল রিমাইন্ডার",
|
||||
"custom_email_reminder_description": "ইভেন্ট হোস্টের জন্য পুনঃনির্ধারিত করা হয়েছে",
|
||||
"count_managed_to_limit": "পরিচালিত ইভেন্টের প্রকারগুলি থেকে বুকিং গণনা অন্তর্ভুক্ত করুন",
|
||||
"welcome_to_cal_header": "স্বাগতম {{appName}}আর!",
|
||||
"edit_form_later_subtitle": "আপনি পরে এটি সম্পাদনা করতে পারবেন।",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "বুকিং পৃষ্ঠায় দেখান",
|
||||
"visit_cancelled_booking": "আপনি বাতিল হওয়া বুকিং পৃষ্ঠাটি দেখতে পারেন",
|
||||
"get_started_zapier_templates": "জ্যাপিয়ার টেম্পলেটগুলি দিয়ে শুরু করুন",
|
||||
"standard_templates": "স্ট্যান্ডার্ড টেমপ্লেট",
|
||||
"cal_ai_templates": "Cal.ai টেমপ্লেট",
|
||||
"team_is_unpublished": "{{team}} অপ্রকাশিত",
|
||||
"org_is_unpublished_description": "এই সংস্থার লিঙ্কটি বর্তমানে উপলভ্য নয়। ",
|
||||
"team_is_unpublished_description": "এই দলের লিঙ্কটি বর্তমানে উপলভ্য নয়। ",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "অর্থ প্রদানের জন্য কার্ড চার্জ করতে পারেনি।",
|
||||
"insights": "অন্তর্দৃষ্টি",
|
||||
"routing_forms": "রাউটিং ফর্ম",
|
||||
"testing_workflow_info_message": "এই কর্মপ্রবাহটি পরীক্ষা করার সময়, সচেতন হন যে ইমেলগুলি এবং এসএমএস কেবলমাত্র কমপক্ষে 1 ঘন্টা আগে নির্ধারিত হতে পারে",
|
||||
"insights_no_data_found_for_filter": "নির্বাচিত ফিল্টার বা নির্বাচিত তারিখগুলির জন্য কোনও ডেটা পাওয়া যায় নি।",
|
||||
"acknowledge_booking_no_show_fee": "আমি স্বীকার করি যে আমি যদি এই ইভেন্টে অংশ না নিই তবে ক {{amount, currency}} আমার কার্ডে কোনও শো ফি প্রয়োগ করা হবে না।",
|
||||
"days": "দিন",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "দল: {{teamName}}",
|
||||
"insights_user_filter": "ব্যবহারকারী: {{userName}}",
|
||||
"insights_subtitle": "আপনার ইভেন্টগুলি জুড়ে বুকিং অন্তর্দৃষ্টি দেখুন",
|
||||
"call_history": "কল ইতিহাস",
|
||||
"call_history_subtitle": "আপনার Cal.ai কলগুলির কল ইতিহাস দেখুন",
|
||||
"location_options": "{{locationCount}} অবস্থান বিকল্প",
|
||||
"channel_type": "চ্যানেল টাইপ",
|
||||
"end_reason": "শেষ হওয়ার কারণ",
|
||||
"session_status": "সেশন স্ট্যাটাস",
|
||||
"user_sentiment": "ব্যবহারকারীর অনুভূতি",
|
||||
"time_header": "সময়",
|
||||
"from_header": "থেকে",
|
||||
"custom_plan": "কাস্টম পরিকল্পনা",
|
||||
"email_embed": "ইমেল এম্বেড",
|
||||
"add_times_to_your_email": "কয়েকটি উপলভ্য সময় নির্বাচন করুন এবং সেগুলি আপনার ইমেলটিতে এম্বেড করুন",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "অ্যাকাউন্ট ইতিমধ্যে লিঙ্কযুক্ত",
|
||||
"send_email": "ইমেল প্রেরণ",
|
||||
"cal_ai_phone_call_action": "Cal.ai ভয়েস এজেন্ট ব্যবহার করে অংশগ্রহণকারীকে কল করুন",
|
||||
"call_to_confirm_booking": "বুকিং নিশ্চিত করতে কল করুন",
|
||||
"cal_ai_phone_call_action_description": "ইভেন্ট শুরু হওয়ার ২ ঘন্টা আগে",
|
||||
"cal_ai_agent_configuration": "Cal.ai এজেন্ট কনফিগারেশন",
|
||||
"choose_at_least_one_event_type_test_call": "একটি টেস্ট কল করতে অনুগ্রহ করে কমপক্ষে একটি ইভেন্ট টাইপ বেছে নিন।",
|
||||
"mark_as_no_show": "নো-শো হিসাবে চিহ্নিত করুন",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "ইমেইল পরিবর্তন যাচাই করুন",
|
||||
"buy_credits": "ক্রেডিট কিনুন",
|
||||
"credits": "ক্রেডিট",
|
||||
"view_and_manage_credits": "ক্রেডিট দেখুন এবং পরিচালনা করুন",
|
||||
"credits_used": "ব্যবহৃত ক্রেডিট",
|
||||
"total_credits_remaining": "মোট অবশিষ্ট",
|
||||
"credits_per_tip_org": "আপনি প্রতি মাসে, প্রতি টিম সদস্যের জন্য ১০০০ ক্রেডিট পান",
|
||||
"credits_per_tip_teams": "আপনি প্রতি মাসে, প্রতি টিম সদস্যের জন্য ৭৫০ ক্রেডিট পান",
|
||||
"view_and_manage_credits": "এসএমএস বার্তা পাঠানোর জন্য ক্রেডিট দেখুন এবং পরিচালনা করুন",
|
||||
"view_and_manage_credits_description": "এসএমএস বার্তা পাঠানোর জন্য ক্রেডিট দেখুন এবং পরিচালনা করুন। একটি ক্রেডিটের মূল্য 1¢ (USD)। <0>আরও জানুন</0>",
|
||||
"credit_worth_description": "একটি ক্রেডিটের মূল্য ১¢ (USD)। <0>আরও জানুন</0>",
|
||||
"buy_additional_credits": "অতিরিক্ত ক্রেডিট কিনুন ($0.01 প্রতি ক্রেডিট)",
|
||||
"view_additional_credits_expense_tip": "আপনি আপনার ব্যয় লগে অতিরিক্ত ক্রেডিট খরচ দেখতে পারেন",
|
||||
"overview": "সংক্ষিপ্ত বিবরণ",
|
||||
"organization_slug_taken": "সংগঠনের স্লাগ ইতিমধ্যে নেওয়া হয়েছে",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "আপনি একটি সংগঠন তৈরি করতে পারবেন না কারণ আপনি ইতিমধ্যে একটি সংগঠনের অংশ",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "আপনার Cal.com টিম {{teamName}} এর ক্রেডিট শেষ হয়ে গেছে। ফলস্বরূপ, এসএমএস বার্তাগুলি এখন ইমেইলের মাধ্যমে পাঠানো হচ্ছে। এসএমএস পাঠানো পুনরায় শুরু করতে, অনুগ্রহ করে অতিরিক্ত ক্রেডিট কিনুন।",
|
||||
"credit_limit_reached_message_user": "আপনার Cal.com অ্যাকাউন্টের ক্রেডিট শেষ হয়ে গেছে। ফলস্বরূপ, এসএমএস বার্তাগুলি এখন ইমেইলের মাধ্যমে পাঠানো হচ্ছে। এসএমএস পাঠানো পুনরায় শুরু করতে, অনুগ্রহ করে অতিরিক্ত ক্রেডিট কিনুন।",
|
||||
"current_credit_balance": "বর্তমান ব্যালেন্স: {{balance}} ক্রেডিট",
|
||||
"current_balance": "বর্তমান ব্যালেন্স:",
|
||||
"notification_about_your_booking": "আপনার বুকিং সম্পর্কে বিজ্ঞপ্তি",
|
||||
"monthly_credits": "মাসিক ক্রেডিট",
|
||||
"total_credits": "মোট ক্রেডিট: {{totalCredits}}",
|
||||
"remaining_credits": "অবশিষ্ট ক্রেডিট: {{remainingCredits}}",
|
||||
"remaining": "অবশিষ্ট",
|
||||
"total": "মোট",
|
||||
"additional_credits": "অতিরিক্ত ক্রেডিট",
|
||||
"routing_form_next_in_queue": "সারিতে পরবর্তী {{count}}টি",
|
||||
"routing_form_select_members_to_email": "ইমেইল প্রতিক্রিয়া পাঠানোর জন্য সদস্য নির্বাচন করুন",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "বিদ্যমান কর্মপ্রবাহ এবং তাদের কনফিগারেশন দেখুন",
|
||||
"pbac_desc_update_workflows": "কর্মপ্রবাহ সেটিংস সম্পাদনা ও পরিবর্তন করুন",
|
||||
"pbac_desc_delete_workflows": "সিস্টেম থেকে কর্মপ্রবাহ সরান",
|
||||
"pbac_resource_webhook": "ওয়েবহুক",
|
||||
"pbac_desc_create_webhooks": "ওয়েবহুক তৈরি করুন",
|
||||
"pbac_desc_view_webhooks": "ওয়েবহুক দেখুন",
|
||||
"pbac_desc_update_webhooks": "ওয়েবহুক আপডেট করুন",
|
||||
"pbac_desc_delete_webhooks": "ওয়েবহুক মুছুন",
|
||||
"pbac_desc_manage_workflows": "সমস্ত কর্মপ্রবাহের জন্য সম্পূর্ণ ব্যবস্থাপনা অ্যাক্সেস",
|
||||
"pbac_desc_create_event_types": "ইভেন্টের ধরণ তৈরি করুন",
|
||||
"pbac_desc_view_event_types": "ইভেন্টের ধরণ দেখুন",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "ট্রিগার ইভেন্টের নাম (যেমন, BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "ওয়েবহুকের সময়",
|
||||
"webhook_type": "ইভেন্ট টাইপ স্লাগ",
|
||||
"set_up_agent": "এজেন্ট সেট আপ করুন",
|
||||
"webhook_title": "ইভেন্ট টাইপের নাম",
|
||||
"webhook_start_time": "ইভেন্টের শুরু সময়",
|
||||
"webhook_end_time": "ইভেন্টের শেষ সময়",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "দেখুন",
|
||||
"location_custom_label_input_label": "বুকিং পৃষ্ঠায় কাস্টম লেবেল",
|
||||
"meeting_link": "সভার লিঙ্ক",
|
||||
"session_outcome": "সেশনের ফলাফল",
|
||||
"call_created": "কল তৈরি হয়েছে",
|
||||
"voicemail": "ভয়েসমেইল",
|
||||
"my_bookings": "আমার বুকিং",
|
||||
"phone": "ফোন",
|
||||
"free": "বিনামূল্যে",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "ব্যবহারকারীর নাম",
|
||||
"expand_panel": "প্যানেল বিস্তৃত করুন",
|
||||
"collapse_panel": "প্যানেল সংকুচিত করুন",
|
||||
"email_verification_required": "এই ইভেন্ট টাইপের জন্য ইমেইল যাচাইকরণ প্রয়োজন",
|
||||
"invalid_verification_code": "অবৈধ যাচাইকরণ কোড প্রদান করা হয়েছে",
|
||||
"you_have_one_team": "আপনার একটি দল আছে",
|
||||
"consider_consolidating_one_team_org": "আপনার দলের মধ্যে বিলিং, অ্যাডমিন টুলস এবং অ্যানালিটিক্স একত্রিত করতে একটি সংগঠন সেট আপ করার কথা বিবেচনা করুন।",
|
||||
"consider_consolidating_multi_team_org": "আপনার দলগুলির মধ্যে বিলিং, অ্যাডমিন টুলস এবং অ্যানালিটিক্স একত্রিত করতে একটি সংগঠন সেট আপ করার কথা বিবেচনা করুন।",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "নির্ধারিত শুরুর সময়ের আগে",
|
||||
"cancel_booking_acknowledge_no_show_fee": "আমি স্বীকার করছি যে শুরুর সময়ের {{timeValue}} {{timeUnit}} এর মধ্যে বুকিং বাতিল করলে আমাকে {{amount, currency}} অনুপস্থিতি ফি চার্জ করা হবে",
|
||||
"contact_organizer": "আপনার কোন প্রশ্ন থাকলে, অনুগ্রহ করে আয়োজকের সাথে যোগাযোগ করুন।",
|
||||
"booking_time_option": "বুকিং সময়",
|
||||
"booking_time_option_description": "যখন বুকিং নির্ধারিত হয় (শুরু থেকে শেষ)",
|
||||
"created_at_option": "তৈরি হয়েছে",
|
||||
"created_at_option_description": "যখন বুকিং প্রথম তৈরি করা হয়েছিল",
|
||||
"call_details": "কলের বিবরণ",
|
||||
"call_id": "কল আইডি",
|
||||
"call_information": "কলের তথ্য",
|
||||
"sentiment": "অনুভূতি",
|
||||
"disconnect_reason": "সংযোগ বিচ্ছেদের কারণ",
|
||||
"call_summary": "কলের সারাংশ",
|
||||
"transcription": "ট্রান্সক্রিপশন",
|
||||
"event_details": "ইভেন্টের বিবরণ",
|
||||
"agent": "এজেন্ট",
|
||||
"no_transcript_available": "কোন ট্রান্সক্রিপ্ট উপলব্ধ নেই",
|
||||
"testing_sms_workflow_info_message": "এই ওয়ার্কফ্লো পরীক্ষা করার সময়, মনে রাখবেন যে এসএমএস অবশ্যই কমপক্ষে ১৫ মিনিট আগে নির্ধারিত করতে হবে",
|
||||
"start_from_scratch_title": "শূন্য থেকে শুরু করুন",
|
||||
"start_from_scratch_description": "শূন্য থেকে আপনার নিজের ওয়ার্কফ্লো তৈরি করুন।",
|
||||
"cal_ai_template_title": "Cal.ai টেমপ্লেট",
|
||||
"cal_ai_template_description": "এআই এজেন্ট যারা মিটিং বুক করে, রিমাইন্ডার পাঠায়, এবং ফলো আপ করে!",
|
||||
"voice": "ভয়েস",
|
||||
"select_voice": "ভয়েস নির্বাচন করুন",
|
||||
"select_voice_for_agent": "আপনার এজেন্টের জন্য একটি ভয়েস নির্বাচন করুন",
|
||||
"choose_a_voice_for_your_agent": "আপনার এজেন্টের জন্য একটি ভয়েস বেছে নিন",
|
||||
"trait": "বৈশিষ্ট্য",
|
||||
"voice_id": "ভয়েস আইডি",
|
||||
"use_voice": "ভয়েস ব্যবহার করুন",
|
||||
"current_voice": "বর্তমান ভয়েস",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑ুষান্ত"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "Verifica la teva adreça de correu per garantir la millor entrega de correus i calendaris",
|
||||
"verify_email_email_header": "Verifica la teva adreça de correu",
|
||||
"verify_email_button": "Verifica el correu electrònic",
|
||||
"cal_ai_assistant": "Assistent IA de Cal",
|
||||
"cal_ai_assistant": "Assistent",
|
||||
"send_cal_video_transcription_emails": "Envia correus amb la transcripció del vídeo",
|
||||
"description_send_cal_video_transcription_emails": "Envia correus amb la transcripció del vídeo de Cal després que finalitzi la reunió. (Requereix un pla de pagament)",
|
||||
"verify_email_change_description": "Has sol·licitat recentment canviar l'adreça de correu que utilitzes per iniciar sessió al teu compte de {{appName}}. Si us plau, fes clic al botó següent per confirmar la nova adreça.",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "La validació del flux de treball ha fallat",
|
||||
"workflow_validation_empty_fields": "Un o més passos del flux de treball tenen contingut de missatge buit",
|
||||
"workflow_validation_unverified_contacts": "Un o més números de telèfon o adreces de correu electrònic no estan verificats",
|
||||
"supercharge_your_workflows_with_cal_ai": "Potencia els teus fluxos de treball amb Cal.ai",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "Agents d'IA realistes que programen reunions, envien recordatoris i fan seguiment amb els teus clients.",
|
||||
"phone_number_imported_successfully": "Número de telèfon importat i vinculat a l'agent correctament",
|
||||
"phone_number_deleted_successfully": "Número de telèfon eliminat correctament",
|
||||
"delete_phone_number_confirmation": "Estàs segur que vols eliminar aquest número de telèfon? Aquesta acció no es pot desfer.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "Subscripció del número de telèfon cancel·lada correctament",
|
||||
"updating": "Actualitzant",
|
||||
"round_robin": "Round Robin",
|
||||
"hi_how_are_you_doing": "Hola, com estàs?",
|
||||
"round_robin_description": "Alterna les reunions entre diversos membres de l'equip.",
|
||||
"managed_event": "Esdeveniment gestionat",
|
||||
"username_placeholder": "nom d'usuari",
|
||||
@@ -876,9 +879,9 @@
|
||||
"are_you_sure_you_want_to_delete_workflow_step": "Estàs segur que vols eliminar aquest pas del flux de treball?",
|
||||
"do_you_still_want_to_unsubscribe": "Encara vols donar de baixa el número de telèfon d'aquest agent?",
|
||||
"the_action_will_disconnect_phone_number": "Aquesta acció desconnectarà el número de telèfon de l'agent. L'agent no podrà fer trucades fins que es connecti un nou número de telèfon.",
|
||||
"cal_ai_phone_numbers": "Números de telèfon de Cal AI",
|
||||
"cal_ai_phone_numbers": "Números de telèfon",
|
||||
"connect_phone_number": "Connecta un número de telèfon",
|
||||
"cal_ai_phone_numbers_description": "Gestiona els teus números de telèfon de Cal AI",
|
||||
"cal_ai_phone_numbers_description": "Gestiona els teus números de telèfon",
|
||||
"import_number": "Importa un número",
|
||||
"this_action_will_also": "Aquesta acció també:",
|
||||
"import_phone_number": "Importa un número de telèfon",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "Sí, cancel·lar la subscripció",
|
||||
"cancel_phone_number_subscription_confirmation": "Estàs segur que vols cancel·lar aquesta subscripció de número de telèfon? Aquesta acció no es pot desfer i perdràs l'accés a aquest número de telèfon.",
|
||||
"add_members": "Afegir membres...",
|
||||
"add_members_no_ellipsis": "Afegir membres",
|
||||
"no_assigned_members": "No hi ha membres assignats",
|
||||
"assigned_to": "Assignat a",
|
||||
"you_must_be_logged_in_to": "Has d'iniciar sessió per {{url}}",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "Categories",
|
||||
"pricing": "Preus",
|
||||
"learn_more": "Més informació",
|
||||
"try_now": "Prova-ho ara",
|
||||
"privacy_policy": "Política de privacitat",
|
||||
"terms_of_service": "Condicions del servei",
|
||||
"remove": "Elimina",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "enviar missatge de WhatsApp a l'assistent",
|
||||
"workflows": "Fluxos de treball",
|
||||
"new_workflow_btn": "Nou flux de treball",
|
||||
"how_would_you_like_to_start": "Com vols començar?",
|
||||
"add_new_workflow": "Afegir un nou flux de treball",
|
||||
"reschedule_event_trigger": "quan es canvia l'hora de l'esdeveniment",
|
||||
"trigger": "Activador",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "La durada de l'esdeveniment",
|
||||
"event_time_info": "L'hora d'inici de l'esdeveniment",
|
||||
"event_type_not_found": "Tipus d'esdeveniment no trobat",
|
||||
"number_to_call_variable": "Número per trucar",
|
||||
"number_to_call_info": "El número de telèfon de l'usuari al qual estàs trucant",
|
||||
"location_variable": "Ubicació",
|
||||
"location_info": "La ubicació de l'esdeveniment",
|
||||
"additional_notes_variable": "Notes addicionals",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "URL de l'equip",
|
||||
"team_members": "Membres de l'equip",
|
||||
"more": "Més",
|
||||
"cal_ai_workflows": "Fluxos de treball de Cal.ai",
|
||||
"and_count_more": "i {{count}} més",
|
||||
"more_page_footer": "Considerem l'aplicació mòbil com una extensió de l'aplicació web. Si has de realitzar accions complexes, si us plau, torna a l'aplicació web.",
|
||||
"workflow_example_1": "Envia un recordatori per SMS 24 hores abans que comenci l'esdeveniment al participant",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "Envia un recordatori per correu 1 hora abans que comenci l'esdeveniment al participant",
|
||||
"workflow_example_5": "Envia un correu personalitzat a l'amfitrió quan es reprogrami l'esdeveniment",
|
||||
"workflow_example_6": "Envia un SMS personalitzat a l'amfitrió quan es reservi un nou esdeveniment",
|
||||
"send_sms_reminder": "Enviar recordatori per SMS",
|
||||
"send_sms_reminder_description": "24 hores abans de l'inici de l'esdeveniment",
|
||||
"follow_up_with_no_shows": "Seguiment dels que no s'han presentat",
|
||||
"follow_up_with_no_shows_description": "30 minuts després de finalitzar l'esdeveniment",
|
||||
"remind_attendees_to_bring_id": "Recordar als assistents que portin identificació",
|
||||
"remind_attendees_to_bring_id_description": "1 dia abans de l'inici de l'esdeveniment",
|
||||
"email_to_remind_booking": "Recordatori per correu electrònic",
|
||||
"email_to_remind_booking_description": "1 hora abans de l'inici de l'esdeveniment",
|
||||
"custom_sms_reminder": "Recordatori SMS personalitzat",
|
||||
"custom_sms_reminder_description": "Quan es programa l'esdeveniment",
|
||||
"custom_email_reminder": "Recordatori per correu electrònic personalitzat",
|
||||
"custom_email_reminder_description": "L'esdeveniment es reprograma per a l'amfitrió",
|
||||
"count_managed_to_limit": "Inclou el recompte de reserves dels tipus d'esdeveniments gestionats",
|
||||
"welcome_to_cal_header": "Benvingut/da a {{appName}}!",
|
||||
"edit_form_later_subtitle": "Podràs editar-ho més tard.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "Mostra a la pàgina de reserva",
|
||||
"visit_cancelled_booking": "Pots visitar la pàgina de reserva cancel·lada",
|
||||
"get_started_zapier_templates": "Comença amb les plantilles de Zapier",
|
||||
"standard_templates": "Plantilles estàndard",
|
||||
"cal_ai_templates": "Plantilles de Cal.ai",
|
||||
"team_is_unpublished": "{{team}} no està publicat",
|
||||
"org_is_unpublished_description": "Aquest enllaç d'organització no està disponible actualment. Si us plau, contacta amb el propietari de l'organització o demana-li que el publiqui.",
|
||||
"team_is_unpublished_description": "Aquest enllaç d'equip no està disponible actualment. Si us plau, contacta amb el propietari de l'equip o demana-li que el publiqui.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "No s'ha pogut carregar la targeta per al pagament.",
|
||||
"insights": "Estadístiques",
|
||||
"routing_forms": "Formularis de redirecció",
|
||||
"testing_workflow_info_message": "Quan provis aquest flux de treball, tingues en compte que els correus electrònics i SMS només es poden programar amb almenys 1 hora d'antelació",
|
||||
"insights_no_data_found_for_filter": "No s'han trobat dades per al filtre seleccionat o les dates seleccionades.",
|
||||
"acknowledge_booking_no_show_fee": "Reconec que si no assisteixo a aquest esdeveniment, s'aplicarà una penalització de {{amount, currency}} a la meva targeta.",
|
||||
"days": "dies",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "Equip: {{teamName}}",
|
||||
"insights_user_filter": "Usuari: {{userName}}",
|
||||
"insights_subtitle": "Visualitzeu les estadístiques de reserves dels vostres esdeveniments",
|
||||
"call_history": "Historial de trucades",
|
||||
"call_history_subtitle": "Visualitza l'historial de trucades de les teves trucades de Cal.ai",
|
||||
"location_options": "{{locationCount}} opcions d'ubicació",
|
||||
"channel_type": "Tipus de canal",
|
||||
"end_reason": "Motiu de finalització",
|
||||
"session_status": "Estat de la sessió",
|
||||
"user_sentiment": "Sentiment de l'usuari",
|
||||
"time_header": "Hora",
|
||||
"from_header": "De",
|
||||
"custom_plan": "Pla personalitzat",
|
||||
"email_embed": "Incrustació al correu",
|
||||
"add_times_to_your_email": "Seleccioneu alguns horaris disponibles i incrusteu-los al vostre correu",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "El compte ja està vinculat",
|
||||
"send_email": "Envia un correu electrònic",
|
||||
"cal_ai_phone_call_action": "Trucar a l'assistent utilitzant l'agent de veu Cal.ai",
|
||||
"call_to_confirm_booking": "Trucada per confirmar la reserva",
|
||||
"cal_ai_phone_call_action_description": "2 hores abans de l'inici de l'esdeveniment",
|
||||
"cal_ai_agent_configuration": "Configuració de l'agent Cal.ai",
|
||||
"choose_at_least_one_event_type_test_call": "Si us plau, escull almenys un tipus d'esdeveniment per fer una trucada de prova.",
|
||||
"mark_as_no_show": "Marca com a no presentat",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "Verifica el canvi de correu electrònic",
|
||||
"buy_credits": "Compra crèdits",
|
||||
"credits": "Crèdits",
|
||||
"view_and_manage_credits": "Visualitza i gestiona els crèdits",
|
||||
"credits_used": "Crèdits utilitzats",
|
||||
"total_credits_remaining": "Total restant",
|
||||
"credits_per_tip_org": "Reps 1000 crèdits al mes per cada membre de l'equip",
|
||||
"credits_per_tip_teams": "Reps 750 crèdits al mes per cada membre de l'equip",
|
||||
"view_and_manage_credits": "Visualitza i gestiona els crèdits per enviar missatges SMS",
|
||||
"view_and_manage_credits_description": "Visualitza i gestiona els crèdits per enviar missatges SMS. Un crèdit val 1¢ (USD). <0>Més informació</0>",
|
||||
"credit_worth_description": "Un crèdit val 1¢ (USD). <0>Més informació</0>",
|
||||
"buy_additional_credits": "Compra crèdits addicionals (0,01 $ per crèdit)",
|
||||
"view_additional_credits_expense_tip": "Pots veure la despesa de crèdits addicionals al teu registre de despeses",
|
||||
"overview": "Resum",
|
||||
"organization_slug_taken": "L'slug de l'organització ja està en ús",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "No podeu crear una organització perquè ja formeu part d'una",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "El teu equip de Cal.com {{teamName}} s'ha quedat sense crèdits. Com a resultat, els missatges SMS ara s'estan enviant per correu electrònic. Per reprendre l'enviament d'SMS, si us plau, compra crèdits addicionals.",
|
||||
"credit_limit_reached_message_user": "El teu compte de Cal.com s'ha quedat sense crèdits. Com a resultat, els missatges SMS ara s'envien per correu electrònic. Per reprendre l'enviament d'SMS, si us plau, compra crèdits addicionals.",
|
||||
"current_credit_balance": "Saldo actual: {{balance}} crèdits",
|
||||
"current_balance": "Saldo actual:",
|
||||
"notification_about_your_booking": "Notificació sobre la teva reserva",
|
||||
"monthly_credits": "Crèdits mensuals",
|
||||
"total_credits": "Total de crèdits: {{totalCredits}}",
|
||||
"remaining_credits": "Crèdits restants: {{remainingCredits}}",
|
||||
"remaining": "Restant",
|
||||
"total": "Total",
|
||||
"additional_credits": "Crèdits addicionals",
|
||||
"routing_form_next_in_queue": "{{count}} següent a la cua",
|
||||
"routing_form_select_members_to_email": "Enviar respostes per correu electrònic a",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "Veure fluxos de treball existents i les seves configuracions",
|
||||
"pbac_desc_update_workflows": "Editar i modificar la configuració dels fluxos de treball",
|
||||
"pbac_desc_delete_workflows": "Eliminar fluxos de treball del sistema",
|
||||
"pbac_resource_webhook": "Webhook",
|
||||
"pbac_desc_create_webhooks": "Crea webhooks",
|
||||
"pbac_desc_view_webhooks": "Visualitza webhooks",
|
||||
"pbac_desc_update_webhooks": "Actualitza els webhooks",
|
||||
"pbac_desc_delete_webhooks": "Elimina els webhooks",
|
||||
"pbac_desc_manage_workflows": "Accés complet de gestió a tots els fluxos de treball",
|
||||
"pbac_desc_create_event_types": "Crear tipus d'esdeveniments",
|
||||
"pbac_desc_view_event_types": "Veure tipus d'esdeveniments",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "El nom de l'esdeveniment desencadenant (p. ex., BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "L'hora del webhook",
|
||||
"webhook_type": "L'slug del tipus d'esdeveniment",
|
||||
"set_up_agent": "Configura l'agent",
|
||||
"webhook_title": "El nom del tipus d'esdeveniment",
|
||||
"webhook_start_time": "L'hora d'inici de l'esdeveniment",
|
||||
"webhook_end_time": "L'hora de finalització de l'esdeveniment",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "Visita",
|
||||
"location_custom_label_input_label": "Etiqueta personalitzada a la pàgina de reserva",
|
||||
"meeting_link": "Enllaç de la reunió",
|
||||
"session_outcome": "Resultat de la sessió",
|
||||
"call_created": "Trucada creada",
|
||||
"voicemail": "Bústia de veu",
|
||||
"my_bookings": "Les meves reserves",
|
||||
"phone": "Telèfon",
|
||||
"free": "Gratuït",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "Nom d'usuari",
|
||||
"expand_panel": "Expandeix el panell",
|
||||
"collapse_panel": "Contrau el panell",
|
||||
"email_verification_required": "Es requereix verificació de correu electrònic per a aquest tipus d'esdeveniment",
|
||||
"invalid_verification_code": "El codi de verificació proporcionat no és vàlid",
|
||||
"you_have_one_team": "Tens un equip",
|
||||
"consider_consolidating_one_team_org": "Considera configurar una organització per unificar la facturació, les eines d'administració i les analítiques del teu equip.",
|
||||
"consider_consolidating_multi_team_org": "Considera configurar una organització per unificar la facturació, les eines d'administració i les analítiques dels teus equips.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "Abans de l'hora d'inici programada",
|
||||
"cancel_booking_acknowledge_no_show_fee": "Reconec que en cancel·lar la reserva dins dels {{timeValue}} {{timeUnit}} abans de l'hora d'inici se'm cobrarà la tarifa per no assistència de {{amount, currency}}",
|
||||
"contact_organizer": "Si teniu alguna pregunta, poseu-vos en contacte amb l'organitzador.",
|
||||
"booking_time_option": "Hora de reserva",
|
||||
"booking_time_option_description": "Quan es programa la reserva (inici a fi)",
|
||||
"created_at_option": "Creat el",
|
||||
"created_at_option_description": "Quan es va crear originalment la reserva",
|
||||
"call_details": "Detalls de la trucada",
|
||||
"call_id": "ID de trucada",
|
||||
"call_information": "Informació de la trucada",
|
||||
"sentiment": "Sentiment",
|
||||
"disconnect_reason": "Motiu de desconnexió",
|
||||
"call_summary": "Resum de la trucada",
|
||||
"transcription": "Transcripció",
|
||||
"event_details": "Detalls de l'esdeveniment",
|
||||
"agent": "Agent",
|
||||
"no_transcript_available": "No hi ha cap transcripció disponible",
|
||||
"testing_sms_workflow_info_message": "Quan proveu aquest flux de treball, tingueu en compte que els SMS s'han de programar amb almenys 15 minuts d'antelació",
|
||||
"start_from_scratch_title": "Comença des de zero",
|
||||
"start_from_scratch_description": "Crea el teu propi flux de treball des de zero.",
|
||||
"cal_ai_template_title": "Plantilla Cal.ai",
|
||||
"cal_ai_template_description": "Agents d'IA que programen reunions, envien recordatoris i fan seguiment!",
|
||||
"voice": "Veu",
|
||||
"select_voice": "Selecciona la veu",
|
||||
"select_voice_for_agent": "Selecciona una veu per al teu agent",
|
||||
"choose_a_voice_for_your_agent": "Tria una veu per al teu agent",
|
||||
"trait": "Característica",
|
||||
"voice_id": "ID de veu",
|
||||
"use_voice": "Utilitza la veu",
|
||||
"current_voice": "Veu actual",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "Ověřte svoji e-mailovou adresu, abyste zaručili co nejlepší doručitelnost e-mailů a kalendáře",
|
||||
"verify_email_email_header": "Ověřte svoji e-mailovou adresu",
|
||||
"verify_email_button": "Ověřit e-mail",
|
||||
"cal_ai_assistant": "asistent",
|
||||
"cal_ai_assistant": "Asistent",
|
||||
"send_cal_video_transcription_emails": "Posílat e-maily s přepisem Cal videa",
|
||||
"description_send_cal_video_transcription_emails": "Posílat e-maily s přepisem Cal videa po skončení schůzky. (Vyžaduje placený tarif)",
|
||||
"verify_email_change_description": "Nedávno jste požádali o změnu e-mailové adresy, kterou používáte k přihlášení do svého účtu {{appName}}. Klikněte prosím na tlačítko níže, abyste potvrdili svou novou e-mailovou adresu.",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "Validace workflow selhala",
|
||||
"workflow_validation_empty_fields": "Jeden nebo více kroků workflow má prázdný obsah zprávy",
|
||||
"workflow_validation_unverified_contacts": "Jedno nebo více telefonních čísel nebo e-mailových adres není ověřeno",
|
||||
"supercharge_your_workflows_with_cal_ai": "Posilte své pracovní postupy s Cal.ai",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "Realistické AI agenty, které plánují schůzky, posílají připomínky a komunikují s vašimi zákazníky.",
|
||||
"phone_number_imported_successfully": "Telefonní číslo bylo úspěšně importováno a propojeno s agentem",
|
||||
"phone_number_deleted_successfully": "Telefonní číslo bylo úspěšně smazáno",
|
||||
"delete_phone_number_confirmation": "Opravdu chcete smazat toto telefonní číslo? Tuto akci nelze vrátit zpět.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "Předplatné telefonního čísla bylo úspěšně zrušeno",
|
||||
"updating": "Aktualizuji",
|
||||
"round_robin": "Plánování Round Robin",
|
||||
"hi_how_are_you_doing": "Ahoj, jak se máte?",
|
||||
"round_robin_description": "Schůzky v řadě mezi několika členy týmu.",
|
||||
"managed_event": "Spravovaná událost",
|
||||
"username_placeholder": "uživatelské jméno",
|
||||
@@ -876,9 +879,9 @@
|
||||
"are_you_sure_you_want_to_delete_workflow_step": "Opravdu chcete odstranit tento krok pracovního postupu?",
|
||||
"do_you_still_want_to_unsubscribe": "Opravdu chcete odhlásit telefonní číslo od tohoto agenta?",
|
||||
"the_action_will_disconnect_phone_number": "Tato akce odpojí telefonní číslo od agenta. Agent nebude moci uskutečňovat hovory, dokud nebude připojeno nové telefonní číslo.",
|
||||
"cal_ai_phone_numbers": "Telefonní čísla Cal AI",
|
||||
"cal_ai_phone_numbers": "Telefonní čísla",
|
||||
"connect_phone_number": "Připojit telefonní číslo",
|
||||
"cal_ai_phone_numbers_description": "Spravujte svá telefonní čísla Cal AI",
|
||||
"cal_ai_phone_numbers_description": "Spravujte svá telefonní čísla",
|
||||
"import_number": "Importovat číslo",
|
||||
"this_action_will_also": "Tato akce také:",
|
||||
"import_phone_number": "Importovat telefonní číslo",
|
||||
@@ -886,7 +889,7 @@
|
||||
"cancel_your_phone_number_subscription": "Zrušit předplatné telefonního čísla",
|
||||
"delete_associated_phone_number": "Odstranit přidružené telefonní číslo",
|
||||
"unauthorized_create_workflow": "Nemáte oprávnění vytvořit tento pracovní postup",
|
||||
"import_phone_number_description": "Importujte své telefonní číslo Twilio pro použití s Phone",
|
||||
"import_phone_number_description": "Importujte své telefonní číslo z Twilio pro použití s telefonem",
|
||||
"phone_number_cost": "${{price}}/měsíc",
|
||||
"buy_new_number": "Koupit nové číslo",
|
||||
"buy_number_cost_x_per_month": "Nákup telefonního čísla stojí ${{priceInDollars}} měsíčně. Za každé aktivní telefonní číslo vám bude účtován měsíční poplatek.",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "Ano, zrušit předplatné",
|
||||
"cancel_phone_number_subscription_confirmation": "Opravdu chcete zrušit toto předplatné telefonního čísla? Tuto akci nelze vrátit zpět a ztratíte přístup k tomuto telefonnímu číslu.",
|
||||
"add_members": "Přidat členy...",
|
||||
"add_members_no_ellipsis": "Přidat členy",
|
||||
"no_assigned_members": "Žádní přiřazení členové",
|
||||
"assigned_to": "Přiřazeno k",
|
||||
"you_must_be_logged_in_to": "Musíte být přihlášeni k {{url}}",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "Kategorie",
|
||||
"pricing": "Ceník",
|
||||
"learn_more": "Zjistit více",
|
||||
"try_now": "Vyzkoušet nyní",
|
||||
"privacy_policy": "Zásady ochrany soukromí",
|
||||
"terms_of_service": "Podmínky používání",
|
||||
"remove": "Odebrat",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "odeslat Whatsapp účastníkovi",
|
||||
"workflows": "Pracovní postupy",
|
||||
"new_workflow_btn": "Nový pracovní postup",
|
||||
"how_would_you_like_to_start": "Jak chcete začít?",
|
||||
"add_new_workflow": "Přidat nový pracovní postup",
|
||||
"reschedule_event_trigger": "pokud je událost přeložena na jiný termín",
|
||||
"trigger": "Spouštěč",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "Délka události",
|
||||
"event_time_info": "Čas začátku události",
|
||||
"event_type_not_found": "Typ události nebyl nalezen",
|
||||
"number_to_call_variable": "Číslo k zavolání",
|
||||
"number_to_call_info": "Telefonní číslo uživatele, kterému voláte",
|
||||
"location_variable": "Místo",
|
||||
"location_info": "Místo události",
|
||||
"additional_notes_variable": "Doplňující poznámky",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "Adresa URL týmu",
|
||||
"team_members": "Členové týmu",
|
||||
"more": "Více",
|
||||
"cal_ai_workflows": "Cal.ai pracovní postupy",
|
||||
"and_count_more": "a ještě {{count}}",
|
||||
"more_page_footer": "Mobilní aplikaci považujeme za rozšíření webové aplikace. Pokud provádíte složitější akce, proveďte je prosím ve webové aplikaci.",
|
||||
"workflow_example_1": "Odeslat účastníkovi SMS upomínku 24 hodin před začátkem akce",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "Odeslat účastníkovi upomínku 1 hodinu před začátkem akce",
|
||||
"workflow_example_5": "Odeslat hostiteli vlastní e-mail, pokud je událost přeložena na jiný termín",
|
||||
"workflow_example_6": "Odeslat hostiteli vlastní SMS, pokud je rezervována nová událost",
|
||||
"send_sms_reminder": "Poslat SMS připomínku",
|
||||
"send_sms_reminder_description": "24 hodin před začátkem události",
|
||||
"follow_up_with_no_shows": "Následná komunikace s nepřítomnými",
|
||||
"follow_up_with_no_shows_description": "30 minut po skončení události",
|
||||
"remind_attendees_to_bring_id": "Připomenout účastníkům, aby si vzali průkaz totožnosti",
|
||||
"remind_attendees_to_bring_id_description": "1 den před začátkem události",
|
||||
"email_to_remind_booking": "E-mailová připomínka",
|
||||
"email_to_remind_booking_description": "1 hodinu před začátkem události",
|
||||
"custom_sms_reminder": "Vlastní SMS připomínka",
|
||||
"custom_sms_reminder_description": "Když je událost naplánována",
|
||||
"custom_email_reminder": "Vlastní e-mailová připomínka",
|
||||
"custom_email_reminder_description": "Událost je přeplánována pro hostitele",
|
||||
"count_managed_to_limit": "Zahrnout počty rezervací ze spravovaných typů událostí",
|
||||
"welcome_to_cal_header": "Vítá vás {{appName}}!",
|
||||
"edit_form_later_subtitle": "Toto budete moci později upravit.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "Zobrazit na stránce rezervace",
|
||||
"visit_cancelled_booking": "Můžete navštívit stránku zrušené rezervace",
|
||||
"get_started_zapier_templates": "Začněte používat šablony Zapier",
|
||||
"standard_templates": "Standardní šablony",
|
||||
"cal_ai_templates": "Cal.ai šablony",
|
||||
"team_is_unpublished": "Tým {{team}} není zveřejněn",
|
||||
"org_is_unpublished_description": "Tento odkaz organizace není v současné době k dispozici. Kontaktujte prosím vlastníka organizace nebo ho požádejte o jeho zveřejnění.",
|
||||
"team_is_unpublished_description": "Tento odkaz subjektu ({{entity}}) není v současné době k dispozici. Kontaktujte prosím vlastníka subjektu ({{entity}}) nebo ho požádejte o jeho zveřejnění.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "Nepodařilo se strhnout platbu z karty.",
|
||||
"insights": "Přehledy",
|
||||
"routing_forms": "Směrovací formuláře",
|
||||
"testing_workflow_info_message": "Při testování tohoto pracovního postupu mějte na paměti, že e-maily a SMS lze naplánovat pouze nejméně 1 hodinu předem.",
|
||||
"insights_no_data_found_for_filter": "Pro vybraný filtr nebo vybraná data nebyla nalezena žádná data.",
|
||||
"acknowledge_booking_no_show_fee": "Beru na vědomí, že pokud se této události nezúčastním, bude z mé karty stržen poplatek za nedostavení se ve výši {{amount, currency}}.",
|
||||
"days": "dny",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "Tým: {{teamName}}",
|
||||
"insights_user_filter": "Uživatel: {{userName}}",
|
||||
"insights_subtitle": "Zobrazte si Insight rezervací napříč vašimi událostmi",
|
||||
"call_history": "Historie hovorů",
|
||||
"call_history_subtitle": "Zobrazte historii hovorů napříč vašimi Cal.ai hovory",
|
||||
"location_options": "Možná místa konání: {{locationCount}}",
|
||||
"channel_type": "Typ kanálu",
|
||||
"end_reason": "Důvod ukončení",
|
||||
"session_status": "Stav relace",
|
||||
"user_sentiment": "Sentiment uživatele",
|
||||
"time_header": "Čas",
|
||||
"from_header": "Od",
|
||||
"custom_plan": "Vlastní plán",
|
||||
"email_embed": "Vložení do e-mailu",
|
||||
"add_times_to_your_email": "Vyberte několik dostupných časů a vložte je do e-mailu",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "Účet je již propojen",
|
||||
"send_email": "Odeslat e-mail",
|
||||
"cal_ai_phone_call_action": "Zavolat účastníkovi pomocí hlasového agenta Cal.ai",
|
||||
"call_to_confirm_booking": "Hovor pro potvrzení rezervace",
|
||||
"cal_ai_phone_call_action_description": "2 hodiny před začátkem události",
|
||||
"cal_ai_agent_configuration": "Konfigurace agenta Cal.ai",
|
||||
"choose_at_least_one_event_type_test_call": "Vyberte prosím alespoň jeden typ události pro testovací hovor.",
|
||||
"mark_as_no_show": "Označit jako nepřítomný",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "Ověřit změnu e-mailu",
|
||||
"buy_credits": "Koupit kredity",
|
||||
"credits": "Kredity",
|
||||
"view_and_manage_credits": "Zobrazit a spravovat kredity",
|
||||
"credits_used": "Použité kredity",
|
||||
"total_credits_remaining": "Celkem zbývá",
|
||||
"credits_per_tip_org": "Získáváte 1000 kreditů měsíčně na každého člena týmu",
|
||||
"credits_per_tip_teams": "Získáváte 750 kreditů měsíčně na každého člena týmu",
|
||||
"view_and_manage_credits": "Zobrazit a spravovat kredity pro odesílání SMS zpráv",
|
||||
"view_and_manage_credits_description": "Zobrazte a spravujte kredity pro odesílání SMS zpráv. Jeden kredit má hodnotu 1¢ (USD). <0>Zjistit více</0>",
|
||||
"credit_worth_description": "Jeden kredit má hodnotu 1¢ (USD). <0>Zjistit více</0>",
|
||||
"buy_additional_credits": "Koupit další kredity (0,01 $ za kredit)",
|
||||
"view_additional_credits_expense_tip": "Dodatečné čerpání kreditů můžete zobrazit v protokolu výdajů",
|
||||
"overview": "Přehled",
|
||||
"organization_slug_taken": "Slug organizace je již obsazen",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Nemůžete vytvořit organizaci, protože již jste součástí jiné organizace",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "Vašemu týmu Cal.com {{teamName}} došly kredity. V důsledku toho jsou nyní SMS zprávy odesílány prostřednictvím e-mailu. Pro obnovení odesílání SMS prosím zakupte další kredity.",
|
||||
"credit_limit_reached_message_user": "Vašemu účtu Cal.com došly kredity. V důsledku toho jsou nyní SMS zprávy zasílány e-mailem. Pro obnovení zasílání SMS si prosím zakupte další kredity.",
|
||||
"current_credit_balance": "Aktuální zůstatek: {{balance}} kreditů",
|
||||
"current_balance": "Aktuální zůstatek:",
|
||||
"notification_about_your_booking": "Oznámení o vaší rezervaci",
|
||||
"monthly_credits": "Měsíční kredity",
|
||||
"total_credits": "Celkem kreditů: {{totalCredits}}",
|
||||
"remaining_credits": "Zbývající kredity: {{remainingCredits}}",
|
||||
"remaining": "Zbývá",
|
||||
"total": "Celkem",
|
||||
"additional_credits": "Dodatečné kredity",
|
||||
"routing_form_next_in_queue": "{{count}} další ve frontě",
|
||||
"routing_form_select_members_to_email": "Odeslat e-mailové odpovědi na",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "Zobrazit existující pracovní postupy a jejich konfigurace",
|
||||
"pbac_desc_update_workflows": "Upravovat a měnit nastavení pracovních postupů",
|
||||
"pbac_desc_delete_workflows": "Odstraňovat pracovní postupy ze systému",
|
||||
"pbac_resource_webhook": "Webhook",
|
||||
"pbac_desc_create_webhooks": "Vytvářet webhooky",
|
||||
"pbac_desc_view_webhooks": "Zobrazit webhooky",
|
||||
"pbac_desc_update_webhooks": "Aktualizovat webhooky",
|
||||
"pbac_desc_delete_webhooks": "Smazat webhooky",
|
||||
"pbac_desc_manage_workflows": "Plný přístup ke správě všech pracovních postupů",
|
||||
"pbac_desc_create_event_types": "Vytvářet typy událostí",
|
||||
"pbac_desc_view_event_types": "Zobrazit typy událostí",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "Název spouštěcí události (např. BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "Čas vytvoření webhooku",
|
||||
"webhook_type": "Slug typu události",
|
||||
"set_up_agent": "Nastavit agenta",
|
||||
"webhook_title": "Název typu události",
|
||||
"webhook_start_time": "Čas začátku události",
|
||||
"webhook_end_time": "Čas ukončení události",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "Navštívit",
|
||||
"location_custom_label_input_label": "Vlastní popisek na stránce rezervace",
|
||||
"meeting_link": "Odkaz na schůzku",
|
||||
"session_outcome": "Výsledek relace",
|
||||
"call_created": "Hovor vytvořen",
|
||||
"voicemail": "Hlasová schránka",
|
||||
"my_bookings": "Moje rezervace",
|
||||
"phone": "Telefon",
|
||||
"free": "Zdarma",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "Jméno uživatele",
|
||||
"expand_panel": "Rozbalit panel",
|
||||
"collapse_panel": "Sbalit panel",
|
||||
"email_verification_required": "Pro tento typ události je vyžadováno ověření e-mailu",
|
||||
"invalid_verification_code": "Byl poskytnut neplatný ověřovací kód",
|
||||
"you_have_one_team": "Máte jeden tým",
|
||||
"consider_consolidating_one_team_org": "Zvažte založení organizace pro sjednocení fakturace, administrativních nástrojů a analytiky napříč vaším týmem.",
|
||||
"consider_consolidating_multi_team_org": "Zvažte založení organizace pro sjednocení fakturace, administrativních nástrojů a analytiky napříč vašimi týmy.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "Před plánovaným časem zahájení",
|
||||
"cancel_booking_acknowledge_no_show_fee": "Beru na vědomí, že zrušením rezervace v době kratší než {{timeValue}} {{timeUnit}} před začátkem mi bude účtován poplatek za nedostavení se ve výši {{amount, currency}}",
|
||||
"contact_organizer": "Pokud máte jakékoli dotazy, kontaktujte prosím organizátora.",
|
||||
"booking_time_option": "Čas rezervace",
|
||||
"booking_time_option_description": "Kdy je rezervace naplánována (od začátku do konce)",
|
||||
"created_at_option": "Vytvořeno",
|
||||
"created_at_option_description": "Kdy byla rezervace původně vytvořena",
|
||||
"call_details": "Podrobnosti hovoru",
|
||||
"call_id": "ID hovoru",
|
||||
"call_information": "Informace o hovoru",
|
||||
"sentiment": "Sentiment",
|
||||
"disconnect_reason": "Důvod odpojení",
|
||||
"call_summary": "Shrnutí hovoru",
|
||||
"transcription": "Transkripce",
|
||||
"event_details": "Podrobnosti události",
|
||||
"agent": "Agent",
|
||||
"no_transcript_available": "Žádný přepis není k dispozici",
|
||||
"testing_sms_workflow_info_message": "Při testování tohoto workflow mějte na paměti, že SMS musí být naplánovány nejméně 15 minut předem",
|
||||
"start_from_scratch_title": "Začít od nuly",
|
||||
"start_from_scratch_description": "Vytvořte si vlastní workflow od začátku.",
|
||||
"cal_ai_template_title": "Šablona Cal.ai",
|
||||
"cal_ai_template_description": "AI agenti, kteří rezervují schůzky, posílají připomenutí a následně komunikují!",
|
||||
"voice": "Hlas",
|
||||
"select_voice": "Vybrat hlas",
|
||||
"select_voice_for_agent": "Vyberte hlas pro vašeho agenta",
|
||||
"choose_a_voice_for_your_agent": "Zvolte hlas pro vašeho agenta",
|
||||
"trait": "Vlastnost",
|
||||
"voice_id": "ID hlasu",
|
||||
"use_voice": "Použít hlas",
|
||||
"current_voice": "Aktuální hlas",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "Workflow-validering mislykkedes",
|
||||
"workflow_validation_empty_fields": "Et eller flere workflow-trin har tomt meddelelsesindhold",
|
||||
"workflow_validation_unverified_contacts": "Et eller flere telefonnumre eller e-mailadresser er ikke verificeret",
|
||||
"supercharge_your_workflows_with_cal_ai": "Boost dine workflows med Cal.ai",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "Livagtige AI-agenter, der booker møder, sender påmindelser og følger op med dine kunder.",
|
||||
"phone_number_imported_successfully": "Telefonnummer importeret og knyttet til agenten med succes",
|
||||
"phone_number_deleted_successfully": "Telefonnummer slettet med succes",
|
||||
"delete_phone_number_confirmation": "Er du sikker på, at du vil slette dette telefonnummer? Denne handling kan ikke fortrydes.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "Telefonnummerabonnement annulleret med succes",
|
||||
"updating": "Opdaterer",
|
||||
"round_robin": "Round Robin",
|
||||
"hi_how_are_you_doing": "Hej, hvordan har du det?",
|
||||
"round_robin_description": "Cyklusmøder mellem flere teammedlemmer.",
|
||||
"managed_event": "Administreret begivenhed",
|
||||
"username_placeholder": "brugernavn",
|
||||
@@ -876,9 +879,9 @@
|
||||
"are_you_sure_you_want_to_delete_workflow_step": "Er du sikker på, at du vil slette dette workflow-trin?",
|
||||
"do_you_still_want_to_unsubscribe": "Vil du stadig afmelde telefonnummeret fra denne agent?",
|
||||
"the_action_will_disconnect_phone_number": "Denne handling vil afbryde telefonnummeret fra agenten. Agenten vil ikke kunne foretage opkald, før et nyt telefonnummer er forbundet.",
|
||||
"cal_ai_phone_numbers": "Cal AI-telefonnumre",
|
||||
"cal_ai_phone_numbers": "Telefonnumre",
|
||||
"connect_phone_number": "Forbind telefonnummer",
|
||||
"cal_ai_phone_numbers_description": "Administrer dine Cal AI-telefonnumre",
|
||||
"cal_ai_phone_numbers_description": "Administrer dine telefonnumre",
|
||||
"import_number": "Importer nummer",
|
||||
"this_action_will_also": "Denne handling vil også:",
|
||||
"import_phone_number": "Importer telefonnummer",
|
||||
@@ -886,7 +889,7 @@
|
||||
"cancel_your_phone_number_subscription": "Annuller dit telefonnummerabonnement",
|
||||
"delete_associated_phone_number": "Slet det tilknyttede telefonnummer",
|
||||
"unauthorized_create_workflow": "Du har ikke tilladelse til at oprette dette workflow",
|
||||
"import_phone_number_description": "Importer dit Twilio-telefonnummer til brug med Cal AI-telefon",
|
||||
"import_phone_number_description": "Importer dit Twilio-telefonnummer til brug med Phone",
|
||||
"phone_number_cost": "${{price}}/måned",
|
||||
"buy_new_number": "Køb nyt nummer",
|
||||
"buy_number_cost_x_per_month": "At købe et telefonnummer koster ${{priceInDollars}} om måneden. Du vil blive opkrævet månedligt for hvert aktive telefonnummer.",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "Ja, annuller abonnement",
|
||||
"cancel_phone_number_subscription_confirmation": "Er du sikker på, at du vil annullere dette telefonnummerabonnement? Denne handling kan ikke fortrydes, og du vil miste adgangen til dette telefonnummer.",
|
||||
"add_members": "Tilføj medlemmer...",
|
||||
"add_members_no_ellipsis": "Tilføj medlemmer",
|
||||
"no_assigned_members": "Ingen tildelte medlemmer",
|
||||
"assigned_to": "Tildelt til",
|
||||
"you_must_be_logged_in_to": "Du skal være logget ind på {{url}}",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "Kategorier",
|
||||
"pricing": "Priser",
|
||||
"learn_more": "Få flere oplysninger",
|
||||
"try_now": "Prøv nu",
|
||||
"privacy_policy": "Privatlivspolitik",
|
||||
"terms_of_service": "Vilkår og betingelser",
|
||||
"remove": "Fjern",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "send WhatsApp-besked til deltager",
|
||||
"workflows": "Workflows",
|
||||
"new_workflow_btn": "Nyt Workflow",
|
||||
"how_would_you_like_to_start": "Hvordan vil du gerne starte?",
|
||||
"add_new_workflow": "Tilføj et nyt workflow",
|
||||
"reschedule_event_trigger": "når begivenheden bliver omlagt",
|
||||
"trigger": "Trigger",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "Begivenhedens varighed",
|
||||
"event_time_info": "Starttidspunkt for begivenheden",
|
||||
"event_type_not_found": "Begivenhedstype ikke fundet",
|
||||
"number_to_call_variable": "Nummer at ringe til",
|
||||
"number_to_call_info": "Telefonnummeret på den bruger, du ringer til",
|
||||
"location_variable": "Placering",
|
||||
"location_info": "Begivenhedens placering",
|
||||
"additional_notes_variable": "Yderligere bemærkninger",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "Team URL",
|
||||
"team_members": "Teammedlemmer",
|
||||
"more": "Flere",
|
||||
"cal_ai_workflows": "Cal.ai Workflows",
|
||||
"and_count_more": "og {{count}} mere",
|
||||
"more_page_footer": "Vi ser mobilapplikationen som en udvidelse af webapplikationen. Hvis du udfører komplicerede handlinger, henvises tilbage til webapplikationen.",
|
||||
"workflow_example_1": "Send sms-påmindelse til deltageren 24 timer før begivenheden starter",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "Send e-mail påmindelse til deltager 1 time før begivenheder starter",
|
||||
"workflow_example_5": "Send brugerdefineret e-mail til vært når begivenheden omlægges",
|
||||
"workflow_example_6": "Send brugerdefineret SMS til vært når ny begivenhed bookes",
|
||||
"send_sms_reminder": "Send SMS-påmindelse",
|
||||
"send_sms_reminder_description": "24 timer før begivenheden starter",
|
||||
"follow_up_with_no_shows": "Følg op med udeblivelser",
|
||||
"follow_up_with_no_shows_description": "30 minutter efter begivenheden slutter",
|
||||
"remind_attendees_to_bring_id": "Mind deltagere om at medbringe ID",
|
||||
"remind_attendees_to_bring_id_description": "1 dag før begivenheden starter",
|
||||
"email_to_remind_booking": "E-mail-påmindelse",
|
||||
"email_to_remind_booking_description": "1 time før begivenheden starter",
|
||||
"custom_sms_reminder": "Brugerdefineret SMS-påmindelse",
|
||||
"custom_sms_reminder_description": "Når begivenheden er planlagt",
|
||||
"custom_email_reminder": "Brugerdefineret e-mail-påmindelse",
|
||||
"custom_email_reminder_description": "Begivenheden er omlagt til værten",
|
||||
"count_managed_to_limit": "Inkluder bookingantal fra administrerede begivenhedstyper",
|
||||
"welcome_to_cal_header": "Velkommen til {{appName}}!",
|
||||
"edit_form_later_subtitle": "Du vil kunne redigere dette senere.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "Vis på bookingsiden",
|
||||
"visit_cancelled_booking": "Du kan besøge siden for den aflyste booking",
|
||||
"get_started_zapier_templates": "Kom i gang med Zapier skabeloner",
|
||||
"standard_templates": "Standard skabeloner",
|
||||
"cal_ai_templates": "Cal.ai-skabeloner",
|
||||
"team_is_unpublished": "{{team}} er upubliceret",
|
||||
"org_is_unpublished_description": "Dette organisationslink er i øjeblikket ikke tilgængeligt. Kontakt venligst organisationsindehaveren eller bed dem om at offentliggøre det.",
|
||||
"team_is_unpublished_description": "Dette teamlink er i øjeblikket ikke tilgængeligt. Kontakt venligst teamindehaveren eller bed dem om at offentliggøre det.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "Kunne ikke trække betaling fra kortet.",
|
||||
"insights": "Indsigter",
|
||||
"routing_forms": "Ruteformularer",
|
||||
"testing_workflow_info_message": "Når du tester denne arbejdsgang, skal du være opmærksom på, at e-mails og SMS'er kun kan planlægges mindst 1 time i forvejen",
|
||||
"insights_no_data_found_for_filter": "Ingen data fundet for det valgte filter eller de valgte datoer.",
|
||||
"acknowledge_booking_no_show_fee": "Jeg anerkender, at hvis jeg ikke deltager i denne begivenhed, vil et gebyr for udeblivelse på {{amount, currency}} blive opkrævet på mit kort.",
|
||||
"days": "dage",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "Team: {{teamName}}",
|
||||
"insights_user_filter": "Bruger: {{userName}}",
|
||||
"insights_subtitle": "Se bookingindsigter på tværs af dine begivenheder",
|
||||
"call_history": "Opkaldshistorik",
|
||||
"call_history_subtitle": "Se opkaldshistorik for dine Cal.ai-opkald",
|
||||
"location_options": "{{locationCount}} placeringsmuligheder",
|
||||
"channel_type": "Kanaltype",
|
||||
"end_reason": "Afslutningsårsag",
|
||||
"session_status": "Sessionsstatus",
|
||||
"user_sentiment": "Brugerens holdning",
|
||||
"time_header": "Tid",
|
||||
"from_header": "Fra",
|
||||
"custom_plan": "Tilpasset plan",
|
||||
"email_embed": "E-mail indlejring",
|
||||
"add_times_to_your_email": "Vælg nogle tilgængelige tidspunkter og indlejre dem i din e-mail",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "Kontoen er allerede forbundet",
|
||||
"send_email": "Send e-mail",
|
||||
"cal_ai_phone_call_action": "Ring til deltager ved hjælp af Cal.ai Voice Agent",
|
||||
"call_to_confirm_booking": "Opkald for at bekræfte booking",
|
||||
"cal_ai_phone_call_action_description": "2 timer før begivenheden starter",
|
||||
"cal_ai_agent_configuration": "Cal.ai-agentkonfiguration",
|
||||
"choose_at_least_one_event_type_test_call": "Vælg venligst mindst én begivenhedstype for at foretage et testopkald.",
|
||||
"mark_as_no_show": "Markér som udeblevet",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "Bekræft ændring af e-mail",
|
||||
"buy_credits": "Køb credits",
|
||||
"credits": "Credits",
|
||||
"view_and_manage_credits": "Se og administrer credits",
|
||||
"credits_used": "Brugte credits",
|
||||
"total_credits_remaining": "Samlet tilbageværende",
|
||||
"credits_per_tip_org": "Du modtager 1000 credits om måneden pr. teammedlem",
|
||||
"credits_per_tip_teams": "Du modtager 750 credits om måneden pr. teammedlem",
|
||||
"view_and_manage_credits": "Se og administrer credits til at sende SMS-beskeder",
|
||||
"view_and_manage_credits_description": "Se og administrer credits til at sende SMS-beskeder. Én credit er 1¢ (USD). <0>Læs mere</0>",
|
||||
"credit_worth_description": "Én credit er 1¢ (USD) værd. <0>Læs mere</0>",
|
||||
"buy_additional_credits": "Køb ekstra credits ($0,01 pr. credit)",
|
||||
"view_additional_credits_expense_tip": "Du kan se yderligere kreditforbrug i din udgiftslog",
|
||||
"overview": "Oversigt",
|
||||
"organization_slug_taken": "Organisationens slug er allerede taget",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Du kan ikke oprette en organisation, da du allerede er en del af en organisation",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "Dit Cal.com-team {{teamName}} er løbet tør for credits. Som følge heraf bliver SMS-beskeder nu sendt via e-mail i stedet. For at genoptage afsendelse af SMS skal du købe flere credits.",
|
||||
"credit_limit_reached_message_user": "Din Cal.com-konto er løbet tør for credits. Som følge heraf bliver SMS-beskeder nu sendt via e-mail i stedet. For at genoptage afsendelse af SMS'er skal du købe flere credits.",
|
||||
"current_credit_balance": "Nuværende saldo: {{balance}} credits",
|
||||
"current_balance": "Nuværende saldo:",
|
||||
"notification_about_your_booking": "Notifikation om din booking",
|
||||
"monthly_credits": "Månedlige credits",
|
||||
"total_credits": "Samlede credits: {{totalCredits}}",
|
||||
"remaining_credits": "Resterende credits: {{remainingCredits}}",
|
||||
"remaining": "Tilbageværende",
|
||||
"total": "Samlet",
|
||||
"additional_credits": "Ekstra credits",
|
||||
"routing_form_next_in_queue": "{{count}} næste i køen",
|
||||
"routing_form_select_members_to_email": "Send e-mail-svar til",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "Se eksisterende workflows og deres konfigurationer",
|
||||
"pbac_desc_update_workflows": "Rediger og tilpas workflowindstillinger",
|
||||
"pbac_desc_delete_workflows": "Fjern workflows fra systemet",
|
||||
"pbac_resource_webhook": "Webhook",
|
||||
"pbac_desc_create_webhooks": "Opret webhooks",
|
||||
"pbac_desc_view_webhooks": "Se webhooks",
|
||||
"pbac_desc_update_webhooks": "Opdater webhooks",
|
||||
"pbac_desc_delete_webhooks": "Slet webhooks",
|
||||
"pbac_desc_manage_workflows": "Fuld administrationsadgang til alle workflows",
|
||||
"pbac_desc_create_event_types": "Opret begivenhedstyper",
|
||||
"pbac_desc_view_event_types": "Vis begivenhedstyper",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "Navnet på triggerbegivenheden (f.eks. BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "Tidspunktet for webhooken",
|
||||
"webhook_type": "Slug for begivenhedstypen",
|
||||
"set_up_agent": "Opsæt agent",
|
||||
"webhook_title": "Navnet på begivenhedstypen",
|
||||
"webhook_start_time": "Begivenhedens starttidspunkt",
|
||||
"webhook_end_time": "Begivenhedens sluttidspunkt",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "Besøg",
|
||||
"location_custom_label_input_label": "Brugerdefineret etiket på bookingsiden",
|
||||
"meeting_link": "Mødelink",
|
||||
"session_outcome": "Sessionens resultat",
|
||||
"call_created": "Opkald oprettet",
|
||||
"voicemail": "Telefonsvarer",
|
||||
"my_bookings": "Mine bookinger",
|
||||
"phone": "Telefon",
|
||||
"free": "Gratis",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "Brugers navn",
|
||||
"expand_panel": "Udvid panel",
|
||||
"collapse_panel": "Skjul panel",
|
||||
"email_verification_required": "Emailbekræftelse er påkrævet for denne type begivenhed",
|
||||
"invalid_verification_code": "Ugyldig bekræftelseskode angivet",
|
||||
"you_have_one_team": "Du har ét team",
|
||||
"consider_consolidating_one_team_org": "Overvej at oprette en organisation for at samle fakturering, adminværktøjer og analyser på tværs af dit team.",
|
||||
"consider_consolidating_multi_team_org": "Overvej at oprette en organisation for at samle fakturering, adminværktøjer og analyser på tværs af dine teams.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "Før den planlagte starttid",
|
||||
"cancel_booking_acknowledge_no_show_fee": "Jeg anerkender, at ved at aflyse bookingen inden for {{timeValue}} {{timeUnit}} før starttiden, vil jeg blive opkrævet gebyret for udeblivelse på {{amount, currency}}",
|
||||
"contact_organizer": "Hvis du har spørgsmål, bedes du kontakte arrangøren.",
|
||||
"booking_time_option": "Bookingtid",
|
||||
"booking_time_option_description": "Når bookingen er planlagt (start til slut)",
|
||||
"created_at_option": "Oprettet den",
|
||||
"created_at_option_description": "Når bookingen oprindeligt blev oprettet",
|
||||
"call_details": "Opkaldsdetaljer",
|
||||
"call_id": "Opkalds-ID",
|
||||
"call_information": "Opkaldsinformation",
|
||||
"sentiment": "Stemning",
|
||||
"disconnect_reason": "Afbrydelsesårsag",
|
||||
"call_summary": "Opkaldsoversigt",
|
||||
"transcription": "Transskription",
|
||||
"event_details": "Begivenhedsdetaljer",
|
||||
"agent": "Agent",
|
||||
"no_transcript_available": "Ingen transskription tilgængelig",
|
||||
"testing_sms_workflow_info_message": "Når du tester denne arbejdsgang, skal du være opmærksom på, at SMS'er skal planlægges mindst 15 minutter i forvejen",
|
||||
"start_from_scratch_title": "Start fra bunden",
|
||||
"start_from_scratch_description": "Opret din egen arbejdsgang fra bunden.",
|
||||
"cal_ai_template_title": "Cal.ai-skabelon",
|
||||
"cal_ai_template_description": "AI-agenter, der booker møder, sender påmindelser og følger op!",
|
||||
"voice": "Stemme",
|
||||
"select_voice": "Vælg stemme",
|
||||
"select_voice_for_agent": "Vælg en stemme til din agent",
|
||||
"choose_a_voice_for_your_agent": "Vælg en stemme til din agent",
|
||||
"trait": "Egenskab",
|
||||
"voice_id": "Stemme-ID",
|
||||
"use_voice": "Brug stemme",
|
||||
"current_voice": "Nuværende stemme",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die Zustellbarkeit von E-Mails und Kalenderbenachrichtigungen zu gewährleisten",
|
||||
"verify_email_email_header": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"verify_email_button": "E-Mail bestätigen",
|
||||
"cal_ai_assistant": "Cal KI-Assistent",
|
||||
"cal_ai_assistant": "Assistent",
|
||||
"send_cal_video_transcription_emails": "Cal Video-Transkriptions-E-Mails senden",
|
||||
"description_send_cal_video_transcription_emails": "E-Mails mit der Transkription des Cal Videos nach Beendigung des Meetings versenden. (Erfordert einen kostenpflichtigen Plan)",
|
||||
"verify_email_change_description": "Sie haben vor kurzem angefordert, die E-Mail-Adresse zu ändern, die Sie verwenden, um sich bei Ihrem {{appName}} Konto anzumelden. Bitte klicken Sie auf die Schaltfläche unten, um Ihre neue E-Mail-Adresse zu bestätigen.",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "Workflow-Validierung fehlgeschlagen",
|
||||
"workflow_validation_empty_fields": "Ein oder mehrere Workflow-Schritte haben leere Nachrichteninhalte",
|
||||
"workflow_validation_unverified_contacts": "Eine oder mehrere Telefonnummern oder E-Mail-Adressen sind nicht verifiziert",
|
||||
"supercharge_your_workflows_with_cal_ai": "Optimiere deine Arbeitsabläufe mit Cal.ai",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "Lebensechte KI-Agenten, die Meetings buchen, Erinnerungen senden und mit deinen Kunden Nachfassen.",
|
||||
"phone_number_imported_successfully": "Telefonnummer erfolgreich importiert und mit Agent verknüpft",
|
||||
"phone_number_deleted_successfully": "Telefonnummer erfolgreich gelöscht",
|
||||
"delete_phone_number_confirmation": "Sind Sie sicher, dass Sie diese Telefonnummer löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "Telefonnummer-Abonnement erfolgreich gekündigt",
|
||||
"updating": "Aktualisierung läuft",
|
||||
"round_robin": "Round Robin",
|
||||
"hi_how_are_you_doing": "Hallo, wie geht es dir?",
|
||||
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
|
||||
"managed_event": "Verwaltetes Ereignis",
|
||||
"username_placeholder": "Benutzername",
|
||||
@@ -878,7 +881,7 @@
|
||||
"the_action_will_disconnect_phone_number": "Diese Aktion wird die Telefonnummer vom Agent trennen. Der Agent kann keine Anrufe tätigen, bis eine neue Telefonnummer verbunden wird.",
|
||||
"cal_ai_phone_numbers": "Telefonnummern",
|
||||
"connect_phone_number": "Telefonnummer verbinden",
|
||||
"cal_ai_phone_numbers_description": "Verwalten Sie Ihre Telefonnummern",
|
||||
"cal_ai_phone_numbers_description": "Verwalte deine Telefonnummern",
|
||||
"import_number": "Nummer importieren",
|
||||
"this_action_will_also": "Diese Aktion wird auch:",
|
||||
"import_phone_number": "Telefonnummer importieren",
|
||||
@@ -886,7 +889,7 @@
|
||||
"cancel_your_phone_number_subscription": "Ihr Telefonnummer-Abonnement kündigen",
|
||||
"delete_associated_phone_number": "Die zugehörige Telefonnummer löschen",
|
||||
"unauthorized_create_workflow": "Sie sind nicht berechtigt, diesen Workflow zu erstellen",
|
||||
"import_phone_number_description": "Importieren Sie Ihre Twilio-Telefonnummer zur Verwendung mit Phone",
|
||||
"import_phone_number_description": "Importiere deine Twilio-Telefonnummer zur Verwendung mit Telefon",
|
||||
"phone_number_cost": "${{price}}/Monat",
|
||||
"buy_new_number": "Neue Nummer kaufen",
|
||||
"buy_number_cost_x_per_month": "Der Kauf einer Telefonnummer kostet ${{priceInDollars}} pro Monat. Ihnen wird monatlich für jede aktive Telefonnummer eine Gebühr berechnet.",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "Ja, Abonnement kündigen",
|
||||
"cancel_phone_number_subscription_confirmation": "Sind Sie sicher, dass Sie dieses Telefonnummer-Abonnement kündigen möchten? Diese Aktion kann nicht rückgängig gemacht werden und Sie verlieren den Zugriff auf diese Telefonnummer.",
|
||||
"add_members": "Mitglieder hinzufügen...",
|
||||
"add_members_no_ellipsis": "Mitglieder hinzufügen",
|
||||
"no_assigned_members": "Keine zugeordneten Mitglieder",
|
||||
"assigned_to": "Zugeordnet zu",
|
||||
"you_must_be_logged_in_to": "Sie müssen bei {{url}} angemeldet sein",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "Kategorien",
|
||||
"pricing": "Preise",
|
||||
"learn_more": "Mehr erfahren",
|
||||
"try_now": "Jetzt ausprobieren",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"terms_of_service": "Nutzungsbedingungen",
|
||||
"remove": "Entfernen",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "WhatsApp-Nachricht an Teilnehmer senden",
|
||||
"workflows": "Workflows",
|
||||
"new_workflow_btn": "Neuer Workflow",
|
||||
"how_would_you_like_to_start": "Wie möchtest du beginnen?",
|
||||
"add_new_workflow": "Einen neuen Workflow hinzufügen",
|
||||
"reschedule_event_trigger": "wenn das Ereignis neu geplant wird",
|
||||
"trigger": "Auslöser",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "Die Dauer des Ereignisses",
|
||||
"event_time_info": "Die Startzeit des Termins",
|
||||
"event_type_not_found": "Ereignistyp nicht gefunden",
|
||||
"number_to_call_variable": "Anzurufende Nummer",
|
||||
"number_to_call_info": "Die Telefonnummer des Benutzers, den du anrufst",
|
||||
"location_variable": "Ort",
|
||||
"location_info": "Der Ort des Events",
|
||||
"additional_notes_variable": "Zusätzliche Notizen",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "Team-URL",
|
||||
"team_members": "Teammitglieder",
|
||||
"more": "Mehr",
|
||||
"cal_ai_workflows": "Cal.ai Arbeitsabläufe",
|
||||
"and_count_more": "und {{count}} mehr",
|
||||
"more_page_footer": "Wir betrachten die mobile Anwendung als Erweiterung der Web-Anwendung. Wenn Sie komplizierte Aktionen durchführen, verwenden Sie bitte die Web-Anwendung.",
|
||||
"workflow_example_1": "Eine Erinnerung per Mail an die Teilnehmer verschicken, 24 Stunden bevor das Event startet",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "Eine Erinnerung per SMS an die Teilnehmer verschicken, 24 Stunden bevor das Event startet",
|
||||
"workflow_example_5": "Benutzerdefinierte E-Mail senden, wenn der Termin zum Moderieren neu geplant wird",
|
||||
"workflow_example_6": "Benutzerdefinierte SMS senden, wenn der Termin zum Moderieren gebucht wird",
|
||||
"send_sms_reminder": "SMS-Erinnerung senden",
|
||||
"send_sms_reminder_description": "24 Stunden vor Terminbeginn",
|
||||
"follow_up_with_no_shows": "Nachfassen bei Nichterscheinen",
|
||||
"follow_up_with_no_shows_description": "30 Minuten nach Terminende",
|
||||
"remind_attendees_to_bring_id": "Teilnehmer erinnern, Ausweis mitzubringen",
|
||||
"remind_attendees_to_bring_id_description": "1 Tag vor Terminbeginn",
|
||||
"email_to_remind_booking": "E-Mail-Erinnerung",
|
||||
"email_to_remind_booking_description": "1 Stunde vor Terminbeginn",
|
||||
"custom_sms_reminder": "Benutzerdefinierte SMS-Erinnerung",
|
||||
"custom_sms_reminder_description": "Wenn Termin geplant wird",
|
||||
"custom_email_reminder": "Benutzerdefinierte E-Mail-Erinnerung",
|
||||
"custom_email_reminder_description": "Termin wird für Gastgeber umgeplant",
|
||||
"count_managed_to_limit": "Buchungszahlen von verwalteten Ereignistypen einbeziehen",
|
||||
"welcome_to_cal_header": "Willkommen bei {{appName}}!",
|
||||
"edit_form_later_subtitle": "Du kannst dies später bearbeiten.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "Auf der Buchungsseite anzeigen",
|
||||
"visit_cancelled_booking": "Sie können die Seite der stornierten Buchung besuchen",
|
||||
"get_started_zapier_templates": "Legen Sie mit Zapier-Vorlagen los",
|
||||
"standard_templates": "Standardvorlagen",
|
||||
"cal_ai_templates": "Cal.ai-Vorlagen",
|
||||
"team_is_unpublished": "{{team}} ist unveröffentlicht",
|
||||
"org_is_unpublished_description": "Dieser Organisations-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den Organisations-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
|
||||
"team_is_unpublished_description": "Dieser {{entity}}-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den {{entity}}-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "Karte konnte nicht für die Zahlung belastet werden.",
|
||||
"insights": "Erkenntnisse",
|
||||
"routing_forms": "Weiterleitungsformulare",
|
||||
"testing_workflow_info_message": "Bitte beachten Sie beim Testen dieses Workflows, dass E-Mails und SMS spätestens 1 Stunde im Voraus geplant werden können",
|
||||
"insights_no_data_found_for_filter": "Keine Daten für den ausgewählten Filter oder das ausgewählte Datum gefunden.",
|
||||
"acknowledge_booking_no_show_fee": "Ich bestätige, dass meiner Karte eine Gebühr von {{amount, currency}} berechnet wird, sofern ich nicht an diesem Ereignis teilnehme.",
|
||||
"days": "Tage",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "Team: {{teamName}}",
|
||||
"insights_user_filter": "Benutzer: {{userName}}",
|
||||
"insights_subtitle": "Erfahren Sie mehr über Ihre Termine und Ihr Team",
|
||||
"call_history": "Anrufverlauf",
|
||||
"call_history_subtitle": "Anrufverlauf aller Cal.ai-Anrufe anzeigen",
|
||||
"location_options": "{{locationCount}} Veranstaltungsort-Optionen",
|
||||
"channel_type": "Kanaltyp",
|
||||
"end_reason": "Beendigungsgrund",
|
||||
"session_status": "Sitzungsstatus",
|
||||
"user_sentiment": "Nutzer-Stimmung",
|
||||
"time_header": "Zeit",
|
||||
"from_header": "Von",
|
||||
"custom_plan": "Maßgeschneiderter Tarif",
|
||||
"email_embed": "E-Mail Einbettung",
|
||||
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "Konto ist bereits verknüpft",
|
||||
"send_email": "E-Mail senden",
|
||||
"cal_ai_phone_call_action": "Teilnehmer mit Cal.ai Sprachassistent anrufen",
|
||||
"call_to_confirm_booking": "Anruf zur Buchungsbestätigung",
|
||||
"cal_ai_phone_call_action_description": "2 Std. vor Terminbeginn",
|
||||
"cal_ai_agent_configuration": "Cal.ai Agent-Konfiguration",
|
||||
"choose_at_least_one_event_type_test_call": "Bitte wählen Sie mindestens einen Ereignistyp aus, um einen Testanruf zu tätigen.",
|
||||
"mark_as_no_show": "Als No-Show markieren",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "E-Mail-Änderung bestätigen",
|
||||
"buy_credits": "Credits kaufen",
|
||||
"credits": "Credits",
|
||||
"view_and_manage_credits": "Credits anzeigen und verwalten",
|
||||
"credits_used": "Verbrauchte Credits",
|
||||
"total_credits_remaining": "Verbleibende Gesamtanzahl",
|
||||
"credits_per_tip_org": "Sie erhalten 1000 Credits pro Monat pro Teammitglied",
|
||||
"credits_per_tip_teams": "Sie erhalten 750 Credits pro Monat pro Teammitglied",
|
||||
"view_and_manage_credits": "Credits für den Versand von SMS-Nachrichten anzeigen und verwalten",
|
||||
"view_and_manage_credits_description": "Credits für das Senden von SMS-Nachrichten anzeigen und verwalten. Ein Credit entspricht 1¢ (USD). <0>Mehr erfahren</0>",
|
||||
"credit_worth_description": "Ein Credit entspricht 1¢ (USD). <0>Mehr erfahren</0>",
|
||||
"buy_additional_credits": "Zusätzliche Credits kaufen (0,01 $ pro Credit)",
|
||||
"view_additional_credits_expense_tip": "Zusätzliche Credit-Ausgaben können Sie in Ihrem Ausgabenprotokoll einsehen",
|
||||
"overview": "Übersicht",
|
||||
"organization_slug_taken": "Der Organisations-Slug ist bereits vergeben",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Sie können keine Organisation erstellen, da Sie bereits Teil einer Organisation sind",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "Ihrem Cal.com-Team {{teamName}} sind die Credits ausgegangen. Infolgedessen werden SMS-Nachrichten jetzt per E-Mail gesendet. Um wieder SMS zu senden, kaufen Sie bitte zusätzliche Credits.",
|
||||
"credit_limit_reached_message_user": "Dein Cal.com-Konto hat keine Credits mehr. Infolgedessen werden SMS-Nachrichten jetzt stattdessen per E-Mail gesendet. Um das Senden von SMS fortzusetzen, kaufe bitte zusätzliche Credits.",
|
||||
"current_credit_balance": "Aktuelles Guthaben: {{balance}} Credits",
|
||||
"current_balance": "Aktuelles Guthaben:",
|
||||
"notification_about_your_booking": "Benachrichtigung über deine Buchung",
|
||||
"monthly_credits": "Monatliche Credits",
|
||||
"total_credits": "Gesamte Credits: {{totalCredits}}",
|
||||
"remaining_credits": "Verbleibende Credits: {{remainingCredits}}",
|
||||
"remaining": "Verbleibend",
|
||||
"total": "Gesamt",
|
||||
"additional_credits": "Zusätzliche Credits",
|
||||
"routing_form_next_in_queue": "{{count}} nächste in der Warteschlange",
|
||||
"routing_form_select_members_to_email": "E-Mail-Antworten senden an",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "Bestehende Workflows und ihre Konfigurationen anzeigen",
|
||||
"pbac_desc_update_workflows": "Workflow-Einstellungen bearbeiten und ändern",
|
||||
"pbac_desc_delete_workflows": "Workflows aus dem System entfernen",
|
||||
"pbac_resource_webhook": "Webhook",
|
||||
"pbac_desc_create_webhooks": "Webhooks erstellen",
|
||||
"pbac_desc_view_webhooks": "Webhooks anzeigen",
|
||||
"pbac_desc_update_webhooks": "Webhooks aktualisieren",
|
||||
"pbac_desc_delete_webhooks": "Webhooks löschen",
|
||||
"pbac_desc_manage_workflows": "Vollständiger Verwaltungszugriff auf alle Workflows",
|
||||
"pbac_desc_create_event_types": "Ereignistypen erstellen",
|
||||
"pbac_desc_view_event_types": "Ereignistypen anzeigen",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "Der Name des Auslöseereignisses (z.B. BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "Der Zeitpunkt des Webhooks",
|
||||
"webhook_type": "Der Ereignistyp-Slug",
|
||||
"set_up_agent": "Agent einrichten",
|
||||
"webhook_title": "Der Name des Ereignistyps",
|
||||
"webhook_start_time": "Die Startzeit des Ereignisses",
|
||||
"webhook_end_time": "Die Endzeit des Ereignisses",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "Besuchen",
|
||||
"location_custom_label_input_label": "Benutzerdefinierte Bezeichnung auf der Buchungsseite",
|
||||
"meeting_link": "Meeting-Link",
|
||||
"session_outcome": "Sitzungsergebnis",
|
||||
"call_created": "Anruf erstellt",
|
||||
"voicemail": "Sprachnachricht",
|
||||
"my_bookings": "Meine Buchungen",
|
||||
"phone": "Telefon",
|
||||
"free": "Kostenlos",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "Benutzername",
|
||||
"expand_panel": "Panel erweitern",
|
||||
"collapse_panel": "Panel einklappen",
|
||||
"email_verification_required": "E-Mail-Verifizierung ist für diesen Ereignistyp erforderlich",
|
||||
"invalid_verification_code": "Ungültiger Verifizierungscode angegeben",
|
||||
"you_have_one_team": "Sie haben ein Team",
|
||||
"consider_consolidating_one_team_org": "Erwägen Sie die Einrichtung einer Organisation, um Abrechnung, Administrationstools und Analysen für Ihr Team zu vereinheitlichen.",
|
||||
"consider_consolidating_multi_team_org": "Erwägen Sie die Einrichtung einer Organisation, um Abrechnung, Administrationstools und Analysen für Ihre Teams zu vereinheitlichen.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "Vor dem geplanten Startzeitpunkt",
|
||||
"cancel_booking_acknowledge_no_show_fee": "Ich bestätige, dass mir bei einer Stornierung innerhalb von {{timeValue}} {{timeUnit}} vor dem Startzeitpunkt die Ausfallgebühr von {{amount, currency}} berechnet wird",
|
||||
"contact_organizer": "Bei Fragen wenden Sie sich bitte an den Organisator.",
|
||||
"booking_time_option": "Buchungszeit",
|
||||
"booking_time_option_description": "Wann die Buchung geplant ist (Anfang bis Ende)",
|
||||
"created_at_option": "Erstellt am",
|
||||
"created_at_option_description": "Wann die Buchung ursprünglich erstellt wurde",
|
||||
"call_details": "Anrufdetails",
|
||||
"call_id": "Anruf-ID",
|
||||
"call_information": "Anrufinformationen",
|
||||
"sentiment": "Stimmung",
|
||||
"disconnect_reason": "Trennungsgrund",
|
||||
"call_summary": "Anrufzusammenfassung",
|
||||
"transcription": "Transkription",
|
||||
"event_details": "Ereignisdetails",
|
||||
"agent": "Agent",
|
||||
"no_transcript_available": "Kein Transkript verfügbar",
|
||||
"testing_sms_workflow_info_message": "Beachten Sie beim Testen dieses Workflows, dass SMS mindestens 15 Minuten im Voraus geplant werden müssen",
|
||||
"start_from_scratch_title": "Von Grund auf neu beginnen",
|
||||
"start_from_scratch_description": "Erstellen Sie Ihren eigenen Workflow von Grund auf.",
|
||||
"cal_ai_template_title": "Cal.ai-Vorlage",
|
||||
"cal_ai_template_description": "KI-Agenten, die Meetings buchen, Erinnerungen senden und nachfassen!",
|
||||
"voice": "Stimme",
|
||||
"select_voice": "Stimme auswählen",
|
||||
"select_voice_for_agent": "Wähle eine Stimme für deinen Agenten",
|
||||
"choose_a_voice_for_your_agent": "Wähle eine Stimme für deinen Agenten",
|
||||
"trait": "Eigenschaft",
|
||||
"voice_id": "Stimm-ID",
|
||||
"use_voice": "Stimme verwenden",
|
||||
"current_voice": "Aktuelle Stimme",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"verify_email_banner_body": "Επαληθεύστε τη διεύθυνση email σας για να εξασφαλίσετε την καλύτερη δυνατή παράδοση email και ημερολογίου",
|
||||
"verify_email_email_header": "Επιβεβαιώστε τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας",
|
||||
"verify_email_button": "Επαλήθευση email",
|
||||
"cal_ai_assistant": "Βοηθός Cal AI",
|
||||
"cal_ai_assistant": "Βοηθός",
|
||||
"send_cal_video_transcription_emails": "Αποστολή email απομαγνητοφώνησης Cal Video",
|
||||
"description_send_cal_video_transcription_emails": "Αποστολή email με την απομαγνητοφώνηση του Cal Video μετά το τέλος της συνάντησης. (Απαιτείται συνδρομητικό πακέτο)",
|
||||
"verify_email_change_description": "Πρόσφατα ζητήσατε να αλλάξετε τη διεύθυνση email που χρησιμοποιείτε για να συνδεθείτε στον λογαριασμό σας στο {{appName}}. Παρακαλούμε κάντε κλικ στο παρακάτω κουμπί για να επιβεβαιώσετε τη νέα διεύθυνση email σας.",
|
||||
@@ -819,6 +819,8 @@
|
||||
"workflow_validation_failed": "Η επικύρωση της ροής εργασίας απέτυχε",
|
||||
"workflow_validation_empty_fields": "Ένα ή περισσότερα βήματα της ροής εργασίας έχουν κενό περιεχόμενο μηνύματος",
|
||||
"workflow_validation_unverified_contacts": "Ένας ή περισσότεροι αριθμοί τηλεφώνου ή διευθύνσεις email δεν έχουν επαληθευτεί",
|
||||
"supercharge_your_workflows_with_cal_ai": "Ενισχύστε τις ροές εργασίας σας με το Cal.ai",
|
||||
"supercharge_your_workflows_with_cal_ai_description": "Ρεαλιστικοί AI πράκτορες που κλείνουν συναντήσεις, στέλνουν υπενθυμίσεις και επικοινωνούν με τους πελάτες σας.",
|
||||
"phone_number_imported_successfully": "Ο αριθμός τηλεφώνου εισήχθη και συνδέθηκε με τον πράκτορα με επιτυχία",
|
||||
"phone_number_deleted_successfully": "Ο αριθμός τηλεφώνου διαγράφηκε με επιτυχία",
|
||||
"delete_phone_number_confirmation": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον αριθμό τηλεφώνου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
|
||||
@@ -846,6 +848,7 @@
|
||||
"phone_number_subscription_cancelled_successfully": "Η συνδρομή του αριθμού τηλεφώνου ακυρώθηκε με επιτυχία",
|
||||
"updating": "Ενημέρωση",
|
||||
"round_robin": "Round Robin",
|
||||
"hi_how_are_you_doing": "Γεια, πώς είστε;",
|
||||
"round_robin_description": "Κυκλική εναλλαγή συναντήσεων μεταξύ πολλών μελών της ομάδας.",
|
||||
"managed_event": "Διαχειριζόμενη εκδήλωση",
|
||||
"username_placeholder": "όνομα χρήστη",
|
||||
@@ -876,9 +879,9 @@
|
||||
"are_you_sure_you_want_to_delete_workflow_step": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το βήμα ροής εργασίας;",
|
||||
"do_you_still_want_to_unsubscribe": "Θέλετε ακόμα να καταργήσετε την εγγραφή του αριθμού τηλεφώνου από αυτόν τον πράκτορα;",
|
||||
"the_action_will_disconnect_phone_number": "Αυτή η ενέργεια θα αποσυνδέσει τον αριθμό τηλεφώνου από τον πράκτορα. Ο πράκτορας δεν θα μπορεί να πραγματοποιεί κλήσεις μέχρι να συνδεθεί ένας νέος αριθμός τηλεφώνου.",
|
||||
"cal_ai_phone_numbers": "Αριθμοί τηλεφώνου Cal AI",
|
||||
"cal_ai_phone_numbers": "Αριθμοί Τηλεφώνου",
|
||||
"connect_phone_number": "Σύνδεση αριθμού τηλεφώνου",
|
||||
"cal_ai_phone_numbers_description": "Διαχειριστείτε τους αριθμούς τηλεφώνου Cal AI",
|
||||
"cal_ai_phone_numbers_description": "Διαχειριστείτε τους αριθμούς τηλεφώνου σας",
|
||||
"import_number": "Εισαγωγή αριθμού",
|
||||
"this_action_will_also": "Αυτή η ενέργεια επίσης θα:",
|
||||
"import_phone_number": "Εισαγωγή αριθμού τηλεφώνου",
|
||||
@@ -886,7 +889,7 @@
|
||||
"cancel_your_phone_number_subscription": "Ακύρωση της συνδρομής του αριθμού τηλεφώνου σας",
|
||||
"delete_associated_phone_number": "Διαγραφή του συσχετισμένου αριθμού τηλεφώνου",
|
||||
"unauthorized_create_workflow": "Δεν έχετε εξουσιοδότηση για τη δημιουργία αυτής της ροής εργασίας",
|
||||
"import_phone_number_description": "Εισαγάγετε τον αριθμό τηλεφώνου Twilio για χρήση με το Phone",
|
||||
"import_phone_number_description": "Εισαγάγετε τον αριθμό τηλεφώνου Twilio για χρήση με το Τηλέφωνο",
|
||||
"phone_number_cost": "${{price}}/μήνα",
|
||||
"buy_new_number": "Αγορά νέου αριθμού",
|
||||
"buy_number_cost_x_per_month": "Η αγορά ενός αριθμού τηλεφώνου κοστίζει ${{priceInDollars}} ανά μήνα. Θα χρεώνεστε μηνιαία για κάθε ενεργό αριθμό τηλεφώνου.",
|
||||
@@ -897,6 +900,7 @@
|
||||
"yes_cancel_subscription": "Ναι, ακύρωση συνδρομής",
|
||||
"cancel_phone_number_subscription_confirmation": "Είστε βέβαιοι ότι θέλετε να ακυρώσετε αυτή τη συνδρομή αριθμού τηλεφώνου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί και θα χάσετε την πρόσβαση σε αυτόν τον αριθμό τηλεφώνου.",
|
||||
"add_members": "Προσθήκη μελών...",
|
||||
"add_members_no_ellipsis": "Προσθήκη μελών",
|
||||
"no_assigned_members": "Δεν υπάρχουν ανατεθειμένα μέλη",
|
||||
"assigned_to": "Ανατέθηκε σε",
|
||||
"you_must_be_logged_in_to": "Πρέπει να είστε συνδεδεμένοι στο {{url}}",
|
||||
@@ -1207,6 +1211,7 @@
|
||||
"categories": "Κατηγορίες",
|
||||
"pricing": "Τιμολόγηση",
|
||||
"learn_more": "Μάθετε περισσότερα",
|
||||
"try_now": "Δοκιμάστε τώρα",
|
||||
"privacy_policy": "Πολιτική απορρήτου",
|
||||
"terms_of_service": "Όροι χρήσης",
|
||||
"remove": "Αφαίρεση",
|
||||
@@ -1440,6 +1445,7 @@
|
||||
"whatsapp_attendee_action": "αποστολή μηνύματος WhatsApp στον συμμετέχοντα",
|
||||
"workflows": "Ροές εργασίας",
|
||||
"new_workflow_btn": "Νέα ροή εργασίας",
|
||||
"how_would_you_like_to_start": "Πώς θα θέλατε να ξεκινήσετε;",
|
||||
"add_new_workflow": "Προσθήκη νέας ροής εργασίας",
|
||||
"reschedule_event_trigger": "όταν επαναπρογραμματίζεται το συμβάν",
|
||||
"trigger": "Έναυσμα",
|
||||
@@ -1716,6 +1722,8 @@
|
||||
"event_duration_info": "Η διάρκεια της εκδήλωσης",
|
||||
"event_time_info": "Η ώρα έναρξης της εκδήλωσης",
|
||||
"event_type_not_found": "Ο τύπος εκδήλωσης δεν βρέθηκε",
|
||||
"number_to_call_variable": "Αριθμός κλήσης",
|
||||
"number_to_call_info": "Ο αριθμός τηλεφώνου του χρήστη που καλείτε",
|
||||
"location_variable": "Τοποθεσία",
|
||||
"location_info": "Η τοποθεσία της εκδήλωσης",
|
||||
"additional_notes_variable": "Πρόσθετες σημειώσεις",
|
||||
@@ -1753,6 +1761,7 @@
|
||||
"team_url": "URL ομάδας",
|
||||
"team_members": "Μέλη ομάδας",
|
||||
"more": "Περισσότερα",
|
||||
"cal_ai_workflows": "Ροές εργασίας Cal.ai",
|
||||
"and_count_more": "και {{count}} ακόμη",
|
||||
"more_page_footer": "Θεωρούμε την εφαρμογή για κινητά ως επέκταση της διαδικτυακής εφαρμογής. Εάν εκτελείτε περίπλοκες ενέργειες, παρακαλούμε ανατρέξτε στη διαδικτυακή εφαρμογή.",
|
||||
"workflow_example_1": "Αποστολή υπενθύμισης SMS 24 ώρες πριν την έναρξη της εκδήλωσης στον συμμετέχοντα",
|
||||
@@ -1761,6 +1770,18 @@
|
||||
"workflow_example_4": "Αποστολή υπενθύμισης email 1 ώρα πριν την έναρξη της εκδήλωσης στον συμμετέχοντα",
|
||||
"workflow_example_5": "Αποστολή προσαρμοσμένου email στον διοργανωτή όταν η εκδήλωση επαναπρογραμματιστεί",
|
||||
"workflow_example_6": "Αποστολή προσαρμοσμένου SMS στον διοργανωτή όταν γίνει κράτηση νέας εκδήλωσης",
|
||||
"send_sms_reminder": "Αποστολή υπενθύμισης SMS",
|
||||
"send_sms_reminder_description": "24 ώρες πριν την έναρξη του συμβάντος",
|
||||
"follow_up_with_no_shows": "Επικοινωνία με όσους δεν εμφανίστηκαν",
|
||||
"follow_up_with_no_shows_description": "30 λεπτά μετά το τέλος του συμβάντος",
|
||||
"remind_attendees_to_bring_id": "Υπενθύμιση στους συμμετέχοντες να φέρουν ταυτότητα",
|
||||
"remind_attendees_to_bring_id_description": "1 ημέρα πριν την έναρξη του συμβάντος",
|
||||
"email_to_remind_booking": "Υπενθύμιση μέσω Email",
|
||||
"email_to_remind_booking_description": "1 ώρα πριν την έναρξη του συμβάντος",
|
||||
"custom_sms_reminder": "Προσαρμοσμένη υπενθύμιση SMS",
|
||||
"custom_sms_reminder_description": "Όταν προγραμματίζεται το συμβάν",
|
||||
"custom_email_reminder": "Προσαρμοσμένη υπενθύμιση Email",
|
||||
"custom_email_reminder_description": "Το συμβάν επαναπρογραμματίζεται για τον διοργανωτή",
|
||||
"count_managed_to_limit": "Συμπερίληψη αριθμού κρατήσεων από διαχειριζόμενους τύπους εκδηλώσεων",
|
||||
"welcome_to_cal_header": "Καλώς ήρθατε στο {{appName}}!",
|
||||
"edit_form_later_subtitle": "Θα μπορείτε να το επεξεργαστείτε αργότερα.",
|
||||
@@ -2184,6 +2205,8 @@
|
||||
"show_on_booking_page": "Εμφάνιση στη σελίδα κράτησης",
|
||||
"visit_cancelled_booking": "Μπορείτε να επισκεφθείτε τη σελίδα ακυρωμένων κρατήσεων",
|
||||
"get_started_zapier_templates": "Ξεκινήστε με τα πρότυπα του Zapier",
|
||||
"standard_templates": "Τυπικά Πρότυπα",
|
||||
"cal_ai_templates": "Πρότυπα Cal.ai",
|
||||
"team_is_unpublished": "Η ομάδα {{team}} δεν έχει δημοσιευτεί",
|
||||
"org_is_unpublished_description": "Αυτός ο σύνδεσμος οργανισμού δεν είναι διαθέσιμος αυτή τη στιγμή. Παρακαλούμε επικοινωνήστε με τον ιδιοκτήτη του οργανισμού ή ζητήστε του να τον δημοσιεύσει.",
|
||||
"team_is_unpublished_description": "Αυτός ο σύνδεσμος ομάδας δεν είναι διαθέσιμος αυτή τη στιγμή. Παρακαλούμε επικοινωνήστε με τον ιδιοκτήτη της ομάδας ή ζητήστε του να τον δημοσιεύσει.",
|
||||
@@ -2341,7 +2364,6 @@
|
||||
"could_not_charge_card": "Δεν ήταν δυνατή η χρέωση της κάρτας για την πληρωμή.",
|
||||
"insights": "Αναλύσεις",
|
||||
"routing_forms": "Φόρμες δρομολόγησης",
|
||||
"testing_workflow_info_message": "Κατά τη δοκιμή αυτής της ροής εργασίας, λάβετε υπόψη ότι τα email και τα SMS μπορούν να προγραμματιστούν τουλάχιστον 1 ώρα νωρίτερα",
|
||||
"insights_no_data_found_for_filter": "Δεν βρέθηκαν δεδομένα για το επιλεγμένο φίλτρο ή τις επιλεγμένες ημερομηνίες.",
|
||||
"acknowledge_booking_no_show_fee": "Αναγνωρίζω ότι εάν δεν παρευρεθώ σε αυτήν την εκδήλωση, θα χρεωθεί στην κάρτα μου τέλος μη προσέλευσης ύψους {{amount, currency}}.",
|
||||
"days": "ημέρες",
|
||||
@@ -2464,7 +2486,15 @@
|
||||
"insights_team_filter": "Ομάδα: {{teamName}}",
|
||||
"insights_user_filter": "Χρήστης: {{userName}}",
|
||||
"insights_subtitle": "Δείτε στατιστικά κρατήσεων για τις εκδηλώσεις σας",
|
||||
"call_history": "Ιστορικό Κλήσεων",
|
||||
"call_history_subtitle": "Προβολή ιστορικού κλήσεων σε όλες τις κλήσεις Cal.ai",
|
||||
"location_options": "{{locationCount}} επιλογές τοποθεσίας",
|
||||
"channel_type": "Τύπος Καναλιού",
|
||||
"end_reason": "Αιτία Τερματισμού",
|
||||
"session_status": "Κατάσταση Συνεδρίας",
|
||||
"user_sentiment": "Διάθεση Χρήστη",
|
||||
"time_header": "Ώρα",
|
||||
"from_header": "Από",
|
||||
"custom_plan": "Προσαρμοσμένο πλάνο",
|
||||
"email_embed": "Ενσωμάτωση στο email",
|
||||
"add_times_to_your_email": "Επιλέξτε μερικές διαθέσιμες ώρες και ενσωματώστε τις στο email σας",
|
||||
@@ -2752,6 +2782,8 @@
|
||||
"account_already_linked": "Ο λογαριασμός είναι ήδη συνδεδεμένος",
|
||||
"send_email": "Αποστολή email",
|
||||
"cal_ai_phone_call_action": "Κλήση συμμετέχοντα χρησιμοποιώντας τον Φωνητικό Πράκτορα Cal.ai",
|
||||
"call_to_confirm_booking": "Κλήση για επιβεβαίωση κράτησης",
|
||||
"cal_ai_phone_call_action_description": "2 ώρες πριν την έναρξη του συμβάντος",
|
||||
"cal_ai_agent_configuration": "Διαμόρφωση πράκτορα Cal.ai",
|
||||
"choose_at_least_one_event_type_test_call": "Παρακαλώ επιλέξτε τουλάχιστον έναν τύπο εκδήλωσης για να κάνετε μια δοκιμαστική κλήση.",
|
||||
"mark_as_no_show": "Σήμανση ως μη εμφάνιση",
|
||||
@@ -3203,9 +3235,15 @@
|
||||
"verify_email_change": "Επαλήθευση αλλαγής email",
|
||||
"buy_credits": "Αγορά μονάδων",
|
||||
"credits": "Μονάδες",
|
||||
"view_and_manage_credits": "Προβολή και διαχείριση μονάδων",
|
||||
"credits_used": "Πιστώσεις που χρησιμοποιήθηκαν",
|
||||
"total_credits_remaining": "Συνολικό υπόλοιπο",
|
||||
"credits_per_tip_org": "Λαμβάνετε 1000 πιστώσεις ανά μήνα, ανά μέλος της ομάδας",
|
||||
"credits_per_tip_teams": "Λαμβάνετε 750 πιστώσεις ανά μήνα, ανά μέλος της ομάδας",
|
||||
"view_and_manage_credits": "Προβολή και διαχείριση πιστώσεων για αποστολή μηνυμάτων SMS",
|
||||
"view_and_manage_credits_description": "Προβολή και διαχείριση μονάδων για αποστολή μηνυμάτων SMS. Μία μονάδα αξίζει 1¢ (USD). <0>Μάθετε περισσότερα</0>",
|
||||
"credit_worth_description": "Μία πίστωση αξίζει 1¢ (USD). <0>Μάθετε περισσότερα</0>",
|
||||
"buy_additional_credits": "Αγορά επιπλέον μονάδων ($0.01 ανά μονάδα)",
|
||||
"view_additional_credits_expense_tip": "Μπορείτε να δείτε τις επιπλέον δαπάνες πιστώσεων στο αρχείο εξόδων σας",
|
||||
"overview": "Επισκόπηση",
|
||||
"organization_slug_taken": "Το αναγνωριστικό του οργανισμού χρησιμοποιείται ήδη",
|
||||
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Δεν μπορείτε να δημιουργήσετε έναν οργανισμό καθώς είστε ήδη μέλος ενός οργανισμού",
|
||||
@@ -3386,10 +3424,13 @@
|
||||
"credit_limit_reached_message": "Η ομάδα σας {{teamName}} στο Cal.com έχει εξαντλήσει τις μονάδες της. Ως αποτέλεσμα, τα μηνύματα SMS αποστέλλονται τώρα μέσω email. Για να συνεχίσετε την αποστολή SMS, παρακαλούμε αγοράστε επιπλέον μονάδες.",
|
||||
"credit_limit_reached_message_user": "Ο λογαριασμός σας στο Cal.com έχει εξαντλήσει τις μονάδες του. Ως αποτέλεσμα, τα μηνύματα SMS αποστέλλονται τώρα μέσω email. Για να συνεχίσετε την αποστολή SMS, παρακαλούμε αγοράστε επιπλέον μονάδες.",
|
||||
"current_credit_balance": "Τρέχον υπόλοιπο: {{balance}} μονάδες",
|
||||
"current_balance": "Τρέχον υπόλοιπο:",
|
||||
"notification_about_your_booking": "Ειδοποίηση σχετικά με την κράτησή σας",
|
||||
"monthly_credits": "Μηνιαίες μονάδες",
|
||||
"total_credits": "Συνολικές μονάδες: {{totalCredits}}",
|
||||
"remaining_credits": "Υπολειπόμενες μονάδες: {{remainingCredits}}",
|
||||
"remaining": "Υπόλοιπο",
|
||||
"total": "Σύνολο",
|
||||
"additional_credits": "Επιπλέον μονάδες",
|
||||
"routing_form_next_in_queue": "{{count}} επόμενα στην ουρά",
|
||||
"routing_form_select_members_to_email": "Αποστολή απαντήσεων email σε",
|
||||
@@ -3467,6 +3508,11 @@
|
||||
"pbac_desc_view_workflows": "Προβολή υπαρχουσών ροών εργασίας και των διαμορφώσεών τους",
|
||||
"pbac_desc_update_workflows": "Επεξεργασία και τροποποίηση ρυθμίσεων ροών εργασίας",
|
||||
"pbac_desc_delete_workflows": "Αφαίρεση ροών εργασίας από το σύστημα",
|
||||
"pbac_resource_webhook": "Webhook",
|
||||
"pbac_desc_create_webhooks": "Δημιουργία webhooks",
|
||||
"pbac_desc_view_webhooks": "Προβολή webhooks",
|
||||
"pbac_desc_update_webhooks": "Ενημέρωση webhooks",
|
||||
"pbac_desc_delete_webhooks": "Διαγραφή webhooks",
|
||||
"pbac_desc_manage_workflows": "Πλήρης πρόσβαση διαχείρισης σε όλες τις ροές εργασίας",
|
||||
"pbac_desc_create_event_types": "Δημιουργία τύπων εκδηλώσεων",
|
||||
"pbac_desc_view_event_types": "Προβολή τύπων εκδηλώσεων",
|
||||
@@ -3594,6 +3640,7 @@
|
||||
"webhook_trigger_event": "Το όνομα του συμβάντος ενεργοποίησης (π.χ., BOOKING_CREATED, BOOKING_CANCELLED)",
|
||||
"webhook_created_at": "Η ώρα του webhook",
|
||||
"webhook_type": "Το slug του τύπου εκδήλωσης",
|
||||
"set_up_agent": "Ρύθμιση Πράκτορα",
|
||||
"webhook_title": "Το όνομα του τύπου εκδήλωσης",
|
||||
"webhook_start_time": "Η ώρα έναρξης της εκδήλωσης",
|
||||
"webhook_end_time": "Η ώρα λήξης της εκδήλωσης",
|
||||
@@ -3625,6 +3672,9 @@
|
||||
"visit": "Επίσκεψη",
|
||||
"location_custom_label_input_label": "Προσαρμοσμένη ετικέτα στη σελίδα κράτησης",
|
||||
"meeting_link": "Σύνδεσμος συνάντησης",
|
||||
"session_outcome": "Αποτέλεσμα Συνεδρίας",
|
||||
"call_created": "Δημιουργήθηκε Κλήση",
|
||||
"voicemail": "Φωνητικό Μήνυμα",
|
||||
"my_bookings": "Οι κρατήσεις μου",
|
||||
"phone": "Τηλέφωνο",
|
||||
"free": "Δωρεάν",
|
||||
@@ -3632,6 +3682,8 @@
|
||||
"user_name": "Όνομα χρήστη",
|
||||
"expand_panel": "Ανάπτυξη πάνελ",
|
||||
"collapse_panel": "Σύμπτυξη πάνελ",
|
||||
"email_verification_required": "Απαιτείται επαλήθευση email για αυτόν τον τύπο εκδήλωσης",
|
||||
"invalid_verification_code": "Παρέχεται μη έγκυρος κωδικός επαλήθευσης",
|
||||
"you_have_one_team": "Έχετε μία ομάδα",
|
||||
"consider_consolidating_one_team_org": "Σκεφτείτε να δημιουργήσετε έναν οργανισμό για να ενοποιήσετε τη χρέωση, τα εργαλεία διαχείρισης και τα αναλυτικά στοιχεία σε όλη την ομάδα σας.",
|
||||
"consider_consolidating_multi_team_org": "Σκεφτείτε να δημιουργήσετε έναν οργανισμό για να ενοποιήσετε τη χρέωση, τα εργαλεία διαχείρισης και τα αναλυτικά στοιχεία σε όλες τις ομάδες σας.",
|
||||
@@ -3652,5 +3704,32 @@
|
||||
"before_scheduled_start_time": "Πριν την προγραμματισμένη ώρα έναρξης",
|
||||
"cancel_booking_acknowledge_no_show_fee": "Αναγνωρίζω ότι με την ακύρωση της κράτησης εντός {{timeValue}} {{timeUnit}} από την ώρα έναρξης θα χρεωθώ το τέλος μη εμφάνισης ύψους {{amount, currency}}",
|
||||
"contact_organizer": "Εάν έχετε οποιεσδήποτε ερωτήσεις, παρακαλώ επικοινωνήστε με τον διοργανωτή.",
|
||||
"booking_time_option": "Χρόνος κράτησης",
|
||||
"booking_time_option_description": "Όταν προγραμματίζεται η κράτηση (αρχή έως τέλος)",
|
||||
"created_at_option": "Δημιουργήθηκε στις",
|
||||
"created_at_option_description": "Όταν δημιουργήθηκε αρχικά η κράτηση",
|
||||
"call_details": "Λεπτομέρειες Κλήσης",
|
||||
"call_id": "Αναγνωριστικό Κλήσης",
|
||||
"call_information": "Πληροφορίες Κλήσης",
|
||||
"sentiment": "Συναίσθημα",
|
||||
"disconnect_reason": "Αιτία Αποσύνδεσης",
|
||||
"call_summary": "Περίληψη Κλήσης",
|
||||
"transcription": "Απομαγνητοφώνηση",
|
||||
"event_details": "Λεπτομέρειες Εκδήλωσης",
|
||||
"agent": "Πράκτορας",
|
||||
"no_transcript_available": "Δεν υπάρχει διαθέσιμη απομαγνητοφώνηση",
|
||||
"testing_sms_workflow_info_message": "Κατά τη δοκιμή αυτής της ροής εργασίας, λάβετε υπόψη ότι τα SMS πρέπει να προγραμματιστούν τουλάχιστον 15 λεπτά νωρίτερα",
|
||||
"start_from_scratch_title": "Ξεκινήστε από το μηδέν",
|
||||
"start_from_scratch_description": "Δημιουργήστε τη δική σας ροή εργασίας από το μηδέν.",
|
||||
"cal_ai_template_title": "Πρότυπο Cal.ai",
|
||||
"cal_ai_template_description": "AI πράκτορες που κλείνουν συναντήσεις, στέλνουν υπενθυμίσεις και κάνουν follow up!",
|
||||
"voice": "Φωνή",
|
||||
"select_voice": "Επιλογή φωνής",
|
||||
"select_voice_for_agent": "Επιλέξτε φωνή για τον πράκτορά σας",
|
||||
"choose_a_voice_for_your_agent": "Επιλέξτε μια φωνή για τον πράκτορά σας",
|
||||
"trait": "Χαρακτηριστικό",
|
||||
"voice_id": "Αναγνωριστικό φωνής",
|
||||
"use_voice": "Χρήση φωνής",
|
||||
"current_voice": "Τρέχουσα φωνή",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user