diff --git a/.changeset/config.json b/.changeset/config.json index c12b327eb70862..89c3db9ed593bf 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,7 +7,7 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@calcom/platform-libraries", "@calcom/api-v2"], + "ignore": ["@calcom/platform-libraries"], "privatePackages": { "version": false, "tag": false diff --git a/.eslintrc.js b/.eslintrc.js index 6d6b21761b4e8d..43c0cd83593ea1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,4 @@ +// This configuration only applies to the package manager root. /** @type {import("eslint").Linter.Config} */ module.exports = { extends: ["./packages/config/eslint-preset.js"], @@ -6,105 +7,29 @@ module.exports = { "import/no-cycle": ["warn", { maxDepth: Infinity }], }, overrides: [ - // WARN: features must not be imported by app-store or lib { - files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"], + files: ["packages/lib/**/*.{ts,tsx,js,jsx}", "packages/prisma/**/*.{ts,tsx,js,jsx}"], rules: { "no-restricted-imports": [ "warn", { - 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.", - }, - ], + paths: ["@calcom/app-store"], + patterns: ["@calcom/app-store/*"], }, ], }, }, - // 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", - { - patterns: [ - { - 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": [ + "@typescript-eslint/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.", + 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.", + allowTypeImports: false, }, ], }, diff --git a/.yarn/versions/63b22516.yml b/.yarn/versions/63b22516.yml deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/.yarn/versions/734d2c7a.yml b/.yarn/versions/734d2c7a.yml deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/.yarn/versions/99843d84.yml b/.yarn/versions/99843d84.yml deleted file mode 100644 index ccddf60ece96de..00000000000000 --- a/.yarn/versions/99843d84.yml +++ /dev/null @@ -1,2 +0,0 @@ -undecided: - - calcom-monorepo diff --git a/.yarn/versions/df607603.yml b/.yarn/versions/df607603.yml deleted file mode 100644 index dd2ce8f8224c20..00000000000000 --- a/.yarn/versions/df607603.yml +++ /dev/null @@ -1,3 +0,0 @@ -undecided: - - calcom-monorepo - - "@calcom/prisma" diff --git a/apps/api/v1/lib/helpers/captureErrors.ts b/apps/api/v1/lib/helpers/captureErrors.ts index 216276f3fdd640..654750c074f430 100644 --- a/apps/api/v1/lib/helpers/captureErrors.ts +++ b/apps/api/v1/lib/helpers/captureErrors.ts @@ -1,14 +1,20 @@ 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); - res.status(500).json({ message: "Something went wrong" }); + 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 }); } }; diff --git a/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts b/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts index 86dfc51d0548ae..3b57cfeb4ff323 100644 --- a/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts +++ b/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts @@ -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,7 +51,6 @@ 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({ @@ -75,14 +74,15 @@ describe("rateLimitApiKey middleware", () => { success: true, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore (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,11 +96,10 @@ 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); @@ -122,7 +121,6 @@ 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); @@ -133,7 +131,6 @@ 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({ @@ -161,7 +158,6 @@ 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); @@ -172,7 +168,6 @@ 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({ @@ -200,7 +195,6 @@ 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); @@ -241,7 +235,6 @@ 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); diff --git a/apps/api/v1/lib/selects/event-type.ts b/apps/api/v1/lib/selects/event-type.ts deleted file mode 100644 index 7339833144b7b5..00000000000000 --- a/apps/api/v1/lib/selects/event-type.ts +++ /dev/null @@ -1,68 +0,0 @@ -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; diff --git a/apps/api/v1/lib/validations/event-type.ts b/apps/api/v1/lib/validations/event-type.ts index 798ad2c442be7b..ec40237fb4d2d0 100644 --- a/apps/api/v1/lib/validations/event-type.ts +++ b/apps/api/v1/lib/validations/event-type.ts @@ -6,12 +6,13 @@ import { MIN_EVENT_DURATION_MINUTES, } from "@calcom/lib/constants"; import slugify from "@calcom/lib/slugify"; -import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; +import { customInputSchema, 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"; @@ -117,3 +118,67 @@ 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(), + }) +); diff --git a/apps/api/v1/pages/api/bookings/[id]/_get.ts b/apps/api/v1/pages/api/bookings/[id]/_get.ts index a5105c01e777eb..8d514fcbe01cb5 100644 --- a/apps/api/v1/pages/api/bookings/[id]/_get.ts +++ b/apps/api/v1/pages/api/bookings/[id]/_get.ts @@ -102,11 +102,8 @@ 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, }, diff --git a/apps/api/v1/pages/api/bookings/[id]/recordings/_get.ts b/apps/api/v1/pages/api/bookings/[id]/recordings/_get.ts index f7d68b6bdafda6..7aee663c877ff8 100644 --- a/apps/api/v1/pages/api/bookings/[id]/recordings/_get.ts +++ b/apps/api/v1/pages/api/bookings/[id]/recordings/_get.ts @@ -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/app-store/videoClient"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; +} from "@calcom/lib/videoClient"; import prisma from "@calcom/prisma"; import type { RecordingItemSchema } from "@calcom/prisma/zod-utils"; import type { PartialReference } from "@calcom/types/EventManager"; @@ -63,7 +63,6 @@ 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 }, }); diff --git a/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/_get.ts b/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/_get.ts index ee47e89fe9b5b0..9cf5c2e30b7676 100644 --- a/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/_get.ts +++ b/apps/api/v1/pages/api/bookings/[id]/transcripts/[recordingId]/_get.ts @@ -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/app-store/videoClient"; -import { HttpError } from "@calcom/lib/http-error"; -import { defaultResponder } from "@calcom/lib/server/defaultResponder"; +} from "@calcom/lib/videoClient"; import prisma from "@calcom/prisma"; import type { PartialReference } from "@calcom/types/EventManager"; @@ -61,7 +61,6 @@ 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 }, }); diff --git a/apps/api/v1/pages/api/bookings/[id]/transcripts/_get.ts b/apps/api/v1/pages/api/bookings/[id]/transcripts/_get.ts index 17ff122d5e8498..821f2ec2f21629 100644 --- a/apps/api/v1/pages/api/bookings/[id]/transcripts/_get.ts +++ b/apps/api/v1/pages/api/bookings/[id]/transcripts/_get.ts @@ -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,7 +44,6 @@ 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 }, }); diff --git a/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts b/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts index ff2c74718a7c9a..bc213880f5fd23 100644 --- a/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts +++ b/apps/api/v1/pages/api/destination-calendars/[id]/_patch.ts @@ -1,10 +1,7 @@ import type { NextApiRequest } from "next"; import type { z } from "zod"; -import { - getCalendarCredentialsWithoutDelegation, - getConnectedCalendars, -} from "@calcom/features/calendars/lib/CalendarManager"; +import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager"; import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import prisma from "@calcom/prisma"; diff --git a/apps/api/v1/pages/api/destination-calendars/_post.ts b/apps/api/v1/pages/api/destination-calendars/_post.ts index 74a9a1f71b6a67..2962224a7f5690 100644 --- a/apps/api/v1/pages/api/destination-calendars/_post.ts +++ b/apps/api/v1/pages/api/destination-calendars/_post.ts @@ -1,9 +1,6 @@ import type { NextApiRequest } from "next"; -import { - getCalendarCredentialsWithoutDelegation, - getConnectedCalendars, -} from "@calcom/features/calendars/lib/CalendarManager"; +import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager"; import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import prisma from "@calcom/prisma"; diff --git a/apps/api/v1/pages/api/event-types/[id]/_delete.ts b/apps/api/v1/pages/api/event-types/[id]/_delete.ts index f6af2d58fab362..0242d666baa50d 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_delete.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_delete.ts @@ -1,7 +1,8 @@ import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; -import { prisma } from "@calcom/prisma"; +import { defaultResponder } from "@calcom/lib/server/defaultResponder"; +import prisma from "@calcom/prisma"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; @@ -65,4 +66,4 @@ async function checkPermissions(req: NextApiRequest) { throw new HttpError({ statusCode: 403, message: "Forbidden" }); } -export default deleteHandler; +export default defaultResponder(deleteHandler); diff --git a/apps/api/v1/pages/api/event-types/[id]/_get.ts b/apps/api/v1/pages/api/event-types/[id]/_get.ts index e80ac95e491299..123ccbd7423177 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_get.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_get.ts @@ -1,10 +1,11 @@ import type { NextApiRequest } from "next"; import { HttpError } from "@calcom/lib/http-error"; -import { prisma } from "@calcom/prisma"; +import { defaultResponder } from "@calcom/lib/server/defaultResponder"; +import prisma from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; -import { eventTypeSelect } from "~/lib/selects/event-type"; +import { schemaEventTypeReadPublic } from "~/lib/validations/event-type"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware"; @@ -48,13 +49,15 @@ export async function getHandler(req: NextApiRequest) { const eventType = await prisma.eventType.findUnique({ where: { id }, - select: eventTypeSelect, + 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 } }, + }, }); - - if (!eventType) { - throw new HttpError({ statusCode: 404, message: "Event type not found" }); - } - await checkPermissions(req, eventType); const link = eventType ? getCalLink(eventType) : null; @@ -71,12 +74,9 @@ export async function getHandler(req: NextApiRequest) { eventType.scheduleId = user.defaultScheduleId; } - return { - event_type: { - ...eventType, - link, - }, - }; + // TODO: eventType when not found should be a 404 + // but API consumers may depend on the {} behaviour. + return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) }; } type BaseEventTypeCheckPermissions = { @@ -100,4 +100,4 @@ async function checkPermissions( throw new HttpError({ statusCode: 403, message: "Forbidden" }); } -export default getHandler; +export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/event-types/[id]/_patch.ts b/apps/api/v1/pages/api/event-types/[id]/_patch.ts index 5755855df9d1a2..3dcff2f4e8a4dc 100644 --- a/apps/api/v1/pages/api/event-types/[id]/_patch.ts +++ b/apps/api/v1/pages/api/event-types/[id]/_patch.ts @@ -2,13 +2,13 @@ import type { NextApiRequest } from "next"; import type { z } from "zod"; import { HttpError } from "@calcom/lib/http-error"; -import { prisma } from "@calcom/prisma"; +import { defaultResponder } from "@calcom/lib/server/defaultResponder"; +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 } from "~/lib/validations/event-type"; +import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } 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, select: eventTypeSelect }); - return { event_type: eventType }; + const eventType = await prisma.eventType.update({ where: { id }, data }); + return { event_type: schemaEventTypeReadPublic.parse(eventType) }; } async function checkPermissions(req: NextApiRequest, body: z.infer) { @@ -249,4 +249,4 @@ async function checkPermissions(req: NextApiRequest, body: z.infer({ eventTypes: data, prisma, userIds })).map( (eventType) => { const link = getCalLink(eventType); - return { ...eventType, link }; + return schemaEventTypeReadPublic.parse({ ...eventType, link }); } ), }; diff --git a/apps/api/v1/pages/api/event-types/_post.ts b/apps/api/v1/pages/api/event-types/_post.ts index 02a3c4097d2cb7..15e1aecc726aaa 100644 --- a/apps/api/v1/pages/api/event-types/_post.ts +++ b/apps/api/v1/pages/api/event-types/_post.ts @@ -2,12 +2,11 @@ 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 { eventTypeSelect } from "~/lib/selects/event-type"; -import { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type"; +import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware"; import checkParentEventOwnership from "./_utils/checkParentEventOwnership"; @@ -307,10 +306,10 @@ async function postHandler(req: NextApiRequest) { data.hosts = { createMany: { data: hosts } }; } - const eventType = await prisma.eventType.create({ data, select: eventTypeSelect }); + const eventType = await prisma.eventType.create({ data, include: { hosts: true } }); return { - event_type: eventType, + event_type: schemaEventTypeReadPublic.parse(eventType), message: "Event type created successfully", }; } diff --git a/apps/api/v1/pages/api/payments/[id].ts b/apps/api/v1/pages/api/payments/[id].ts index 35ab674c07d8a7..0b621cb86d8b6d 100644 --- a/apps/api/v1/pages/api/payments/[id].ts +++ b/apps/api/v1/pages/api/payments/[id].ts @@ -46,7 +46,6 @@ 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 diff --git a/apps/api/v1/pages/api/schedules/[id]/_get.ts b/apps/api/v1/pages/api/schedules/[id]/_get.ts index ffded6bae12ecc..0600b5eaa3f39c 100644 --- a/apps/api/v1/pages/api/schedules/[id]/_get.ts +++ b/apps/api/v1/pages/api/schedules/[id]/_get.ts @@ -75,11 +75,7 @@ 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 }, - // eslint-disable-next-line @calcom/eslint/no-prisma-include-true - include: { availability: true }, - }); + const data = await prisma.schedule.findUniqueOrThrow({ where: { id }, include: { availability: true } }); return { schedule: schemaSchedulePublic.parse(data) }; } diff --git a/apps/api/v1/pages/api/teams/[teamId]/_patch.ts b/apps/api/v1/pages/api/teams/[teamId]/_patch.ts index a79a4ce09ce5af..4af941904eadad 100644 --- a/apps/api/v1/pages/api/teams/[teamId]/_patch.ts +++ b/apps/api/v1/pages/api/teams/[teamId]/_patch.ts @@ -62,7 +62,6 @@ 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"] } } } }, }); diff --git a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts b/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts index 637af631d6cb13..24312019bdf1f0 100644 --- a/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts +++ b/apps/api/v1/pages/api/teams/[teamId]/event-types/_get.ts @@ -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 { eventTypeSelect } from "~/lib/selects/event-type"; +import { schemaEventTypeReadPublic } from "~/lib/validations/event-type"; const querySchema = z.object({ teamId: z.coerce.number(), @@ -57,11 +57,17 @@ async function getHandler(req: NextApiRequest) { members: { some: { userId } }, }, }, - select: eventTypeSelect, - }; - return { - event_types: await prisma.eventType.findMany(args), + 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 } }, + }, }; + + const data = await prisma.eventType.findMany(args); + return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) }; } export default defaultResponder(getHandler); diff --git a/apps/api/v1/test/lib/bookings/[id]/recordings/_get.test.ts b/apps/api/v1/test/lib/bookings/[id]/recordings/_get.test.ts index 582f01d830db68..2e241fcb88c497 100644 --- a/apps/api/v1/test/lib/bookings/[id]/recordings/_get.test.ts +++ b/apps/api/v1/test/lib/bookings/[id]/recordings/_get.test.ts @@ -9,7 +9,7 @@ import { buildBooking } from "@calcom/lib/test/builder"; import { getRecordingsOfCalVideoByRoomName, getDownloadLinkOfCalVideoByRecordingId, -} from "@calcom/app-store/videoClient"; +} from "@calcom/lib/videoClient"; import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers"; @@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response; const adminUserId = 1; const memberUserId = 10; -vi.mock("@calcom/app-store/videoClient", () => { +vi.mock("@calcom/lib/videoClient", () => { return { getRecordingsOfCalVideoByRoomName: vi.fn(), getDownloadLinkOfCalVideoByRecordingId: vi.fn(), diff --git a/apps/api/v1/test/lib/bookings/[id]/transcripts/[recordingId]/_get.test.ts b/apps/api/v1/test/lib/bookings/[id]/transcripts/[recordingId]/_get.test.ts index 9839308d0c9b93..8d865ceda59c4a 100644 --- a/apps/api/v1/test/lib/bookings/[id]/transcripts/[recordingId]/_get.test.ts +++ b/apps/api/v1/test/lib/bookings/[id]/transcripts/[recordingId]/_get.test.ts @@ -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/app-store/videoClient"; -import { buildBooking } from "@calcom/lib/test/builder"; +} from "@calcom/lib/videoClient"; 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/app-store/videoClient", () => { +vi.mock("@calcom/lib/videoClient", () => { return { getTranscriptsAccessLinkFromRecordingId: vi.fn(), checkIfRoomNameMatchesInRecording: vi.fn(), diff --git a/apps/api/v1/test/lib/bookings/[id]/transcripts/_get.test.ts b/apps/api/v1/test/lib/bookings/[id]/transcripts/_get.test.ts index 6234bca548b354..49fb9d33fbb47b 100644 --- a/apps/api/v1/test/lib/bookings/[id]/transcripts/_get.test.ts +++ b/apps/api/v1/test/lib/bookings/[id]/transcripts/_get.test.ts @@ -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/app-store/videoClient"; +import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/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/app-store/videoClient", () => { +vi.mock("@calcom/lib/videoClient", () => { return { getAllTranscriptsAccessLinkFromRoomName: vi.fn(), }; diff --git a/apps/api/v1/test/lib/bookings/_post.test.ts b/apps/api/v1/test/lib/bookings/_post.test.ts index dcebba79f66c65..77978723b8d121 100644 --- a/apps/api/v1/test/lib/bookings/_post.test.ts +++ b/apps/api/v1/test/lib/bookings/_post.test.ts @@ -86,7 +86,7 @@ vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({ }), })); -vi.mock("@calcom/features/bookings/lib/EventManager", () => ({ +vi.mock("@calcom/lib/EventManager", () => ({ default: vi.fn().mockImplementation(() => ({ reschedule: vi.fn().mockResolvedValue({ results: [], diff --git a/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts b/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts index 2e891b097301e2..863317a2d5caad 100644 --- a/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts +++ b/apps/api/v1/test/lib/event-types/[id]/_delete.test.ts @@ -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 } = createMocks({ + const { req, res } = createMocks({ method: "DELETE", body: {}, query: { @@ -71,18 +71,15 @@ describe("DELETE /api/event-types/[id]", () => { // Assign userId to the request objects req.userId = memberUser; - await expect(handler(req)).rejects.toThrowError( - expect.objectContaining({ - statusCode: 403, - }) - ); + await handler(req, res); + expect(res.statusCode).toBe(403); // Check if the deletion was successful }); }); describe("Success", async () => { test("Removes event type if user is owner of team associated with event type", async () => { // Mocks for DELETE request - const { req } = createMocks({ + const { req, res } = createMocks({ method: "DELETE", body: {}, query: { @@ -93,8 +90,8 @@ describe("DELETE /api/event-types/[id]", () => { // Assign userId to the request objects req.userId = adminUser; - const deletedEventType = await handler(req); - expect(deletedEventType).not.toBeNull(); + await handler(req, res); + expect(res.statusCode).toBe(200); // Check if the deletion was successful }); }); }); diff --git a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts index e1307b97223628..8f1db30a3a6705 100644 --- a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts +++ b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts @@ -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 } = createMocks({ + const { req, res } = createMocks({ method: "GET", body: {}, query: { @@ -32,40 +32,16 @@ describe("GET /api/event-types/[id]", () => { ); req.userId = 333333; + await handler(req, res); - await expect(handler(req)).rejects.toThrowError( - expect.objectContaining({ - statusCode: 403, - }) - ); - }); - test("Returns 404 if event type not found", async () => { - const { req } = createMocks({ - 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, - }) - ); + expect(res.statusCode).toBe(403); }); }); describe("Success", async () => { test("Returns event type if user is admin", async () => { const eventTypeId = 123456; - const { req } = createMocks({ + const { req, res } = createMocks({ method: "GET", body: {}, query: { @@ -81,17 +57,17 @@ describe("GET /api/event-types/[id]", () => { req.isSystemWideAdmin = true; req.userId = 333333; + await handler(req, res); - const responseData = await handler(req); - - expect(responseData.event_type.id).toEqual(eventTypeId); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData()).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 } = createMocks({ + const { req, res } = createMocks({ method: "GET", body: {}, query: { @@ -118,10 +94,10 @@ describe("GET /api/event-types/[id]", () => { req.isSystemWideAdmin = false; req.userId = userId; + await handler(req, res); - const responseData = await handler(req); - - expect(responseData.event_type.id).toEqual(eventTypeId); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); expect(prismaMock.team.findFirst).toHaveBeenCalledWith({ where: { id: teamId, @@ -140,7 +116,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 } = createMocks({ + const { req, res } = createMocks({ method: "GET", body: {}, query: { @@ -158,10 +134,10 @@ describe("GET /api/event-types/[id]", () => { req.isSystemWideAdmin = false; req.userId = userId; + await handler(req, res); - const responseData = await handler(req); - - expect(responseData.event_type.id).toEqual(eventTypeId); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); expect(prismaMock.team.findFirst).not.toHaveBeenCalled(); }); }); diff --git a/apps/api/v2/src/modules/auth/guards/optional-api-auth/optional-api-auth.guard.ts b/apps/api/v2/src/modules/auth/guards/optional-api-auth/optional-api-auth.guard.ts index 8e3d070e084dcd..6cdd594daa115b 100644 --- a/apps/api/v2/src/modules/auth/guards/optional-api-auth/optional-api-auth.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/optional-api-auth/optional-api-auth.guard.ts @@ -2,7 +2,6 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { NO_AUTH_PROVIDED_MESSAGE, ONLY_CLIENT_ID_PROVIDED_MESSAGE, - ONLY_CLIENT_SECRET_PROVIDED_MESSAGE, } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; export class OptionalApiAuthGuard extends ApiAuthGuard { @@ -10,16 +9,11 @@ export class OptionalApiAuthGuard extends ApiAuthGuard { // note(Lauris): optional means that auth is not required but if it is invalid then still throw error. const noAuthProvided = error && error.message.includes(NO_AUTH_PROVIDED_MESSAGE); const onlyClientIdProvided = error && error.message.includes(ONLY_CLIENT_ID_PROVIDED_MESSAGE); - const onlyClientSecretProvided = error && error.message.includes(ONLY_CLIENT_SECRET_PROVIDED_MESSAGE); if (onlyClientIdProvided) { return null; } - if (onlyClientSecretProvided) { - return null; - } - if (user || noAuthProvided || !error) { return user || null; } else { diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts index 7d192121103352..72ba40b14e72df 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts @@ -32,11 +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, - ONLY_CLIENT_ID_PROVIDED_MESSAGE, - ONLY_CLIENT_SECRET_PROVIDED_MESSAGE, -} from "./api-auth.strategy"; +import { ApiAuthGuardRequest, ONLY_CLIENT_ID_PROVIDED_MESSAGE } from "./api-auth.strategy"; import { ApiAuthStrategy } from "./api-auth.strategy"; describe("ApiAuthStrategy", () => { @@ -271,29 +267,6 @@ describe("ApiAuthStrategy", () => { } }); - it("should throw 401 if only OAuth Client Secret is provided", async () => { - const context: ExecutionContext = { - switchToHttp: () => ({ - getRequest: () => ({ - headers: { - [X_CAL_SECRET_KEY]: `${oAuthClient.secret}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_SECRET_PROVIDED_MESSAGE); - } - } - }); - it("should throw 401 if OAuth ID is invalid", async () => { const context: ExecutionContext = { switchToHttp: () => ({ diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts index 9f24bea005ee5b..3b1ecb5378958d 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts @@ -29,11 +29,11 @@ export type ApiAuthGuardRequest = Request & { user: ApiAuthGuardUser; allowedAuthMethods?: AllowedAuthMethod[]; }; -export const NO_AUTH_PROVIDED_MESSAGE = `No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as '${X_CAL_SECRET_KEY}' and '${X_CAL_CLIENT_ID}' headers`; +export const NO_AUTH_PROVIDED_MESSAGE = + "No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers"; -export const ONLY_CLIENT_ID_PROVIDED_MESSAGE = `Only '${X_CAL_CLIENT_ID}' header provided. Please also provide '${X_CAL_SECRET_KEY}' header or Auth bearer token as 'Authentication' header`; - -export const ONLY_CLIENT_SECRET_PROVIDED_MESSAGE = `Only '${X_CAL_SECRET_KEY}' header provided. Please also provide '${X_CAL_CLIENT_ID}' header or Auth bearer token as 'Authentication' header`; +export const ONLY_CLIENT_ID_PROVIDED_MESSAGE = + "Only 'x-cal-client-id' header provided. Please also provide 'x-cal-secret-key' header or Auth bearer token as 'Authentication' header"; @Injectable() export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") { @@ -121,9 +121,6 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken; const onlyClientIdProvided = !!oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken; - const onlyClientSecretProvided = - !oAuthClientId && !!oAuthClientSecret && !bearerToken && !nextAuthToken; - if (noAuthProvided) { throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`); } @@ -132,10 +129,6 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_ID_PROVIDED_MESSAGE}`); } - if (onlyClientSecretProvided) { - throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_SECRET_PROVIDED_MESSAGE}`); - } - throw new UnauthorizedException( `ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${ allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method" diff --git a/apps/api/v2/src/modules/billing/billing.module.ts b/apps/api/v2/src/modules/billing/billing.module.ts index 8dc8c2213cbea7..7cf287b617d087 100644 --- a/apps/api/v2/src/modules/billing/billing.module.ts +++ b/apps/api/v2/src/modules/billing/billing.module.ts @@ -2,7 +2,6 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings import { BillingProcessor } from "@/modules/billing/billing.processor"; import { BillingRepository } from "@/modules/billing/billing.repository"; import { BillingController } from "@/modules/billing/controllers/billing.controller"; -import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy"; import { BillingConfigService } from "@/modules/billing/services/billing.config.service"; import { BillingService } from "@/modules/billing/services/billing.service"; import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service"; @@ -10,7 +9,6 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OrganizationsModule } from "@/modules/organizations/organizations.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { RedisModule } from "@/modules/redis/redis.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; import { UsersModule } from "@/modules/users/users.module"; import { BullModule } from "@nestjs/bull"; @@ -20,7 +18,6 @@ import { Module } from "@nestjs/common"; imports: [ PrismaModule, StripeModule, - RedisModule, MembershipsModule, OrganizationsModule, BullModule.registerQueue({ @@ -35,10 +32,6 @@ import { Module } from "@nestjs/common"; providers: [ BillingConfigService, BillingService, - { - provide: "IBillingService", - useClass: BillingServiceCachingProxy, - }, BillingRepository, BillingProcessor, ManagedOrganizationsBillingService, diff --git a/apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts b/apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts index ee4960ad851bab..12573cc344eb67 100644 --- a/apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts @@ -1,6 +1,5 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; -import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { StripeService } from "@/modules/stripe/stripe.service"; import { TokensModule } from "@/modules/tokens/tokens.module"; @@ -17,7 +16,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; -import { withNextAuth } from "test/utils/withNextAuth"; +import { withApiAuth } from "test/utils/withApiAuth"; import type { Team, PlatformBilling } from "@calcom/prisma/client"; @@ -32,8 +31,9 @@ describe("Platform Billing Controller (e2e)", () => { let membershipsRepositoryFixture: MembershipRepositoryFixture; let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture; let organization: Team; + beforeAll(async () => { - const moduleRef = await withNextAuth( + const moduleRef = await withApiAuth( userEmail, Test.createTestingModule({ imports: [AppModule, PrismaModule, UsersModule, TokensModule], @@ -44,10 +44,8 @@ describe("Platform Billing Controller (e2e)", () => { profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); - organization = await organizationsRepositoryFixture.create({ name: `billing-organization-${randomString()}`, - isPlatform: true, }); user = await userRepositoryFixture.create({ @@ -89,17 +87,6 @@ describe("Platform Billing Controller (e2e)", () => { await app.close(); }); - it("/billing/webhook (GET) should not get billing plan for org since it's not set yet", () => { - return request(app.getHttpServer()) - .get(`/v2/billing/${organization.id}/check`) - - .expect(200) - .then(async (res) => { - const data = res.body.data as CheckPlatformBillingResponseDto; - expect(data?.plan).toEqual("FREE"); - }); - }); - it("/billing/webhook (POST) should set billing free plan for org", () => { jest.spyOn(StripeService.prototype, "getStripe").mockImplementation( () => @@ -132,17 +119,6 @@ describe("Platform Billing Controller (e2e)", () => { expect(billing?.plan).toEqual("FREE"); }); }); - - it("/billing/webhook (GET) should get billing plan for org", () => { - return request(app.getHttpServer()) - .get(`/v2/billing/${organization.id}/check`) - .expect(200) - .then(async (res) => { - const data = res.body.data as CheckPlatformBillingResponseDto; - expect(data?.plan).toEqual("FREE"); - }); - }); - it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => { jest.spyOn(StripeService.prototype, "getStripe").mockImplementation( () => diff --git a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts index 572d29f666fb18..783e39e5675293 100644 --- a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts +++ b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts @@ -6,7 +6,7 @@ import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input"; import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto"; import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto"; -import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface"; +import { BillingService } from "@/modules/billing/services/billing.service"; import { StripeService } from "@/modules/stripe/stripe.service"; import { Body, @@ -19,7 +19,6 @@ import { Headers, HttpCode, HttpStatus, - Inject, Logger, Delete, ParseIntPipe, @@ -41,7 +40,7 @@ export class BillingController { private logger = new Logger("Billing Controller"); constructor( - @Inject("IBillingService") private readonly billingService: IBillingService, + private readonly billingService: BillingService, public readonly stripeService: StripeService, private readonly configService: ConfigService ) { diff --git a/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts b/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts deleted file mode 100644 index 776e191476fd56..00000000000000 --- a/apps/api/v2/src/modules/billing/interfaces/billing-service.interface.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PlatformPlan } from "@/modules/billing/types"; -import type { StripeService } from "@/modules/stripe/stripe.service"; -import Stripe from "stripe"; -import { PlatformBilling, Team } from "@calcom/prisma/client"; - -export type BillingData = { - team: (Team & { platformBilling: PlatformBilling | null }) | null; - status: "valid" | "no_subscription" | "no_billing"; - plan: string; -}; - -export interface IBillingService { - getBillingData(teamId: number): Promise; - createTeamBilling(teamId: number): Promise; - redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string): Promise; - updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise; - cancelTeamSubscription(teamId: number): Promise; - handleStripeSubscriptionDeleted(event: Stripe.Event): Promise; - handleStripePaymentSuccess(event: Stripe.Event): Promise; - handleStripePaymentFailed(event: Stripe.Event): Promise; - handleStripePaymentPastDue(event: Stripe.Event): Promise; - handleStripeCheckoutEvents(event: Stripe.Event): Promise; - handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise; - getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null; - getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null; - stripeService: StripeService; -} diff --git a/apps/api/v2/src/modules/billing/services/billing-service-caching-proxy.ts b/apps/api/v2/src/modules/billing/services/billing-service-caching-proxy.ts deleted file mode 100644 index 7e45eff371f383..00000000000000 --- a/apps/api/v2/src/modules/billing/services/billing-service-caching-proxy.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { IBillingService, BillingData } from "@/modules/billing/interfaces/billing-service.interface"; -import { BillingService } from "@/modules/billing/services/billing.service"; -import { PlatformPlan } from "@/modules/billing/types"; -import { RedisService } from "@/modules/redis/redis.service"; -import { Injectable } from "@nestjs/common"; -import Stripe from "stripe"; - -export const REDIS_BILLING_CACHE_KEY = (teamId: number) => `apiv2:team:${teamId}:billing`; -export const BILLING_CACHE_TTL_MS = 3_600_000; // 1 hour - -@Injectable() -export class BillingServiceCachingProxy implements IBillingService { - constructor(private readonly billingService: BillingService, private readonly redisService: RedisService) {} - - async getBillingData(teamId: number) { - const cachedBillingData = await this.getBillingCache(teamId); - if (cachedBillingData) { - return cachedBillingData; - } - - const billingData = await this.billingService.getBillingData(teamId); - await this.setBillingCache(teamId, billingData); - return billingData; - } - - private async deleteBillingCache(teamId: number) { - await this.redisService.del(REDIS_BILLING_CACHE_KEY(teamId)); - } - - private async getBillingCache(teamId: number) { - const cachedResult = await this.redisService.get(REDIS_BILLING_CACHE_KEY(teamId)); - return cachedResult; - } - - private async setBillingCache(teamId: number, billingData: BillingData): Promise { - await this.redisService.set(REDIS_BILLING_CACHE_KEY(teamId), billingData, { - ttl: BILLING_CACHE_TTL_MS, - }); - } - - async createTeamBilling(teamId: number): Promise { - return this.billingService.createTeamBilling(teamId); - } - - async redirectToSubscribeCheckout( - teamId: number, - plan: PlatformPlan, - customerId?: string - ): Promise { - return this.billingService.redirectToSubscribeCheckout(teamId, plan, customerId); - } - - async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise { - return this.billingService.updateSubscriptionForTeam(teamId, plan); - } - - async cancelTeamSubscription(teamId: number): Promise { - await this.billingService.cancelTeamSubscription(teamId); - await this.deleteBillingCache(teamId); - } - - async handleStripeSubscriptionDeleted(event: Stripe.Event): Promise { - await this.billingService.handleStripeSubscriptionDeleted(event); - const subscription = event.data.object as Stripe.Subscription; - const teamId = subscription?.metadata?.teamId; - if (teamId) { - await this.deleteBillingCache(Number.parseInt(teamId)); - } - } - - async handleStripePaymentSuccess(event: Stripe.Event): Promise { - await this.billingService.handleStripePaymentSuccess(event); - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - if (subscriptionId) { - const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId( - subscriptionId - ); - if (teamBilling?.id) { - await this.deleteBillingCache(teamBilling.id); - } - } - } - - async handleStripePaymentFailed(event: Stripe.Event): Promise { - await this.billingService.handleStripePaymentFailed(event); - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = this.getSubscriptionIdFromInvoice(invoice); - if (subscriptionId) { - const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId( - subscriptionId - ); - if (teamBilling?.id) { - await this.deleteBillingCache(teamBilling.id); - } - } - } - - async handleStripePaymentPastDue(event: Stripe.Event): Promise { - await this.billingService.handleStripePaymentPastDue(event); - const subscription = event.data.object as Stripe.Subscription; - const subscriptionId = subscription.id; - if (subscriptionId) { - const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId( - subscriptionId - ); - if (teamBilling?.id) { - await this.deleteBillingCache(teamBilling.id); - } - } - } - - async handleStripeCheckoutEvents(event: Stripe.Event): Promise { - await this.billingService.handleStripeCheckoutEvents(event); - const checkoutSession = event.data.object as Stripe.Checkout.Session; - const teamId = checkoutSession.metadata?.teamId; - if (teamId) { - await this.deleteBillingCache(Number.parseInt(teamId)); - } - } - - async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise { - return this.billingService.handleStripeSubscriptionForActiveManagedUsers(event); - } - - getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null { - return this.billingService.getSubscriptionIdFromInvoice(invoice); - } - - getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null { - return this.billingService.getCustomerIdFromInvoice(invoice); - } - - get stripeService() { - return this.billingService.stripeService; - } -} diff --git a/apps/api/v2/src/modules/billing/services/billing.service.ts b/apps/api/v2/src/modules/billing/services/billing.service.ts index 0d5ffcf3163f24..477883ef1409aa 100644 --- a/apps/api/v2/src/modules/billing/services/billing.service.ts +++ b/apps/api/v2/src/modules/billing/services/billing.service.ts @@ -2,7 +2,6 @@ import { AppConfig } from "@/config/type"; import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor"; import { BillingRepository } from "@/modules/billing/billing.repository"; -import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface"; import { BillingConfigService } from "@/modules/billing/services/billing.config.service"; import { PlatformPlan } from "@/modules/billing/types"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; @@ -24,14 +23,14 @@ import { DateTime } from "luxon"; import Stripe from "stripe"; @Injectable() -export class BillingService implements IBillingService, OnModuleDestroy { +export class BillingService implements OnModuleDestroy { private logger = new Logger("BillingService"); private readonly webAppUrl: string; constructor( private readonly teamsRepository: OrganizationsRepository, public readonly stripeService: StripeService, - public readonly billingRepository: BillingRepository, + private readonly billingRepository: BillingRepository, private readonly configService: ConfigService, private readonly billingConfigService: BillingConfigService, private readonly usersRepository: UsersRepository, @@ -44,23 +43,18 @@ export class BillingService implements IBillingService, OnModuleDestroy { async getBillingData(teamId: number) { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - if (teamWithBilling?.platformBilling) { if (!teamWithBilling?.platformBilling.subscriptionId) { - return { team: teamWithBilling, status: "no_subscription" as const, plan: "none" }; - } else { - return { - team: teamWithBilling, - status: "valid" as const, - plan: teamWithBilling.platformBilling.plan, - }; + return { team: teamWithBilling, status: "no_subscription", plan: "none" }; } + + return { team: teamWithBilling, status: "valid", plan: teamWithBilling.platformBilling.plan }; } else { - return { team: teamWithBilling, status: "no_billing" as const, plan: "none" }; + return { team: teamWithBilling, status: "no_billing", plan: "none" }; } } - async createTeamBilling(teamId: number): Promise { + async createTeamBilling(teamId: number) { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); let customerId = teamWithBilling?.platformBilling?.customerId; @@ -73,7 +67,7 @@ export class BillingService implements IBillingService, OnModuleDestroy { }); } - return customerId!; + return customerId; } async redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string) { diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-admin-not-team-member-event-types.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.e2e-spec.ts similarity index 99% rename from apps/api/v2/src/modules/organizations/event-types/organizations-admin-not-team-member-event-types.e2e-spec.ts rename to apps/api/v2/src/modules/organizations/event-types/organizations-event-types.e2e-spec.ts index 792618a6fe58b4..d33e9c12e5486a 100644 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-admin-not-team-member-event-types.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types.e2e-spec.ts @@ -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 but not team admin", () => { + describe("User Authentication - User is Org Admin", () => { let app: INestApplication; let userRepositoryFixture: UserRepositoryFixture; diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts deleted file mode 100644 index 43edaf2530ec97..00000000000000 --- a/apps/api/v2/src/modules/organizations/event-types/organizations-member-team-admin-event-types.e2e-spec.ts +++ /dev/null @@ -1,1238 +0,0 @@ -import { bootstrap } from "@/app"; -import { AppModule } from "@/app.module"; -import { PrismaModule } from "@/modules/prisma/prisma.module"; -import { TokensModule } from "@/modules/tokens/tokens.module"; -import { UsersModule } from "@/modules/users/users.module"; -import { INestApplication } from "@nestjs/common"; -import { NestExpressApplication } from "@nestjs/platform-express"; -import { Test } from "@nestjs/testing"; -import * as request from "supertest"; -import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; -import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; -import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; -import { randomString } from "test/utils/randomString"; -import { withApiAuth } from "test/utils/withApiAuth"; - -import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants"; -import { - BookingWindowPeriodInputTypeEnum_2024_06_14, - BookerLayoutsInputEnum_2024_06_14, - ConfirmationPolicyEnum, - NoticeThresholdUnitEnum, -} from "@calcom/platform-enums"; -import { SchedulingType } from "@calcom/platform-libraries"; -import type { - ApiSuccessResponse, - CreateTeamEventTypeInput_2024_06_14, - EventTypeOutput_2024_06_14, - Host, - TeamEventTypeOutput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, -} from "@calcom/platform-types"; -import type { User, Team } from "@calcom/prisma/client"; - -describe("Organizations Event Types Endpoints", () => { - describe("User Authentication - User is Org member and team admin", () => { - let app: INestApplication; - - let userRepositoryFixture: UserRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let teamsRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - - let org: Team; - let team: Team; - let falseTestOrg: Team; - let falseTestTeam: Team; - - const userEmail = `organizations-event-types-admin-${randomString()}@api.com`; - let userAdmin: User; - - const teammate1Email = `organizations-event-types-teammate1-${randomString()}@api.com`; - const teammate2Email = `organizations-event-types-teammate2-${randomString()}@api.com`; - const falseTestUserEmail = `organizations-event-types-false-user-${randomString()}@api.com`; - let teammate1: User; - let teammate2: User; - let falseTestUser: User; - - let collectiveEventType: TeamEventTypeOutput_2024_06_14; - let managedEventType: TeamEventTypeOutput_2024_06_14; - - const managedEventTypeSlug = `organizations-event-types-managed-${randomString()}`; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - - userAdmin = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - role: "ADMIN", - }); - - teammate1 = await userRepositoryFixture.create({ - email: teammate1Email, - username: teammate1Email, - name: "alice", - }); - - teammate2 = await userRepositoryFixture.create({ - email: teammate2Email, - username: teammate2Email, - name: "bob", - }); - - falseTestUser = await userRepositoryFixture.create({ - email: falseTestUserEmail, - username: falseTestUserEmail, - }); - - org = await organizationsRepositoryFixture.create({ - name: `organizations-event-types-organization-${randomString()}`, - isOrganization: true, - slug: `organizations-event-types-organization-${randomString()}`, - }); - - falseTestOrg = await organizationsRepositoryFixture.create({ - name: `organizations-event-types-false-org-${randomString()}`, - isOrganization: true, - }); - - team = await teamsRepositoryFixture.create({ - name: `organizations-event-types-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); - - falseTestTeam = await teamsRepositoryFixture.create({ - name: `organizations-event-types-false-team-${randomString()}`, - isOrganization: false, - parent: { connect: { id: falseTestOrg.id } }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${userAdmin.id}`, - username: userEmail, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: userAdmin.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teammate1.id}`, - username: teammate1Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: teammate1.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teammate2.id}`, - username: teammate2Email, - organization: { - connect: { - id: org.id, - }, - }, - user: { - connect: { - id: teammate2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: org.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: userAdmin.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate1.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teammate2.id } }, - team: { connect: { id: team.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: falseTestUser.id } }, - team: { connect: { id: falseTestTeam.id } }, - accepted: true, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(userAdmin).toBeDefined(); - expect(org).toBeDefined(); - }); - - it("should not be able to create event-type for team outside org", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - { - type: "organizersDefaultApp", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types`) - .send(body) - .expect(403); - }); - - it("should not be able to create event-type for user outside org", async () => { - const userId = falseTestUser.id; - - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation", - slug: "coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId, - mandatory: true, - priority: "high", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(404); - }); - - it("should create a collective team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234", - title: "Coding consultation collective", - slug: `organizations-event-types-collective-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - bookingFields: [ - { - type: "select", - label: "select which language is your codebase in", - slug: "select-language", - required: true, - placeholder: "select language", - options: ["javascript", "python", "cobol"], - }, - ], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - schedulingType: "collective", - hosts: [ - { - userId: teammate1.id, - }, - { - userId: teammate2.id, - }, - ], - bookingLimitsCount: { - day: 2, - week: 5, - }, - onlyShowFirstAvailableSlot: true, - bookingLimitsDuration: { - day: 60, - week: 100, - }, - offsetStart: 30, - bookingWindow: { - type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, - value: 30, - rolling: true, - }, - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - requiresBookerEmailVerification: true, - hideCalendarNotes: true, - hideCalendarEventDetails: true, - hideOrganizerEmail: true, - lockTimeZoneToggleOnBookingPage: true, - color: { - darkThemeHex: "#292929", - lightThemeHex: "#fafafa", - }, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("collective"); - evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]); - evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]); - expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); - expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); - expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); - expect(data.offsetStart).toEqual(body.offsetStart); - expect(data.bookingWindow).toEqual(body.bookingWindow); - expect(data.bookerLayouts).toEqual(body.bookerLayouts); - expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); - expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); - expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); - expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); - expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail); - expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); - expect(data.color).toEqual(body.color); - expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234"); - collectiveEventType = responseBody.data; - }); - }); - - it("should create a managed team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation managed", - slug: managedEventTypeSlug, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - { - userId: teammate2.id, - mandatory: false, - priority: "low", - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(3); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate1EventTypes[0].slug).toEqual(managedEventTypeSlug); - expect(teammate2EventTypes.length).toEqual(1); - expect(teammate2EventTypes[0].slug).toEqual(managedEventTypeSlug); - expect(teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED").length).toEqual( - 1 - ); - - const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.hosts).toEqual([ - { - userId: teammate1.id, - name: teammate1.name, - username: teammate1.username, - avatarUrl: teammate1.avatarUrl, - }, - { - userId: teammate2.id, - name: teammate2.name, - username: teammate2.username, - avatarUrl: teammate2.avatarUrl, - }, - ]); - - if (!responseTeamEvent) { - throw new Error("Team event not found"); - } - - const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teammate1.id); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teammate2.id); - expect(responseTeammate2Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - managedEventType = responseTeamEvent; - }); - }); - - it("managed team event types should be returned when fetching event types of users", async () => { - return request(app.getHttpServer()) - .get(`/v2/event-types?username=${teammate1.username}&orgSlug=${org.slug}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].slug).toEqual(managedEventTypeSlug); - expect(data[0].ownerId).toEqual(teammate1.id); - expect(data[0].id).not.toEqual(managedEventType.id); - }); - }); - - it("managed team event type should be returned when fetching event types of users", async () => { - return request(app.getHttpServer()) - .get( - `/v2/event-types?username=${teammate1.username}&orgSlug=${org.slug}&eventSlug=${managedEventTypeSlug}` - ) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].slug).toEqual(managedEventTypeSlug); - expect(data[0].ownerId).toEqual(teammate1.id); - expect(data[0].id).not.toEqual(managedEventType.id); - }); - }); - - it("should not get an event-type of team outside org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .expect(403); - }); - - it("should not get a non existing event-type", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`) - .expect(404); - }); - - it("should get a team event-type", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(collectiveEventType.title); - expect(data.hosts.length).toEqual(2); - evaluateHost(collectiveEventType.hosts[0], data.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], data.hosts[1]); - - collectiveEventType = responseBody.data; - }); - }); - - it("should not get event-types of team outside org", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types`) - .expect(404); - }); - - it("should get team event-types", async () => { - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective"); - const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed"); - - expect(eventTypeCollective?.title).toEqual(collectiveEventType.title); - expect(eventTypeCollective?.hosts.length).toEqual(2); - - expect(eventTypeManaged?.title).toEqual(managedEventType.title); - expect(eventTypeManaged?.hosts.length).toEqual(2); - evaluateHost(collectiveEventType.hosts[0], eventTypeCollective?.hosts[0]); - evaluateHost(collectiveEventType.hosts[1], eventTypeCollective?.hosts[1]); - }); - }); - - it("should not be able to update event-type for incorrect team", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Clean code consultation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(403); - }); - - it("should not be able to update non existing event-type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: "Clean code consultation", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`) - .send(body) - .expect(400); - }); - - it("should update collective event-type", async () => { - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teammate1.id, - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - hosts: newHosts, - successRedirectUrl: "https://new-url-success.com", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(200) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const eventType = responseBody.data; - expect(eventType.successRedirectUrl).toEqual("https://new-url-success.com"); - expect(eventType.title).toEqual(collectiveEventType.title); - expect(eventType.hosts.length).toEqual(1); - evaluateHost(eventType.hosts[0], newHosts[0]); - }); - }); - - it("should update managed event-type", async () => { - const newTitle = "Coding consultation managed updated"; - const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ - { - userId: teammate1.id, - mandatory: true, - priority: "medium", - }, - ]; - - const body: UpdateTeamEventTypeInput_2024_06_14 = { - title: newTitle, - hosts: newHosts, - successRedirectUrl: "https://new-url-success-managed.com", - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(2); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate1EventTypes[0].title).toEqual(newTitle); - expect(teammate2EventTypes.length).toEqual(0); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(false); - expect( - teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED")?.[0]?.title - ).toEqual(newTitle); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.title).toEqual(newTitle); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(false); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.title).toEqual(newTitle); - - managedEventType = responseBody.data[0]; - expect(managedEventType.successRedirectUrl).toEqual("https://new-url-success-managed.com"); - }); - }); - - it("should be able to create phone-only event type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Phone coding consultation", - slug: "phone-coding-consultation", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - { - type: "organizersDefaultApp", - }, - ], - schedulingType: "COLLECTIVE", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - ], - bookingFields: [ - { - type: "email", - required: false, - label: "Email", - hidden: true, - }, - { - type: "phone", - slug: "attendeePhoneNumber", - required: true, - label: "Phone number", - hidden: false, - }, - ], - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data.bookingFields).toEqual([ - { - isDefault: true, - type: "name", - slug: "name", - label: "your_name", - required: true, - disableOnPrefill: false, - }, - { - isDefault: true, - type: "email", - slug: "email", - required: false, - label: "Email", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: true, - hidden: false, - label: "Phone number", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "text", - slug: "title", - required: true, - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "textarea", - slug: "notes", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "multiemail", - slug: "guests", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "textarea", - slug: "rescheduleReason", - required: false, - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should be able to configure phone-only event type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - bookingFields: [ - { - type: "email", - required: false, - label: "Email", - hidden: true, - }, - { - type: "phone", - slug: "attendeePhoneNumber", - required: true, - label: "Phone number", - hidden: false, - }, - ], - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - const data = responseBody.data; - expect(data.bookingFields).toEqual([ - { - isDefault: true, - type: "name", - slug: "name", - required: true, - label: "your_name", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "email", - slug: "email", - required: false, - label: "Email", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: true, - hidden: false, - label: "Phone number", - disableOnPrefill: false, - }, - { - isDefault: true, - type: "text", - slug: "title", - required: true, - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - type: "textarea", - slug: "notes", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "multiemail", - slug: "guests", - required: false, - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - type: "textarea", - slug: "rescheduleReason", - required: false, - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should assign all members to managed event-type", async () => { - const body: UpdateTeamEventTypeInput_2024_06_14 = { - assignAllTeamMembers: true, - }; - - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .send(body) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(4); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - const managedTeamEventTypes = teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" - ); - - expect(teammate1EventTypes.length).toEqual(1); - expect(teammate2EventTypes.length).toEqual(1); - expect(managedTeamEventTypes.length).toEqual(1); - expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true); - - const responseTeamEvent = responseBody.data.find( - (eventType) => eventType.schedulingType === "managed" - ); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.teamId).toEqual(team.id); - expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true); - - const responseTeammate1Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate1.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - const responseTeammate2Event = responseBody.data.find( - (eventType) => eventType.ownerId === teammate2.id - ); - expect(responseTeammate1Event).toBeDefined(); - expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); - - if (responseTeamEvent) { - managedEventType = responseTeamEvent; - } - }); - }); - - it("should not delete event-type of team outside org", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${falseTestTeam.id}/event-types/${collectiveEventType.id}`) - .expect(403); - }); - - it("should delete event-type not part of the team", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/99999`) - .expect(404); - }); - - it("should delete collective event-type", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${collectiveEventType.id}`) - .expect(200); - }); - - it("should delete managed event-type", async () => { - return request(app.getHttpServer()) - .delete(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${managedEventType.id}`) - .expect(200); - }); - - it("should return event type with default bookingFields if they are not defined", async () => { - const eventTypeInput = { - title: "unknown field event type two", - slug: `organizations-event-types-unknown-${randomString()}`, - description: "unknown field event type description two", - length: 40, - hidden: false, - locations: [], - schedulingType: SchedulingType.ROUND_ROBIN, - }; - const eventType = await eventTypesRepositoryFixture.createTeamEventType({ - ...eventTypeInput, - team: { connect: { id: team.id } }, - }); - - return request(app.getHttpServer()) - .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}`) - .expect(200) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - const fetchedEventType = responseBody.data; - - expect(fetchedEventType.bookingFields).toEqual([ - { isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false }, - { - isDefault: true, - required: true, - slug: "email", - type: "email", - disableOnPrefill: false, - hidden: false, - }, - { - disableOnPrefill: false, - isDefault: true, - type: "phone", - slug: "attendeePhoneNumber", - required: false, - hidden: true, - }, - { - isDefault: true, - type: "radioInput", - slug: "location", - required: false, - hidden: false, - }, - { - isDefault: true, - required: true, - slug: "title", - type: "text", - disableOnPrefill: false, - hidden: true, - }, - { - isDefault: true, - required: false, - slug: "notes", - type: "textarea", - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - required: false, - slug: "guests", - type: "multiemail", - disableOnPrefill: false, - hidden: false, - }, - { - isDefault: true, - required: false, - slug: "rescheduleReason", - type: "textarea", - disableOnPrefill: false, - hidden: false, - }, - ]); - }); - }); - - it("should create a round robin team event-type", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234", - title: "Coding consultation round robin", - slug: `organizations-event-types-round-robin-${randomString()}`, - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - bookingFields: [ - { - type: "select", - label: "select which language is your codebase in", - slug: "select-language", - required: true, - placeholder: "select language", - options: ["javascript", "python", "cobol"], - }, - ], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - schedulingType: "roundRobin", - hosts: [ - { - userId: teammate1.id, - mandatory: true, - priority: "high", - }, - { - userId: teammate2.id, - mandatory: false, - priority: "medium", - }, - ], - bookingLimitsCount: { - day: 2, - week: 5, - }, - onlyShowFirstAvailableSlot: true, - bookingLimitsDuration: { - day: 60, - week: 100, - }, - offsetStart: 30, - bookingWindow: { - type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, - value: 30, - rolling: true, - }, - bookerLayouts: { - enabledLayouts: [ - BookerLayoutsInputEnum_2024_06_14.column, - BookerLayoutsInputEnum_2024_06_14.month, - BookerLayoutsInputEnum_2024_06_14.week, - ], - defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, - }, - - confirmationPolicy: { - type: ConfirmationPolicyEnum.TIME, - noticeThreshold: { - count: 60, - unit: NoticeThresholdUnitEnum.MINUTES, - }, - blockUnconfirmedBookingsInBooker: true, - }, - requiresBookerEmailVerification: true, - hideCalendarNotes: true, - hideCalendarEventDetails: true, - hideOrganizerEmail: true, - lockTimeZoneToggleOnBookingPage: true, - color: { - darkThemeHex: "#292929", - lightThemeHex: "#fafafa", - }, - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then((response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.title).toEqual(body.title); - expect(data.hosts.length).toEqual(2); - expect(data.schedulingType).toEqual("roundRobin"); - evaluateHost(body.hosts?.[0] || { userId: -1 }, data.hosts[0]); - evaluateHost(body.hosts?.[1] || { userId: -1 }, data.hosts[1]); - expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); - expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); - expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); - expect(data.offsetStart).toEqual(body.offsetStart); - expect(data.bookingWindow).toEqual(body.bookingWindow); - expect(data.bookerLayouts).toEqual(body.bookerLayouts); - expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); - expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); - expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); - expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); - expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail); - expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); - expect(data.color).toEqual(body.color); - expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234"); - collectiveEventType = responseBody.data; - }); - }); - - it("should create a managed team event-type without hosts", async () => { - const body: CreateTeamEventTypeInput_2024_06_14 = { - title: "Coding consultation managed without hosts", - slug: "coding-consultation-managed-without-hosts", - description: "Our team will review your codebase.", - lengthInMinutes: 60, - locations: [ - { - type: "integration", - integration: "cal-video", - }, - ], - schedulingType: "MANAGED", - }; - - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`) - .send(body) - .expect(201) - .then(async (response) => { - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - - const data = responseBody.data; - expect(data.length).toEqual(1); - - const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate1.id); - const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teammate2.id); - const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); - - const teammate1HasThisEvent = teammate1EventTypes.some((eventType) => eventType.slug === body.slug); - const teammate2HasThisEvent = teammate2EventTypes.some((eventType) => eventType.slug === body.slug); - expect(teammate1HasThisEvent).toBe(false); - expect(teammate2HasThisEvent).toBe(false); - expect( - teamEventTypes.filter( - (eventType) => eventType.schedulingType === "MANAGED" && eventType.slug === body.slug - ).length - ).toEqual(1); - - const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); - expect(responseTeamEvent).toBeDefined(); - expect(responseTeamEvent?.hosts).toEqual([]); - - if (!responseTeamEvent) { - throw new Error("Team event not found"); - } - }); - }); - - function evaluateHost(expected: Host, received: Host | undefined) { - expect(expected.userId).toEqual(received?.userId); - expect(expected.mandatory).toEqual(received?.mandatory); - expect(expected.priority).toEqual(received?.priority); - } - - afterAll(async () => { - await userRepositoryFixture.deleteByEmail(userAdmin.email); - await userRepositoryFixture.deleteByEmail(teammate1.email); - await userRepositoryFixture.deleteByEmail(teammate2.email); - await userRepositoryFixture.deleteByEmail(falseTestUser.email); - await teamsRepositoryFixture.delete(team.id); - await teamsRepositoryFixture.delete(falseTestTeam.id); - await organizationsRepositoryFixture.delete(org.id); - await organizationsRepositoryFixture.delete(falseTestOrg.id); - await app.close(); - }); - }); -}); diff --git a/apps/api/v2/src/modules/organizations/index/organizations.repository.ts b/apps/api/v2/src/modules/organizations/index/organizations.repository.ts index 00035ca40f5af3..5c66a4dabc6160 100644 --- a/apps/api/v2/src/modules/organizations/index/organizations.repository.ts +++ b/apps/api/v2/src/modules/organizations/index/organizations.repository.ts @@ -174,14 +174,4 @@ export class OrganizationsRepository { }, }); } - - async findTeamByPlatformBillingId(billingId: number) { - return this.dbRead.prisma.team.findFirst({ - where: { - platformBilling: { - id: billingId, - }, - }, - }); - } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 8ec361f72552b6..862daa8e20db14 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -16780,7 +16780,7 @@ "bookingRequiresAuthentication": { "type": "boolean", "default": false, - "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." + "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type." }, "locations": { "type": "array", @@ -18449,7 +18449,7 @@ }, "bookingRequiresAuthentication": { "type": "boolean", - "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." + "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type." }, "ownerId": { "type": "number", @@ -18808,7 +18808,7 @@ "bookingRequiresAuthentication": { "type": "boolean", "default": false, - "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." + "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type." }, "locations": { "type": "array", @@ -20644,7 +20644,7 @@ "bookingRequiresAuthentication": { "type": "boolean", "default": false, - "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." + "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type." }, "schedulingType": { "type": "string", @@ -21085,7 +21085,7 @@ }, "bookingRequiresAuthentication": { "type": "boolean", - "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." + "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type." }, "teamId": { "type": "number" @@ -21539,7 +21539,7 @@ "bookingRequiresAuthentication": { "type": "boolean", "default": false, - "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." + "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type." }, "hosts": { "type": "array", diff --git a/apps/api/v2/test/fixtures/repository/billing.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/billing.repository.fixture.ts index 16517887db625d..e53709519cf3e4 100644 --- a/apps/api/v2/test/fixtures/repository/billing.repository.fixture.ts +++ b/apps/api/v2/test/fixtures/repository/billing.repository.fixture.ts @@ -18,7 +18,7 @@ export class PlatformBillingRepositoryFixture { id: orgId, customerId: `cus_123_${randomString}`, subscriptionId: `sub_123_${randomString}`, - plan: plan || "FREE", + plan: plan || "STARTER", }, }); } diff --git a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts index 759b76de20a8d8..8b6f09836f3849 100644 --- a/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts +++ b/apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/queries.ts @@ -134,7 +134,7 @@ export async function getCRMData( if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) { const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import( - "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm" + "@calcom/lib/server/getTeamMemberEmailFromCrm" ); const { email, diff --git a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/RouteBuilder.tsx b/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/RouteBuilder.tsx index 2bc7ccfd121a13..cb514c91818669 100644 --- a/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/RouteBuilder.tsx +++ b/apps/web/app/(use-page-wrapper)/apps/routing-forms/[...pages]/RouteBuilder.tsx @@ -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"; diff --git a/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx b/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx index 116c8ff97f3d16..43f400905cee6b 100644 --- a/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx +++ b/apps/web/app/(use-page-wrapper)/event-types/[type]/page.tsx @@ -8,10 +8,6 @@ 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"; @@ -44,95 +40,6 @@ 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) { @@ -152,10 +59,7 @@ const ServerPage = async ({ params }: PageProps) => { throw new Error("This event type does not exist"); } - // Fetch permissions for the event type's team - const permissions = await getEventPermissions(session.user.id, data.eventType.teamId); - - return ; + return ; }; export default ServerPage; diff --git a/apps/web/app/(use-page-wrapper)/insights/checkInsightsPagePermission.ts b/apps/web/app/(use-page-wrapper)/insights/checkInsightsPagePermission.ts index cb86e9e3913ec6..49a5af32fc81b5 100644 --- a/apps/web/app/(use-page-wrapper)/insights/checkInsightsPagePermission.ts +++ b/apps/web/app/(use-page-wrapper)/insights/checkInsightsPagePermission.ts @@ -3,6 +3,7 @@ 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"; @@ -20,5 +21,14 @@ 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; } diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx index 88954141e9e5eb..632863962e1777 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx @@ -101,7 +101,10 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { name: "privacy", href: "/settings/organizations/privacy", }, - + { + name: "billing", + href: "/settings/organizations/billing", + }, { name: "OAuth Clients", href: "/settings/organizations/platform/oauth-clients" }, { name: "SSO", @@ -170,11 +173,17 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { // The following keys are assigned to admin only const adminRequiredKeys = ["admin"]; const organizationRequiredKeys = ["organization"]; -const organizationAdminKeys = ["privacy", "OAuth Clients", "SSO", "directory_sync", "delegation_credential"]; +const organizationAdminKeys = [ + "privacy", + "billing", + "OAuth Clients", + "SSO", + "directory_sync", + "delegation_credential", +]; export interface SettingsPermissions { canViewRoles?: boolean; - canViewOrganizationBilling?: boolean; } const useTabs = ({ @@ -223,27 +232,11 @@ const useTabs = ({ // Add pbac menu item only if feature flag is enabled AND user has permission to view roles // This prevents showing the menu item when user has no organization permissions - if (isPbacEnabled) { - if (permissions?.canViewRoles) { - newArray.push({ - name: "roles_and_permissions", - href: "/settings/organizations/roles", - }); - } - - if (permissions?.canViewOrganizationBilling) { - newArray.push({ - name: "billing", - href: "/settings/organizations/billing", - }); - } - } else { - if (isOrgAdminOrOwner) { - newArray.push({ - name: "billing", - href: "/settings/organizations/billing", - }); - } + if (isPbacEnabled && permissions?.canViewRoles) { + newArray.push({ + name: "roles_and_permissions", + href: "/settings/organizations/roles", + }); } return { @@ -279,7 +272,7 @@ const useTabs = ({ if (isAdmin) return true; return !adminRequiredKeys.includes(tab.name); }); - }, [isAdmin, orgBranding, isOrgAdminOrOwner, user, isDelegationCredentialEnabled, isPbacEnabled, permissions]); + }, [isAdmin, orgBranding, isOrgAdminOrOwner, user, isDelegationCredentialEnabled]); return processTabsMemod; }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx index 4626d80c511fea..93174bcd5f9051 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx @@ -6,6 +6,7 @@ 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"; @@ -25,10 +26,11 @@ const WebhooksViewServerWrapper = async () => { redirect("/auth/login"); } + const isAdmin = session.user.role === UserPermissionRole.ADMIN; const caller = await createRouterCaller(webhookRouter); const data = await caller.getByViewer(); - return ; + return ; }; export default WebhooksViewServerWrapper; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx index 56fc2baf9365d9..0f1d7ead7960ee 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx @@ -3,12 +3,11 @@ import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import React from "react"; -import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import type { TeamFeatures } from "@calcom/features/flags/config"; import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper"; -import { Resource, CrudAction, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry"; +import { Resource, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry"; import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { prisma } from "@calcom/prisma"; @@ -46,16 +45,13 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) { let teamFeatures: Record | null = null; let canViewRoles = false; - let canViewOrganizationBilling = false; const orgId = session?.user?.profile?.organizationId ?? session?.user.org?.id; // For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags if (orgId) { - const isOrgAdminOrOwner = checkAdminOrOwner(session.user.org?.role); - const [features, rolePermissions, organizationPermissions] = await Promise.all([ + const [features, rolePermissions] = await Promise.all([ getTeamFeatures(orgId), getCachedResourcePermissions(userId, orgId, Resource.Role), - getCachedResourcePermissions(userId, orgId, Resource.Organization), ]); if (features) { @@ -66,8 +62,6 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) { // Check if user has permission to read roles const roleActions = PermissionMapper.toActionMap(rolePermissions, Resource.Role); canViewRoles = roleActions[CrudAction.Read] ?? false; - const orgActions = PermissionMapper.toActionMap(organizationPermissions, Resource.Organization); - canViewOrganizationBilling = orgActions[CustomAction.ManageBilling] ?? isOrgAdminOrOwner; } } @@ -76,7 +70,7 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) { ); diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx index 2e84ad1f131580..4b4f2b3930a82f 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/billing/page.tsx @@ -2,11 +2,10 @@ import { _generateMetadata } from "app/_utils"; import { getTranslate } from "app/_utils"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; -import { MembershipRole } from "@calcom/prisma/enums"; import BillingView from "~/settings/billing/billing-view"; -import { validateUserHasOrgPerms } from "../../actions/validateUserHasOrgPerms"; +import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin"; export const generateMetadata = async () => await _generateMetadata( @@ -19,17 +18,15 @@ export const generateMetadata = async () => const Page = async () => { const t = await getTranslate(); + await validateUserHasOrgAdmin(); - await validateUserHasOrgPerms({ - permission: "organization.manageBilling", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); + // TODO(SEAN): Add PBAC to this page in the next PR return ( + borderInShellHeader={true}> ); diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/layout.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/layout.tsx index 6e66836533b990..05be9889d6bd3e 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/layout.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/layout.tsx @@ -1,4 +1,24 @@ +import { cookies, headers } from "next/headers"; +import { redirect } from "next/navigation"; + +import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; + const OrgAdminOnlyLayout = async ({ children }: { children: React.ReactNode }) => { + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + const userProfile = session?.user?.profile; + const userId = session?.user?.id; + const orgRole = + session?.user?.org?.role ?? + userProfile?.organization?.members.find((m: { userId: number }) => m.userId === userId)?.role; + const isOrgAdminOrOwner = checkAdminOrOwner(orgRole); + + if (!isOrgAdminOrOwner) { + return redirect("/settings/organizations/profile"); + } + return children; }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgPerms.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgPerms.tsx deleted file mode 100644 index f882640401c032..00000000000000 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/actions/validateUserHasOrgPerms.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { redirect } from "next/navigation"; - -import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import type { MembershipRole } from "@calcom/prisma/enums"; - -import { validateUserHasOrg } from "./validateUserHasOrg"; - -export const validateUserHasOrgPerms = async ({ - redirectTo, - fallbackRoles, - permission, -}: { - redirectTo?: string; - permission: PermissionString; - fallbackRoles: MembershipRole[]; -}) => { - const session = await validateUserHasOrg(); - - const permissionCheckService = new PermissionCheckService(); - - const hasPermission = await permissionCheckService.checkPermission({ - userId: session.user.id, - teamId: session.user.org.id, - permission, - fallbackRoles, - }); - - if (!hasPermission) { - redirect(redirectTo || "/settings/my-account/profile"); - } - - return session; -}; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx index 68360fdb30b210..df1053095435a2 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/RoleSheet.tsx @@ -107,9 +107,9 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga const { isAdvancedMode, permissions, color } = form.watch(); - const { filteredResources, scopedRegistry } = useMemo(() => { + const filteredResources = useMemo(() => { const scopedRegistry = getPermissionsForScope(scope); - const filteredResources = Object.keys(scopedRegistry).filter((resource) => + return Object.keys(scopedRegistry).filter((resource) => t( scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]] ?.i18nKey || "" @@ -117,7 +117,6 @@ 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({ @@ -213,6 +212,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga disabled={isSystemRole} /> + {filteredResources.map((resource) => ( - ))}{" "} + ))} ) : (
@@ -237,7 +237,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
- {Object.keys(scopedRegistry).map((resource) => ( + {Object.keys(getPermissionsForScope(scope)).map((resource) => ( ))} -
{" "} + )} diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx index e658890c3061f2..5ba1ec0d86114e 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/SimplePermissionItem.tsx @@ -1,11 +1,7 @@ "use client"; -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 type { Resource, Scope } from "@calcom/features/pbac/domain/types/permission-registry"; +import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { ToggleGroup } from "@calcom/ui/components/form"; @@ -25,13 +21,11 @@ export function SimplePermissionItem({ permissions, onChange, disabled, - scope = Scope.Organization, + scope, }: SimplePermissionItemProps) { const { t } = useLocale(); const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope); - const scopedRegistry = getPermissionsForScope(scope); - const registry = scopedRegistry || PERMISSION_REGISTRY; const isAllResources = resource === "*"; const options = isAllResources ? [ @@ -47,7 +41,7 @@ export function SimplePermissionItem({ return (
- {t(registry[resource as Resource]._resource?.i18nKey || resource)} + {t(PERMISSION_REGISTRY[resource as Resource]._resource?.i18nKey || resource)} { diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts index 176186a29fb22f..d7d5c2ea36c746 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/_components/usePermissions.ts @@ -1,5 +1,8 @@ import { CrudAction, Scope } from "@calcom/features/pbac/domain/types/permission-registry"; -import { getPermissionsForScope } from "@calcom/features/pbac/domain/types/permission-registry"; +import { + PERMISSION_REGISTRY, + getPermissionsForScope, +} from "@calcom/features/pbac/domain/types/permission-registry"; import { getTransitiveDependencies, getTransitiveDependents, @@ -49,8 +52,7 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission return permissions.includes("*.*") ? "all" : "none"; } - const scopedRegistry = getPermissionsForScope(scope); - const resourceConfig = scopedRegistry[resource as keyof typeof scopedRegistry]; + const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY]; if (!resourceConfig) return "none"; // Check if global all permissions (*.*) is present @@ -109,19 +111,7 @@ 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; } @@ -142,12 +132,15 @@ 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, scope); + const dependencies = getTransitiveDependencies(permission); dependencies.forEach((dependency) => { if (!newPermissions.includes(dependency)) { newPermissions.push(dependency); @@ -158,7 +151,7 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission newPermissions = newPermissions.filter((p) => p !== permission); // Remove all transitive dependents - const dependents = getTransitiveDependents(permission, scope); + const dependents = getTransitiveDependents(permission); dependents.forEach((dependent) => { newPermissions = newPermissions.filter((p) => p !== dependent); }); diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/roles/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/roles/page.tsx index 626148aeae327c..c89b0e2a62a8bd 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/roles/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[id]/roles/page.tsx @@ -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/features/ee/teams/lib/queries"; +import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; diff --git a/apps/web/app/api/auth/reset-password/route.ts b/apps/web/app/api/auth/reset-password/route.ts index 57b761bf65c7a6..fc1a745bd1cdd5 100644 --- a/apps/web/app/api/auth/reset-password/route.ts +++ b/apps/web/app/api/auth/reset-password/route.ts @@ -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"; diff --git a/apps/web/app/api/auth/setup/route.ts b/apps/web/app/api/auth/setup/route.ts index 693b9597a72102..590d02b2d1e221 100644 --- a/apps/web/app/api/auth/setup/route.ts +++ b/apps/web/app/api/auth/setup/route.ts @@ -4,8 +4,8 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import z from "zod"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; -import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; +import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; import { emailRegex } from "@calcom/lib/emailSchema"; import { HttpError } from "@calcom/lib/http-error"; import slugify from "@calcom/lib/slugify"; diff --git a/apps/web/app/api/availability/calendar/route.ts b/apps/web/app/api/availability/calendar/route.ts index e0a6bdca532578..b84dab24461f36 100644 --- a/apps/web/app/api/availability/calendar/route.ts +++ b/apps/web/app/api/availability/calendar/route.ts @@ -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/features/calendars/lib/CalendarManager"; +import { getCalendarCredentials, getConnectedCalendars } from "@calcom/lib/CalendarManager"; import { HttpError } from "@calcom/lib/http-error"; import notEmpty from "@calcom/lib/notEmpty"; import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; diff --git a/apps/web/app/api/recorded-daily-video/route.ts b/apps/web/app/api/recorded-daily-video/route.ts index b7ab18df15b282..2737e7c1752e0d 100644 --- a/apps/web/app/api/recorded-daily-video/route.ts +++ b/apps/web/app/api/recorded-daily-video/route.ts @@ -17,7 +17,7 @@ import { safeStringify } from "@calcom/lib/safeStringify"; import { getAllTranscriptsAccessLinkFromMeetingId, submitBatchProcessorTranscriptionJob, -} from "@calcom/app-store/videoClient"; +} from "@calcom/lib/videoClient"; import { generateVideoToken } from "@calcom/lib/videoTokens"; import prisma from "@calcom/prisma"; import { getBooking } from "@calcom/web/lib/daily-webhook/getBooking"; diff --git a/apps/web/app/api/video/recording/__tests__/route.test.ts b/apps/web/app/api/video/recording/__tests__/route.test.ts index 3d3a947e262a09..85c93109147bcd 100644 --- a/apps/web/app/api/video/recording/__tests__/route.test.ts +++ b/apps/web/app/api/video/recording/__tests__/route.test.ts @@ -1,12 +1,12 @@ import { NextResponse } from "next/server"; import { describe, expect, test, vi, afterEach } from "vitest"; -import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient"; +import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient"; import { verifyVideoToken } from "@calcom/lib/videoTokens"; import { GET } from "../route"; -vi.mock("@calcom/app-store/videoClient", () => ({ +vi.mock("@calcom/lib/videoClient", () => ({ getDownloadLinkOfCalVideoByRecordingId: vi.fn(), })); diff --git a/apps/web/app/api/video/recording/route.ts b/apps/web/app/api/video/recording/route.ts index 00d04beec50442..ad9c18b1739c88 100644 --- a/apps/web/app/api/video/recording/route.ts +++ b/apps/web/app/api/video/recording/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient"; +import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient"; import { verifyVideoToken } from "@calcom/lib/videoTokens"; export async function GET(request: Request) { diff --git a/apps/web/components/apps/installation/ConfigureStepCard.tsx b/apps/web/components/apps/installation/ConfigureStepCard.tsx index 73a774d236cad8..d4dcb787fd217f 100644 --- a/apps/web/components/apps/installation/ConfigureStepCard.tsx +++ b/apps/web/components/apps/installation/ConfigureStepCard.tsx @@ -7,10 +7,9 @@ import { useFieldArray, useFormContext } from "react-hook-form"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import type { LocationObject } from "@calcom/app-store/locations"; -import { locationsResolver } from "@calcom/app-store/locations"; -import NoSSR from "@calcom/lib/components/NoSSR"; +import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver"; 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"; @@ -189,7 +188,7 @@ const EventTypeGroup = ({ ); }; -const ConfigureStepCardContent: FC = (props) => { +export const ConfigureStepCard: FC = (props) => { const { loading, formPortalRef, handleSetUpLater } = props; const { t } = useLocale(); const { control, watch } = useFormContext(); @@ -213,7 +212,6 @@ const ConfigureStepCardContent: FC = (props) => { ); const [submit, setSubmit] = useState(false); - const [mounted, setMounted] = useState(false); const allUpdated = updatedEventTypesStatus.every((item) => item.every((iitem) => iitem.updated)); useEffect(() => { @@ -223,73 +221,60 @@ const ConfigureStepCardContent: FC = (props) => { } }, [submit, allUpdated, mainForSubmitRef]); - useEffect(() => { - setMounted(true); - }, []); - - if (!formPortalRef?.current || !mounted) { - return null; - } - - return createPortal( -
- {fields.map((group, groupIndex) => ( -
- {eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && ( -
- -

{group.slug}

-
- )} - -
- ))} - - - -
+ return ( + formPortalRef?.current && + createPortal( +
+ {fields.map((group, groupIndex) => ( +
+ {eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && ( +
+ +

{group.slug}

+
+ )} + +
+ ))} + -
-
, - formPortalRef.current - ); -}; -export const ConfigureStepCard: FC = (props) => { - return ( - - - +
+ +
+
, + formPortalRef?.current + ) ); }; diff --git a/apps/web/components/apps/installation/EventTypeAppSettingsWrapper.tsx b/apps/web/components/apps/installation/EventTypeAppSettingsWrapper.tsx index 777e82bebd57d5..49b9015c386a16 100644 --- a/apps/web/components/apps/installation/EventTypeAppSettingsWrapper.tsx +++ b/apps/web/components/apps/installation/EventTypeAppSettingsWrapper.tsx @@ -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/features/apps/hooks/useAppsData"; +import useAppsData from "@calcom/lib/hooks/useAppsData"; import type { ConfigureStepCardProps } from "@components/apps/installation/ConfigureStepCard"; diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index f673aa9f6440fb..e41a2027bd2904 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/* eslint-disable prettier/prettier */ import Link from "next/link"; import { useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; @@ -65,6 +68,7 @@ import { shouldShowRecurringCancelAction, type BookingActionContext, } from "./bookingActions"; +import { useBookingItemState } from "./hooks/useBookingItemState"; type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"]; @@ -119,19 +123,16 @@ const isBookingReroutable = (booking: ParsedBooking): booking is ReroutableBooki function BookingListItem(booking: BookingItemProps) { const parsedBooking = buildParsedBooking(booking); - const { userTimeZone, userTimeFormat, userEmail } = booking.loggedInUser; const { t, i18n: { language }, } = useLocale(); const utils = trpc.useUtils(); - const [rejectionReason, setRejectionReason] = useState(""); - const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); - const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false); - const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState(false); - const [meetingSessionDetailsDialogIsOpen, setMeetingSessionDetailsDialogIsOpen] = useState(false); - const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState(false); + + // Use our centralized state hook + const { dialogState, openDialog, closeDialog, rejectionReason, setRejectionReason } = useBookingItemState(); + const cardCharged = booking?.payment[0]?.success; const attendeeList = booking.attendees.map((attendee) => { @@ -158,13 +159,12 @@ function BookingListItem(booking: BookingItemProps) { const mutation = trpc.viewer.bookings.confirm.useMutation({ onSuccess: (data) => { if (data?.status === BookingStatus.REJECTED) { - setRejectionDialogIsOpen(false); + closeDialog("rejection"); showToast(t("booking_rejection_success"), "success"); } else { showToast(t("booking_confirmation_success"), "success"); } utils.viewer.bookings.invalidate(); - utils.viewer.me.bookingUnconfirmedCount.invalidate(); }, onError: () => { showToast(t("booking_confirmation_failed"), "error"); @@ -185,10 +185,6 @@ function BookingListItem(booking: BookingItemProps) { const isTabUnconfirmed = booking.listingStatus === "unconfirmed"; const isBookingFromRoutingForm = isBookingReroutable(parsedBooking); - const userSeat = booking.seatsReferences.find((seat) => !!userEmail && seat.attendee?.email === userEmail); - - const isAttendee = !!userSeat; - const paymentAppData = getPaymentAppData(booking.eventType); const location = booking.location as ReturnType; @@ -227,7 +223,10 @@ function BookingListItem(booking: BookingItemProps) { }; const getSeatReferenceUid = () => { - return userSeat?.referenceUid; + if (!booking.seatsReferences[0]) { + return undefined; + } + return booking.seatsReferences[0].referenceUid; }; const actionContext: BookingActionContext = { @@ -251,7 +250,6 @@ function BookingListItem(booking: BookingItemProps) { booking.location === "integrations:daily" || (typeof booking.location === "string" && booking.location.trim() === ""), showPendingPayment: paymentAppData.enabled && booking.payment.length && !booking.paid, - isAttendee, cardCharged, attendeeList, getSeatReferenceUid, @@ -263,7 +261,7 @@ function BookingListItem(booking: BookingItemProps) { ...action, onClick: action.id === "reject" - ? () => setRejectionDialogIsOpen(true) + ? () => openDialog("rejection") : action.id === "confirm" ? () => bookingConfirm(true) : undefined, @@ -288,11 +286,8 @@ function BookingListItem(booking: BookingItemProps) { .tz(userTimeZone) .locale(language) .format(isUpcoming ? (isDifferentYear ? "ddd, D MMM YYYY" : "ddd, D MMM") : "D MMMM YYYY"); - const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false); - const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false); - const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false); - const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false); - const [rerouteDialogIsOpen, setRerouteDialogIsOpen] = useState(false); + const [, setIsOpenLocationDialog] = useState(false); + // const [setIsOpenLocationDialog] = useState(false); const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({ onSuccess: () => { showToast(t("location_updated"), "success"); @@ -361,15 +356,15 @@ function BookingListItem(booking: BookingItemProps) { ...action, onClick: action.id === "reschedule_request" - ? () => setIsOpenRescheduleDialog(true) + ? () => openDialog("reschedule") : action.id === "reroute" - ? () => setRerouteDialogIsOpen(true) + ? () => openDialog("reroute") : action.id === "change_location" - ? () => setIsOpenLocationDialog(true) + ? () => openDialog("editLocation") : action.id === "add_members" - ? () => setIsOpenAddGuestsDialog(true) + ? () => openDialog("addGuests") : action.id === "reassign" - ? () => setIsOpenReassignDialog(true) + ? () => openDialog("reassign") : undefined, })) as ActionType[]; @@ -378,11 +373,11 @@ function BookingListItem(booking: BookingItemProps) { ...action, onClick: action.id === "view_recordings" - ? () => setViewRecordingsDialogIsOpen(true) + ? () => openDialog("viewRecordings") : action.id === "meeting_session_details" - ? () => setMeetingSessionDetailsDialogIsOpen(true) + ? () => openDialog("meetingSessionDetails") : action.id === "charge_card" - ? () => setChargeCardDialogIsOpen(true) + ? () => openDialog("chargeCard") : action.id === "no_show" ? () => { if (attendeeList.length === 1) { @@ -393,7 +388,7 @@ function BookingListItem(booking: BookingItemProps) { }); return; } - setIsNoShowDialogOpen(true); + openDialog("noShowAttendees"); } : undefined, disabled: @@ -405,14 +400,14 @@ function BookingListItem(booking: BookingItemProps) { return ( <> (open ? openDialog("reschedule") : closeDialog("reschedule"))} bookingUId={booking.uid} /> - {isOpenReassignDialog && ( + {dialogState.reassign && ( (open ? openDialog("reassign") : closeDialog("reassign"))} bookingId={booking.id} teamId={booking.eventType?.team?.id || 0} bookingFromRoutingForm={isBookingFromRoutingForm} @@ -421,19 +416,19 @@ function BookingListItem(booking: BookingItemProps) { (open ? openDialog("editLocation") : closeDialog("editLocation"))} teamId={booking.eventType?.team?.id} /> (open ? openDialog("addGuests") : closeDialog("addGuests"))} bookingId={booking.id} /> {booking.paid && booking.payment[0] && ( (open ? openDialog("chargeCard") : closeDialog("chargeCard"))} bookingId={booking.id} paymentAmount={booking.payment[0].amount} paymentCurrency={booking.payment[0].currency} @@ -442,28 +437,32 @@ function BookingListItem(booking: BookingItemProps) { {isCalVideoLocation && ( (open ? openDialog("viewRecordings") : closeDialog("viewRecordings"))} timeFormat={userTimeFormat ?? null} /> )} - {isCalVideoLocation && meetingSessionDetailsDialogIsOpen && ( + {isCalVideoLocation && dialogState.meetingSessionDetails && ( + open ? openDialog("meetingSessionDetails") : closeDialog("meetingSessionDetails") + } timeFormat={userTimeFormat ?? null} /> )} - {isNoShowDialogOpen && ( + {dialogState.noShowAttendees && ( (open ? openDialog("noShowAttendees") : closeDialog("noShowAttendees"))} + isOpen={dialogState.noShowAttendees} /> )} - + (open ? openDialog("rejection") : closeDialog("rejection"))}>
closeDialog("reroute")} booking={{ ...parsedBooking, eventType: parsedBooking.eventType }} /> )} @@ -821,7 +820,7 @@ const RecurringBookingsTooltip = ({ return ( recurringDate >= now && !booking.recurringInfo?.bookings[BookingStatus.CANCELLED] - .map((date) => date.toString()) + .map((date: { toString: () => any }) => date.toString()) .includes(recurringDate.toString()) ); }).length; @@ -839,7 +838,7 @@ const RecurringBookingsTooltip = ({ const pastOrCancelled = aDate < now || booking.recurringInfo?.bookings[BookingStatus.CANCELLED] - .map((date) => date.toString()) + .map((date: { toString: () => any }) => date.toString()) .includes(aDate.toString()); return (

diff --git a/apps/web/components/booking/bookingActions.ts b/apps/web/components/booking/bookingActions.ts index f98e99266a5d2a..cdc6982717b4cc 100644 --- a/apps/web/components/booking/bookingActions.ts +++ b/apps/web/components/booking/bookingActions.ts @@ -21,7 +21,6 @@ export interface BookingActionContext { isDisabledRescheduling: boolean; isCalVideoLocation: boolean; showPendingPayment: boolean; - isAttendee: boolean; cardCharged: boolean; attendeeList: Array<{ name: string; @@ -103,7 +102,6 @@ export function getEditEventActions(context: BookingActionContext): ActionType[] isDisabledRescheduling, isBookingFromRoutingForm, getSeatReferenceUid, - isAttendee, t, } = context; @@ -113,7 +111,7 @@ export function getEditEventActions(context: BookingActionContext): ActionType[] icon: "clock", label: t("reschedule_booking"), href: `/reschedule/${booking.uid}${ - booking.seatsReferences.length && isAttendee ? `?seatReferenceUid=${getSeatReferenceUid()}` : "" + booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : "" }`, disabled: (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling, diff --git a/apps/web/components/booking/hooks/useBookingItemState.ts b/apps/web/components/booking/hooks/useBookingItemState.ts new file mode 100644 index 00000000000000..2299e23f69c1f7 --- /dev/null +++ b/apps/web/components/booking/hooks/useBookingItemState.ts @@ -0,0 +1,77 @@ +import { useState } from "react"; + +export type DialogState = { + reschedule: boolean; + reassign: boolean; + editLocation: boolean; + addGuests: boolean; + chargeCard: boolean; + viewRecordings: boolean; + meetingSessionDetails: boolean; + noShowAttendees: boolean; + rejection: boolean; + reroute: boolean; +}; + +export function useBookingItemState() { + const [rejectionReason, setRejectionReason] = useState(""); + const [dialogState, setDialogState] = useState({ + reschedule: false, + reassign: false, + editLocation: false, + addGuests: false, + chargeCard: false, + viewRecordings: false, + meetingSessionDetails: false, + noShowAttendees: false, + rejection: false, + reroute: false, + }); + + const openDialog = (dialog: keyof DialogState) => { + setDialogState((prevState) => ({ + ...prevState, + [dialog]: true, + })); + }; + + const closeDialog = (dialog: keyof DialogState) => { + setDialogState((prevState) => ({ + ...prevState, + [dialog]: false, + })); + }; + + const toggleDialog = (dialog: keyof DialogState) => { + setDialogState((prevState) => ({ + ...prevState, + [dialog]: !prevState[dialog], + })); + }; + + // Helper function to reset all dialogs (useful when one action should close others) + const resetDialogs = () => { + setDialogState({ + reschedule: false, + reassign: false, + editLocation: false, + addGuests: false, + chargeCard: false, + viewRecordings: false, + meetingSessionDetails: false, + noShowAttendees: false, + rejection: false, + reroute: false, + }); + }; + + return { + dialogState, + openDialog, + closeDialog, + toggleDialog, + resetDialogs, + rejectionReason, + setRejectionReason, + }; +} diff --git a/apps/web/components/dialog/ChargeCardDialog.tsx b/apps/web/components/dialog/ChargeCardDialog.tsx index 1de62f399eb87a..8054f997daa112 100644 --- a/apps/web/components/dialog/ChargeCardDialog.tsx +++ b/apps/web/components/dialog/ChargeCardDialog.tsx @@ -1,4 +1,3 @@ -import type { Dispatch, SetStateAction } from "react"; import { useState } from "react"; import { Dialog } from "@calcom/features/components/controlled-dialog"; @@ -9,15 +8,15 @@ import { DialogContent, DialogFooter, DialogHeader, DialogClose } from "@calcom/ import { Icon } from "@calcom/ui/components/icon"; import { showToast } from "@calcom/ui/components/toast"; -interface IRescheduleDialog { +interface IChargeCardDialog { isOpenDialog: boolean; - setIsOpenDialog: Dispatch>; + setIsOpenDialog: (isOpen: boolean) => void; bookingId: number; paymentAmount: number; paymentCurrency: string; } -export const ChargeCardDialog = (props: IRescheduleDialog) => { +export const ChargeCardDialog = (props: IChargeCardDialog) => { const { t } = useLocale(); const utils = trpc.useUtils(); const { isOpenDialog, setIsOpenDialog, bookingId } = props; diff --git a/apps/web/components/setup/AdminUser.tsx b/apps/web/components/setup/AdminUser.tsx index fe9624863f053b..d5d3219625ff5e 100644 --- a/apps/web/components/setup/AdminUser.tsx +++ b/apps/web/components/setup/AdminUser.tsx @@ -5,7 +5,7 @@ import React from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { z } from "zod"; -import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; +import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; import { WEBSITE_URL } from "@calcom/lib/constants"; import { emailRegex } from "@calcom/lib/emailSchema"; import { useLocale } from "@calcom/lib/hooks/useLocale"; diff --git a/apps/web/components/team/screens/Team.tsx b/apps/web/components/team/screens/Team.tsx index d15e943d295555..78b00ffc728334 100644 --- a/apps/web/components/team/screens/Team.tsx +++ b/apps/web/components/team/screens/Team.tsx @@ -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/features/ee/teams/lib/queries"; +import type { TeamWithMembers } from "@calcom/lib/server/queries/teams"; import type { UserProfile } from "@calcom/types/UserProfile"; import { UserAvatar } from "@calcom/ui/components/avatar"; diff --git a/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts b/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts index 303f7928a27b69..09b39906b4d7b1 100644 --- a/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts +++ b/apps/web/lib/__tests__/getTeamMemberEmailFromCrm.test.ts @@ -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/features/ee/teams/lib/getTeamMemberEmailFromCrm"; +import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/lib/server/getTeamMemberEmailFromCrm"; import { SchedulingType } from "@calcom/prisma/enums"; vi.mock("@calcom/app-store/routing-forms/appBookingFormHandler", () => ({ diff --git a/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts b/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts index f168705014142d..1862e0a34840c6 100644 --- a/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts +++ b/apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts @@ -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"; diff --git a/apps/web/lib/hooks/settings/platform/billing/useCheckTeamBilling.ts b/apps/web/lib/hooks/settings/platform/billing/useCheckTeamBilling.ts index 4abab3cb219a36..d26cbedd260701 100644 --- a/apps/web/lib/hooks/settings/platform/billing/useCheckTeamBilling.ts +++ b/apps/web/lib/hooks/settings/platform/billing/useCheckTeamBilling.ts @@ -14,7 +14,6 @@ export const useCheckTeamBilling = (teamId?: number | null, isPlatformTeam?: boo return data.data; }, enabled: !!teamId && !!isPlatformTeam, - staleTime: 5000, }); return isTeamBilledAlready; diff --git a/apps/web/lib/reschedule/[uid]/getServerSideProps.ts b/apps/web/lib/reschedule/[uid]/getServerSideProps.ts index 88d7a26dfbb738..cdf3726e481bf0 100644 --- a/apps/web/lib/reschedule/[uid]/getServerSideProps.ts +++ b/apps/web/lib/reschedule/[uid]/getServerSideProps.ts @@ -4,12 +4,14 @@ import { URLSearchParams } from "url"; import { z } from "zod"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { determineReschedulePreventionRedirect } from "@calcom/features/bookings/lib/reschedule/determineReschedulePreventionRedirect"; -import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getFullName } from "@calcom/features/form-builder/utils"; import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking"; +import { getDefaultEvent } from "@calcom/lib/defaultEvents"; +import { getSafe } from "@calcom/lib/getSafe"; 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(), @@ -39,7 +41,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { uid, seatReferenceUid: maybeSeatReferenceUid, bookingSeat, - } = await maybeGetBookingUidFromSeat(prisma, seatReferenceUid ? seatReferenceUid : bookingUid); + } = await maybeGetBookingUidFromSeat(prisma, bookingUid); const booking = await prisma.booking.findUnique({ where: { @@ -61,7 +63,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { allowReschedulingCancelledBookings: true, team: { select: { - id: true, parentId: true, slug: true, }, @@ -97,6 +98,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { notFound: true, } as const; } + const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef); const userRepo = new UserRepository(prisma); @@ -110,36 +112,59 @@ 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 const; + } as { + notFound: true; + }; } - // 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, - }); + const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date(); + if (isBookingInPast && !eventType.allowReschedulingPastBookings) { + const destinationUrlSearchParams = new URLSearchParams(); + const responses = bookingSeat ? getSafe(bookingSeat.data, ["responses"]) : booking.responses; + const name = getFullName(getSafe(responses, ["name"])); + const email = getSafe(responses, ["email"]); + + if (name) destinationUrlSearchParams.set("name", name); + if (email) destinationUrlSearchParams.set("email", email); - if (reschedulePreventionRedirectUrl) { + const searchParamsString = destinationUrlSearchParams.toString(); return { redirect: { - destination: reschedulePreventionRedirectUrl, + destination: searchParamsString ? `${eventUrl}?${searchParamsString}` : eventUrl, permanent: false, }, }; diff --git a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx index 4923562ece6104..75a956968cf537 100644 --- a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx @@ -108,7 +108,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) { const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import( - "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm" + "@calcom/lib/server/getTeamMemberEmailFromCrm" ); const { email, diff --git a/apps/web/lib/team/[slug]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/getServerSideProps.tsx index 3aa548b1707578..02552a0ddf6b41 100644 --- a/apps/web/lib/team/[slug]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/getServerSideProps.tsx @@ -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/features/ee/teams/lib/queries"; +import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import slugify from "@calcom/lib/slugify"; import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import prisma from "@calcom/prisma"; diff --git a/apps/web/modules/apps/installation/[[...step]]/step-view.tsx b/apps/web/modules/apps/installation/[[...step]]/step-view.tsx index 249be734068440..9646d5f3560f49 100644 --- a/apps/web/modules/apps/installation/[[...step]]/step-view.tsx +++ b/apps/web/modules/apps/installation/[[...step]]/step-view.tsx @@ -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/app-store/locations"; +import type { LocationObject } from "@calcom/lib/location"; import type { Team } from "@calcom/prisma/client"; import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; diff --git a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx index b7ecab1ae5c7bd..df102f6974b960 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx @@ -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/features/eventtypes/lib/defaultEvents"; +import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { shouldHideBrandingForEvent } from "@calcom/lib/hideBranding"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; diff --git a/apps/web/modules/event-types/views/event-types-listing-view.tsx b/apps/web/modules/event-types/views/event-types-listing-view.tsx index 6fe670651a5056..6ca26190604692 100644 --- a/apps/web/modules/event-types/views/event-types-listing-view.tsx +++ b/apps/web/modules/event-types/views/event-types-listing-view.tsx @@ -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 { MembershipRole } from "@calcom/prisma/enums"; +import type { 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,24 +954,6 @@ export const EventTypesCTA = ({ userEventGroupsData }: Omit) => { 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, diff --git a/apps/web/modules/insights/insights-view.tsx b/apps/web/modules/insights/insights-view.tsx index 24a0c007db832c..cbac224b79a810 100644 --- a/apps/web/modules/insights/insights-view.tsx +++ b/apps/web/modules/insights/insights-view.tsx @@ -1,7 +1,5 @@ "use client"; -import { useState, useCallback } from "react"; - import { DataTableProvider, DataTableFilters, @@ -9,7 +7,6 @@ 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, @@ -30,13 +27,11 @@ 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 ( @@ -54,26 +49,10 @@ const createdAtColumn: Extract = { - 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 ( <> @@ -84,16 +63,10 @@ function InsightsPageContent() { - +

- - - - +
diff --git a/apps/web/modules/settings/billing/billing-view.tsx b/apps/web/modules/settings/billing/billing-view.tsx index 1e324a34da793f..739969cd402e53 100644 --- a/apps/web/modules/settings/billing/billing-view.tsx +++ b/apps/web/modules/settings/billing/billing-view.tsx @@ -77,26 +77,21 @@ const BillingView = () => { return ( <> -
-
-
-

{t("manage_billing")}

-

- {t("view_and_manage_billing_details")} -

-
- -
-
-

{t("need_help")}

-
+ +
+ + -
+
- ); }; diff --git a/apps/web/modules/settings/billing/components/BillingCredits.tsx b/apps/web/modules/settings/billing/components/BillingCredits.tsx index 18bb117fb06a4b..8be53a386fc91a 100644 --- a/apps/web/modules/settings/billing/components/BillingCredits.tsx +++ b/apps/web/modules/settings/billing/components/BillingCredits.tsx @@ -8,21 +8,17 @@ 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"; @@ -33,39 +29,6 @@ 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 ( -
- - {label} - - - {numberFormatter.format(value)} - -
- ); -}; - const getMonthOptions = (): MonthOption[] => { const options: MonthOption[] = []; const minDate = dayjs.utc("2025-05-01"); @@ -97,7 +60,6 @@ export default function BillingCredits() { const monthOptions = useMemo(() => getMonthOptions(), []); const [selectedMonth, setSelectedMonth] = useState(monthOptions[0]); const [isDownloading, setIsDownloading] = useState(false); - const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); const utils = trpc.useUtils(); const { @@ -109,7 +71,6 @@ 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) @@ -171,153 +132,117 @@ export default function BillingCredits() { buyCreditsMutation.mutate({ quantity: data.quantity, teamId }); }; - 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(); + const teamCreditsPercentageUsed = + creditsData.credits.totalMonthlyCredits > 0 + ? (creditsData.credits.totalRemainingMonthlyCredits / creditsData.credits.totalMonthlyCredits) * 100 + : 0; return ( - <> -
-
-

{t("credits")}

-

{t("view_and_manage_credits")}

+
+
+

{t("credits")}

+ + Learn more + , + ]} + /> +
+
-
-
- {totalCredits > 0 ? ( - <> -
- - - -
- -
- {/*750 credits per tip*/} -
-

- {orgSlug ? t("credits_per_tip_org") : t("credits_per_tip_teams")} -

- -
-
-
-
-
- - ) : ( - <> - )} - - {/*Auto Top-Up goes here when we have it*/} - {/*
-
-
*/} - {/*Additional Credits*/} -
-
-
- -
-

- {t("current_balance")}{" "} - {numberFormatter.format(creditsData.credits.additionalCredits)} -

- - - -
+
+ {creditsData.credits.totalMonthlyCredits > 0 ? ( +
+ + +
+
+ {t("total_credits", { + totalCredits: creditsData.credits.totalMonthlyCredits, + })}
-
- setValue("quantity", Number(e.target.value))} - min={50} - addOnSuffix={<>{t("credits")}} - /> - +
+ {t("remaining_credits", { + remainingCredits: creditsData.credits.totalRemainingMonthlyCredits, + })}
- {errors.quantity && }
- -
-
- {/*Download Expense Log*/} -
-
- -
- option && setSelectedMonth(option)} + />
+
+ +
- {/*Credit Worth Section*/} -
- - Learn more - , - ]} - /> -
- {teamId && ( - setShowMemberInvitationModal(false)} - onSettingsOpen={() => { - return; - }} - /> - )} - +
); } diff --git a/apps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsx b/apps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsx index 6de0b9576b4b74..7e49241f931b80 100644 --- a/apps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsx +++ b/apps/web/modules/settings/billing/components/BillingCreditsSkeleton.tsx @@ -3,77 +3,78 @@ import { SkeletonText, SkeletonContainer, SkeletonButton } from "@calcom/ui/comp export function BillingCreditsSkeleton() { return ( -
-
- {/* Credits title */} - {/* View and manage credits description */} -
-
-
- {/* Credits section */} -
- {/* Monthly credits row */} -
- - -
- {/* Additional credits row */} -
- - -
- {/* Remaining row */} -
- - -
- {/* Progress bar */} -
-
+
+ {/* Title and Description */} +
+
+ {/* Credits title */} +
+
+ {/* Description */} +
+ +
+
+
+ {/* Monthly credits */} +
+ {/* Monthly credits label */} +
{/* Progress bar */} +
+
+ {/* Total credits */}
- {/* Credits per tip */} -
- - +
+ {/* Remaining credits */}
-
-
+
+ {/* Additional credits */} +
+
+ {/* Additional credits label */} +
+
+ {/* Additional credits value */}
- {/* Additional Credits form */} -
-
- {/* Additional credits label */} -
- {/* Input field */} -
+
+
+
+
+ {/* Buy credits form */} +
+
+
+ {/* Buy credits label */}
-
- {/* Buy button */} +
+ {/* Input field */}
-
-
+ +
+ {/* Buy button */}
- {/* Download Expense Log */} -
-
- {/* Download expense log label */} -
- {/* Select dropdown */} -
+
+
+
+
+ {/* Download expense log */} +
+
+
+ {/* Label */}
-
- {/* Download button */} +
+ {/* Select */}
+ +
+ {/* Download button */} +
- {/* Credit Worth Section */} -
- - -
); diff --git a/apps/web/package.json b/apps/web/package.json index 4bec1f66c2299f..1bf6343d1be85e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,13 +1,13 @@ { "name": "@calcom/web", - "version": "5.7.4", + "version": "5.7.0", "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": "turbo run copy-app-store-static && next dev --turbopack", + "dev": "yarn copy-static && next dev --turbopack", "dev:scan": "yarn dev & npx --yes react-scan@latest localhost:3000", "dev:cron": "npx tsx cron-tester.ts", "dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https", @@ -16,9 +16,8 @@ "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": "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", + "copy-static": "node scripts/copy-app-store-static.js", + "build": "yarn copy-static && next build && yarn sentry:release", "start": "next start", "lint": "eslint . --ignore-path .gitignore", "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix", diff --git a/apps/web/playwright/booking-duplicate-api-calls.e2e.ts b/apps/web/playwright/booking-duplicate-api-calls.e2e.ts index fb8030f1d74d2b..eb297c4720668b 100644 --- a/apps/web/playwright/booking-duplicate-api-calls.e2e.ts +++ b/apps/web/playwright/booking-duplicate-api-calls.e2e.ts @@ -16,8 +16,8 @@ test.describe("Duplicate API Calls Prevention", () => { const trpcCalls: string[] = []; const apiV2Calls: string[] = []; - // Intercept tRPC getSchedule calls - pattern matches /api/trpc/slots/getSchedule - await page.route("**/api/trpc/slots/getSchedule**", async (route) => { + // Intercept tRPC getSchedule calls - pattern matches trpc.viewer.slots.getSchedule.useQuery() + await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => { trpcCalls.push(route.request().url()); await route.continue(); }); @@ -33,10 +33,14 @@ test.describe("Duplicate API Calls Prevention", () => { const totalCalls = trpcCalls.length + apiV2Calls.length; - expect(totalCalls).toBeGreaterThan(0); - expect(totalCalls).toBeLessThanOrEqual(1); - expect(trpcCalls.length).toBeLessThanOrEqual(1); - expect(apiV2Calls.length).toBeLessThanOrEqual(1); + 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); + } }); test("should detect when schedule endpoints are called multiple times for team events", async ({ @@ -61,7 +65,7 @@ test.describe("Duplicate API Calls Prevention", () => { const trpcCalls: string[] = []; const apiV2Calls: string[] = []; - await page.route("**/api/trpc/slots/getSchedule**", async (route) => { + await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => { trpcCalls.push(route.request().url()); await route.continue(); }); @@ -77,10 +81,14 @@ test.describe("Duplicate API Calls Prevention", () => { const totalCalls = trpcCalls.length + apiV2Calls.length; - expect(totalCalls).toBeGreaterThan(0); - expect(totalCalls).toBeLessThanOrEqual(1); - expect(trpcCalls.length).toBeLessThanOrEqual(1); - expect(apiV2Calls.length).toBeLessThanOrEqual(1); + 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); + } }); test("should detect when schedule endpoints are called multiple times for organization team events", async ({ @@ -109,7 +117,7 @@ test.describe("Duplicate API Calls Prevention", () => { const trpcCalls: string[] = []; const apiV2Calls: string[] = []; - await page.route("**/api/trpc/slots/getSchedule**", async (route) => { + await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => { trpcCalls.push(route.request().url()); await route.continue(); }); @@ -125,9 +133,13 @@ test.describe("Duplicate API Calls Prevention", () => { const totalCalls = trpcCalls.length + apiV2Calls.length; - expect(totalCalls).toBeGreaterThan(0); - expect(totalCalls).toBeLessThanOrEqual(1); - expect(trpcCalls.length).toBeLessThanOrEqual(1); - expect(apiV2Calls.length).toBeLessThanOrEqual(1); + 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); + } }); }); diff --git a/apps/web/playwright/booking-seats.e2e.ts b/apps/web/playwright/booking-seats.e2e.ts index 9b6224f0a74159..97fe26ef074bac 100644 --- a/apps/web/playwright/booking-seats.e2e.ts +++ b/apps/web/playwright/booking-seats.e2e.ts @@ -458,128 +458,5 @@ test.describe("Reschedule for booking with seats", () => { await expect(page.locator('[data-testid="confirm-reschedule-button"]')).toHaveCount(1); }); - test("Host reschedule from /upcoming page should have rescheduleUid parameter set to bookingUid", async ({ - page, - users, - bookings, - }) => { - const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [ - { name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" }, - { name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" }, - ]); - await user.apiLogin(); - - const bookingAttendees = await prisma.attendee.findMany({ - where: { bookingId: booking.id }, - select: { - id: true, - name: true, - email: true, - }, - }); - - const bookingSeats = bookingAttendees.map((attendee) => ({ - bookingId: booking.id, - attendeeId: attendee.id, - referenceUid: uuidv4(), - data: { - responses: { - name: attendee.name, - email: attendee.email, - }, - }, - })); - - await prisma.bookingSeat.createMany({ - data: bookingSeats, - }); - - await page.goto("/bookings/upcoming"); - await page.waitForSelector('[data-testid="bookings"]'); - - await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click(); - await page.locator('[data-testid="reschedule"]').click(); - - await page.waitForURL((url) => { - const rescheduleUid = url.searchParams.get("rescheduleUid"); - return !!rescheduleUid && rescheduleUid === booking.uid; - }); - }); - - test("Second attendee reschedule from /upcoming page should use correct seatReferenceUid and show attendee info", async ({ - page, - users, - bookings, - }) => { - const { user, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [ - { name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" }, - { name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" }, - ]); - - const bookingAttendees = await prisma.attendee.findMany({ - where: { bookingId: booking.id }, - select: { - id: true, - name: true, - email: true, - }, - }); - - const bookingSeats = bookingAttendees.map((attendee) => ({ - bookingId: booking.id, - attendeeId: attendee.id, - referenceUid: uuidv4(), - data: { - responses: { - name: attendee.name, - email: attendee.email, - }, - }, - })); - - await prisma.bookingSeat.createMany({ - data: bookingSeats, - }); - - const references = await prisma.bookingSeat.findMany({ - where: { bookingId: booking.id }, - orderBy: { id: "asc" }, - }); - - const secondUser = await users.create({ - name: "Jane Second", - email: "second+seats@cal.com", - }); - await secondUser.apiLogin(); - - await page.goto("/bookings/upcoming"); - await page.waitForSelector('[data-testid="bookings"]'); - - await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click(); - await page.locator('[data-testid="reschedule"]').click(); - - await page.waitForURL((url) => { - const rescheduleUid = url.searchParams.get("rescheduleUid"); - return !!rescheduleUid && rescheduleUid === references[1].referenceUid; - }); - - await selectFirstAvailableTimeSlotNextMonth(page); - - const nameElement = page.locator("input[name=name]"); - const name = await nameElement.inputValue(); - expect(name).toBe("Jane Second"); - - const emailElement = page.locator("input[name=email]"); - const email = await emailElement.inputValue(); - expect(email).toBe("second+seats@cal.com"); - - // Complete the reschedule - await confirmReschedule(page); - - // Verify successful reschedule - await page.waitForURL(/\/booking\/.*/); - await expect(page).toHaveURL(/\/booking\/.*/); - }); - // @TODO: force 404 when rescheduleUid is not found }); diff --git a/apps/web/playwright/fixtures/apps.ts b/apps/web/playwright/fixtures/apps.ts index b511b041a8e2c2..3d213d4e795a63 100644 --- a/apps/web/playwright/fixtures/apps.ts +++ b/apps/web/playwright/fixtures/apps.ts @@ -111,9 +111,6 @@ 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(); diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index d4ac303f0fd968..4642bb1d56d09f 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -584,10 +584,6 @@ 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); @@ -684,10 +680,6 @@ 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({ diff --git a/apps/web/playwright/insights.e2e.ts b/apps/web/playwright/insights.e2e.ts index 49ca82fd557aea..d775545e997804 100644 --- a/apps/web/playwright/insights.e2e.ts +++ b/apps/web/playwright/insights.e2e.ts @@ -1,13 +1,10 @@ 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" }); @@ -271,82 +268,8 @@ test.describe("Insights", async () => { ]; for (const title of expectedChartTitles) { - const chartCard = page - .locator("[data-testid='panel-card'] h2") - .filter({ hasText: new RegExp(`^${title}$`) }); + const chartCard = page.locator("[data-testid='panel-card'] h2").filter({ hasText: 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"); - }); }); diff --git a/apps/web/playwright/lib/test-helpers/pbac.ts b/apps/web/playwright/lib/test-helpers/pbac.ts deleted file mode 100644 index 806ae55a095615..00000000000000 --- a/apps/web/playwright/lib/test-helpers/pbac.ts +++ /dev/null @@ -1,29 +0,0 @@ -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(), - }, - }); -}; diff --git a/apps/web/playwright/out-of-office.e2e.ts b/apps/web/playwright/out-of-office.e2e.ts index 08779282481b07..83f4145b5584fb 100644 --- a/apps/web/playwright/out-of-office.e2e.ts +++ b/apps/web/playwright/out-of-office.e2e.ts @@ -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( diff --git a/apps/web/playwright/workflow.e2e.ts b/apps/web/playwright/workflow.e2e.ts index 290ce20c99a6c7..f262c5e8c2161d 100644 --- a/apps/web/playwright/workflow.e2e.ts +++ b/apps/web/playwright/workflow.e2e.ts @@ -15,7 +15,7 @@ test.describe("Workflow Tab - Event Type", () => { test("Creating a new workflow", async ({ workflowPage }) => { const { createWorkflow, assertListCount } = workflowPage; - await createWorkflow({ name: "test workflow" }); + await createWorkflow({ name: "" }); await assertListCount(3); }); diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index 92a84f5643956f..670621818cb3a3 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم", "verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني", "verify_email_button": "تأكيد البريد الإلكتروني", - "cal_ai_assistant": "المساعد", + "cal_ai_assistant": "مساعد Cal AI", "send_cal_video_transcription_emails": "إرسال رسائل النسخ النصي لفيديو Cal", "description_send_cal_video_transcription_emails": "إرسال رسائل بريد إلكتروني تحتوي على النسخ النصي لفيديو Cal بعد انتهاء الاجتماع. (يتطلب خطة مدفوعة)", "verify_email_change_description": "لقد طلبت تغيير البريد الإلكتروني المستخدم لحسابك في {{appName}}. الرجاء النقر على الزر أدناه لتأكيد بريدك الإلكتروني الجديد.", @@ -819,8 +819,6 @@ "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": "هل أنت متأكد أنك تريد حذف رقم الهاتف هذا؟ لا يمكن التراجع عن هذا الإجراء.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "تم إلغاء اشتراك رقم الهاتف بنجاح", "updating": "جاري التحديث", "round_robin": "الترتيب الدوري", - "hi_how_are_you_doing": "مرحبًا، كيف حالك؟", "round_robin_description": "نقل الاجتماعات بشكل دوري بين أعضاء الفريق المتعددين.", "managed_event": "حدث تم إدارته", "username_placeholder": "اسم المستخدم", @@ -879,9 +876,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_phone_numbers": "أرقام هواتف Cal AI", "connect_phone_number": "توصيل رقم الهاتف", - "cal_ai_phone_numbers_description": "إدارة أرقام هواتفك", + "cal_ai_phone_numbers_description": "إدارة أرقام هواتف الخاصة بك", "import_number": "استيراد رقم", "this_action_will_also": "سيقوم هذا الإجراء أيضًا بـ:", "import_phone_number": "استيراد رقم الهاتف", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "إلغاء اشتراك رقم هاتفك", "delete_associated_phone_number": "حذف رقم الهاتف المرتبط", "unauthorized_create_workflow": "أنت غير مصرح لك بإنشاء مسار العمل هذا", - "import_phone_number_description": "استيراد رقم هاتف Twilio الخاص بك لاستخدامه مع الهاتف", + "import_phone_number_description": "استيراد رقم هاتف Twilio الخاص بك لاستخدامه مع هاتف Cal AI", "phone_number_cost": "${{price}}/شهريًا", "buy_new_number": "شراء رقم جديد", "buy_number_cost_x_per_month": "تكلفة شراء رقم هاتف هي ${{priceInDollars}} شهريًا. سيتم محاسبتك شهريًا على كل رقم هاتف نشط.", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "الفئات", "pricing": "التسعير", "learn_more": "معرفة المزيد", - "try_now": "جرب الآن", "privacy_policy": "سياسة الخصوصية", "terms_of_service": "شروط الخدمة", "remove": "إزالة", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "إرسال رسالة عبر WhatsApp إلى أحد الحاضرين", "workflows": "سير العمل", "new_workflow_btn": "سير عمل جديد", - "how_would_you_like_to_start": "كيف ترغب في البدء؟", "add_new_workflow": "إضافة سير عمل جديد", "reschedule_event_trigger": "متى يتم إعادة جدولة الحدث", "trigger": "مشغل", @@ -1722,8 +1716,6 @@ "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": "ملاحظات إضافية", @@ -1761,7 +1753,6 @@ "team_url": "رابط الفريق", "team_members": "أعضاء الفريق", "more": "المزيد", - "cal_ai_workflows": "سير عمل Cal.ai", "and_count_more": "و {{count}} أكثر", "more_page_footer": "نعتبر تطبيق الهاتف المحمول امتداداً لتطبيق الويب، لكن يرجى العودة لتطبيق الويب إذا كنت تقوم بأي إجراءات معقدة.", "workflow_example_1": "إرسال رسائل قصيرة للحضور للتذكير قبل 24 ساعة من بدء الحدث", @@ -1770,18 +1761,6 @@ "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": "ستتمكن من تعديل هذا لاحقًا.", @@ -2205,8 +2184,6 @@ "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}} أو طلب نشره منه.", @@ -2364,6 +2341,7 @@ "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": "أيام", @@ -2486,15 +2464,7 @@ "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": "حدّد بعض الفترات المتاحة وأدرجها في رسالتك الإلكترونية", @@ -2782,8 +2752,6 @@ "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": "وضع علامة عدم الحضور", @@ -3235,15 +3203,9 @@ "verify_email_change": "تأكيد تغيير البريد الإلكتروني", "buy_credits": "شراء رصيد", "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": "عرض وإدارة الرصيد", "view_and_manage_credits_description": "عرض وإدارة الرصيد لإرسال رسائل SMS. قيمة الرصيد الواحد هي 1¢ (دولار أمريكي). <0>معرفة المزيد", - "credit_worth_description": "الرصيد الواحد يساوي 1 سنت (دولار أمريكي). <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": "لا يمكنك إنشاء مؤسسة لأنك بالفعل جزء من مؤسسة", @@ -3424,13 +3386,10 @@ "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": "إرسال ردود البريد الإلكتروني إلى", @@ -3508,11 +3467,6 @@ "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": "عرض أنواع الأحداث", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "اسم حدث المشغل (مثل BOOKING_CREATED، BOOKING_CANCELLED)", "webhook_created_at": "وقت إنشاء الويب هوك", "webhook_type": "رابط نوع الحدث", - "set_up_agent": "إعداد الوكيل", "webhook_title": "اسم نوع الحدث", "webhook_start_time": "وقت بدء الحدث", "webhook_end_time": "وقت انتهاء الحدث", @@ -3672,9 +3625,6 @@ "visit": "زيارة", "location_custom_label_input_label": "تسمية مخصصة على صفحة الحجز", "meeting_link": "رابط الاجتماع", - "session_outcome": "نتيجة الجلسة", - "call_created": "تم إنشاء المكالمة", - "voicemail": "البريد الصوتي", "my_bookings": "حجوزاتي", "phone": "الهاتف", "free": "مجاني", @@ -3682,8 +3632,6 @@ "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": "فكر في إنشاء مؤسسة لتوحيد الفواتير وأدوات الإدارة والتحليلات عبر فرقك.", @@ -3704,32 +3652,5 @@ "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/az/common.json b/apps/web/public/static/locales/az/common.json index 9304fc70f1208e..41059bf5c77f27 100644 --- a/apps/web/public/static/locales/az/common.json +++ b/apps/web/public/static/locales/az/common.json @@ -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çi", + "cal_ai_assistant": "Köməkçisi", "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,8 +819,6 @@ "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.", @@ -848,7 +846,6 @@ "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ı", @@ -881,7 +878,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 İdarə edin", + "cal_ai_phone_numbers_description": "Telefon Nömrələrinizi idarə 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", @@ -889,7 +886,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": "Telefon ilə istifadə etmək üçün Twilio telefon nömrənizi idxal edin", + "import_phone_number_description": "Phone 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.", @@ -900,7 +897,6 @@ "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", @@ -1211,7 +1207,6 @@ "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", @@ -1445,7 +1440,6 @@ "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", @@ -1722,8 +1716,6 @@ "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", @@ -1761,7 +1753,6 @@ "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", @@ -1770,18 +1761,6 @@ "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.", @@ -2205,8 +2184,6 @@ "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.", @@ -2364,6 +2341,7 @@ "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", @@ -2486,15 +2464,7 @@ "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", @@ -2782,8 +2752,6 @@ "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", @@ -3235,15 +3203,9 @@ "verify_email_change": "E-poçt dəyişikliyini təsdiqlə", "buy_credits": "Kredit alın", "credits": "Kreditlər", - "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": "Kreditləri görüntüləyin 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ı", - "credit_worth_description": "Bir kredit 1¢ (USD) dəyərindədir. <0>Daha ətraflı öyrənin", "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", @@ -3424,13 +3386,10 @@ "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", @@ -3508,11 +3467,6 @@ "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", @@ -3640,7 +3594,6 @@ "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ı", @@ -3672,9 +3625,6 @@ "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", @@ -3682,8 +3632,6 @@ "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.", @@ -3704,32 +3652,5 @@ "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/bg/common.json b/apps/web/public/static/locales/bg/common.json index 239a66a5d8fcaa..8615336b193299 100644 --- a/apps/web/public/static/locales/bg/common.json +++ b/apps/web/public/static/locales/bg/common.json @@ -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,8 +819,6 @@ "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": "Сигурни ли сте, че искате да изтриете този телефонен номер? Това действие не може да бъде отменено.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Абонаментът за телефонен номер е отменен успешно", "updating": "Обновяване", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Здравейте, как сте?", "round_robin_description": "Разпределяйте срещи между множество членове на екипа.", "managed_event": "Управлявано събитие", "username_placeholder": "потребителско име", @@ -879,7 +876,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": "Импортиране на номер", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "Категории", "pricing": "Цени", "learn_more": "Научи повече", - "try_now": "Пробвайте сега", "privacy_policy": "Политика за поверителност", "terms_of_service": "Общи условия", "remove": "Премахни", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "изпращане на WhatsApp съобщение до участника", "workflows": "Работни процеси", "new_workflow_btn": "Нов работен процес", - "how_would_you_like_to_start": "Как бихте искали да започнете?", "add_new_workflow": "Добавяне на нов работен процес", "reschedule_event_trigger": "когато събитието е пренасрочено", "trigger": "Тригер", @@ -1722,8 +1716,6 @@ "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": "Допълнителни бележки", @@ -1761,7 +1753,6 @@ "team_url": "URL на екипа", "team_members": "Членове на екипа", "more": "Още", - "cal_ai_workflows": "Cal.ai работни процеси", "and_count_more": "и още {{count}}", "more_page_footer": "Разглеждаме мобилното приложение като разширение на уеб приложението. Ако извършвате сложни действия, моля, върнете се към уеб приложението.", "workflow_example_1": "Изпрати SMS напомняне 24 часа преди началото на събитието до участника", @@ -1770,18 +1761,6 @@ "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": "Ще можете да редактирате това по-късно.", @@ -2205,8 +2184,6 @@ "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": "Тази връзка към екипа в момента не е достъпна. Моля, свържете се със собственика на екипа или го помолете да я публикува.", @@ -2364,6 +2341,7 @@ "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": "дни", @@ -2486,15 +2464,7 @@ "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": "Изберете няколко свободни часа и ги вградете във вашия имейл", @@ -2782,8 +2752,6 @@ "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": "Маркирай като неявил се", @@ -3235,15 +3203,9 @@ "verify_email_change": "Потвърди промяната на имейл", "buy_credits": "Купете кредити", "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": "Преглед и управление на кредити", "view_and_manage_credits_description": "Преглед и управление на кредити за изпращане на SMS съобщения. Един кредит струва 1¢ (USD). <0>Научете повече", - "credit_worth_description": "Един кредит струва 1¢ (USD). <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": "Не можете да създадете организация, тъй като вече сте част от организация", @@ -3424,13 +3386,10 @@ "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": "Изпращане на имейл отговори до", @@ -3508,11 +3467,6 @@ "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": "Преглед на типове събития", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Името на задействащото събитие (напр. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Времето на уебхука", "webhook_type": "Слъгът на типа събитие", - "set_up_agent": "Настройка на агент", "webhook_title": "Името на типа събитие", "webhook_start_time": "Началното време на събитието", "webhook_end_time": "Крайното време на събитието", @@ -3672,9 +3625,6 @@ "visit": "Посети", "location_custom_label_input_label": "Персонализиран етикет на страницата за резервация", "meeting_link": "Линк за среща", - "session_outcome": "Резултат от сесията", - "call_created": "Създадено обаждане", - "voicemail": "Гласова поща", "my_bookings": "Моите резервации", "phone": "Телефон", "free": "Безплатно", @@ -3682,8 +3632,6 @@ "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": "Помислете за създаване на организация, за да обедините таксуването, административните инструменти и анализите за вашите екипи.", @@ -3704,32 +3652,5 @@ "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/bn/common.json b/apps/web/public/static/locales/bn/common.json index d4f19975c4111b..5077dc1431e14c 100644 --- a/apps/web/public/static/locales/bn/common.json +++ b/apps/web/public/static/locales/bn/common.json @@ -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,8 +819,6 @@ "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": "আপনি কি নিশ্চিত যে আপনি এই ফোন নম্বরটি মুছতে চান? এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "ফোন নম্বর সাবস্ক্রিপশন সফলভাবে বাতিল করা হয়েছে", "updating": "আপডেট করা হচ্ছে", "round_robin": "রাউন্ড রবিন", - "hi_how_are_you_doing": "হাই, আপনি কেমন আছেন?", "round_robin_description": "একাধিক দলের সদস্যের মধ্যে চক্র সভা।", "managed_event": "পরিচালিত ইভেন্ট", "username_placeholder": "ব্যবহারকারীর নাম", @@ -879,7 +876,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": "নম্বর আমদানি করুন", @@ -889,7 +886,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}}। প্রতিটি সক্রিয় ফোন নম্বরের জন্য আপনাকে মাসিক চার্জ করা হবে।", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "বিভাগ", "pricing": "মূল্য নির্ধারণ", "learn_more": "আরও শিখুন", - "try_now": "এখনই চেষ্টা করুন", "privacy_policy": "গোপনীয়তা নীতি", "terms_of_service": "পরিষেবার শর্তাদি", "remove": "সরান", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "অংশগ্রহণকারীদের কাছে হোয়াটসঅ্যাপ বার্তা প্রেরণ করুন", "workflows": "কর্মপ্রবাহ", "new_workflow_btn": "নতুন কর্মপ্রবাহ", - "how_would_you_like_to_start": "আপনি কীভাবে শুরু করতে চান?", "add_new_workflow": "একটি নতুন ওয়ার্কফ্লো যুক্ত করুন", "reschedule_event_trigger": "যখন ইভেন্টটি পুনরায় নির্ধারণ করা হয়", "trigger": "ট্রিগার", @@ -1722,8 +1716,6 @@ "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": "অতিরিক্ত নোট", @@ -1761,7 +1753,6 @@ "team_url": "টিম ইউআরএল", "team_members": "দলের সদস্য", "more": "আরও", - "cal_ai_workflows": "Cal.ai ওয়ার্কফ্লো", "and_count_more": "এবং আরও {{count}}টি", "more_page_footer": "আমরা মোবাইল অ্যাপ্লিকেশনটিকে ওয়েব অ্যাপ্লিকেশনটির এক্সটেনশন হিসাবে দেখি। ", "workflow_example_1": "ইভেন্টটি অংশ নিতে শুরু করার 24 ঘন্টা আগে এসএমএস অনুস্মারক প্রেরণ করুন", @@ -1770,18 +1761,6 @@ "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": "আপনি পরে এটি সম্পাদনা করতে পারবেন।", @@ -2205,8 +2184,6 @@ "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": "এই দলের লিঙ্কটি বর্তমানে উপলভ্য নয়। ", @@ -2364,6 +2341,7 @@ "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": "দিন", @@ -2486,15 +2464,7 @@ "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": "কয়েকটি উপলভ্য সময় নির্বাচন করুন এবং সেগুলি আপনার ইমেলটিতে এম্বেড করুন", @@ -2782,8 +2752,6 @@ "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": "নো-শো হিসাবে চিহ্নিত করুন", @@ -3235,15 +3203,9 @@ "verify_email_change": "ইমেইল পরিবর্তন যাচাই করুন", "buy_credits": "ক্রেডিট কিনুন", "credits": "ক্রেডিট", - "credits_used": "ব্যবহৃত ক্রেডিট", - "total_credits_remaining": "মোট অবশিষ্ট", - "credits_per_tip_org": "আপনি প্রতি মাসে, প্রতি টিম সদস্যের জন্য ১০০০ ক্রেডিট পান", - "credits_per_tip_teams": "আপনি প্রতি মাসে, প্রতি টিম সদস্যের জন্য ৭৫০ ক্রেডিট পান", - "view_and_manage_credits": "এসএমএস বার্তা পাঠানোর জন্য ক্রেডিট দেখুন এবং পরিচালনা করুন", + "view_and_manage_credits": "ক্রেডিট দেখুন এবং পরিচালনা করুন", "view_and_manage_credits_description": "এসএমএস বার্তা পাঠানোর জন্য ক্রেডিট দেখুন এবং পরিচালনা করুন। একটি ক্রেডিটের মূল্য 1¢ (USD)। <0>আরও জানুন", - "credit_worth_description": "একটি ক্রেডিটের মূল্য ১¢ (USD)। <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": "আপনি একটি সংগঠন তৈরি করতে পারবেন না কারণ আপনি ইতিমধ্যে একটি সংগঠনের অংশ", @@ -3424,13 +3386,10 @@ "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": "ইমেইল প্রতিক্রিয়া পাঠানোর জন্য সদস্য নির্বাচন করুন", @@ -3508,11 +3467,6 @@ "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": "ইভেন্টের ধরণ দেখুন", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "ট্রিগার ইভেন্টের নাম (যেমন, BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "ওয়েবহুকের সময়", "webhook_type": "ইভেন্ট টাইপ স্লাগ", - "set_up_agent": "এজেন্ট সেট আপ করুন", "webhook_title": "ইভেন্ট টাইপের নাম", "webhook_start_time": "ইভেন্টের শুরু সময়", "webhook_end_time": "ইভেন্টের শেষ সময়", @@ -3672,9 +3625,6 @@ "visit": "দেখুন", "location_custom_label_input_label": "বুকিং পৃষ্ঠায় কাস্টম লেবেল", "meeting_link": "সভার লিঙ্ক", - "session_outcome": "সেশনের ফলাফল", - "call_created": "কল তৈরি হয়েছে", - "voicemail": "ভয়েসমেইল", "my_bookings": "আমার বুকিং", "phone": "ফোন", "free": "বিনামূল্যে", @@ -3682,8 +3632,6 @@ "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": "আপনার দলগুলির মধ্যে বিলিং, অ্যাডমিন টুলস এবং অ্যানালিটিক্স একত্রিত করতে একটি সংগঠন সেট আপ করার কথা বিবেচনা করুন।", @@ -3704,32 +3652,5 @@ "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": "↑↑↑↑ুষান্ত" } \ No newline at end of file diff --git a/apps/web/public/static/locales/ca/common.json b/apps/web/public/static/locales/ca/common.json index 147e8a6b595c9d..7444fefa2f1867 100644 --- a/apps/web/public/static/locales/ca/common.json +++ b/apps/web/public/static/locales/ca/common.json @@ -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", + "cal_ai_assistant": "Assistent IA de Cal", "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,8 +819,6 @@ "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.", @@ -848,7 +846,6 @@ "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", @@ -879,9 +876,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", + "cal_ai_phone_numbers": "Números de telèfon de Cal AI", "connect_phone_number": "Connecta un número de telèfon", - "cal_ai_phone_numbers_description": "Gestiona els teus números de telèfon", + "cal_ai_phone_numbers_description": "Gestiona els teus números de telèfon de Cal AI", "import_number": "Importa un número", "this_action_will_also": "Aquesta acció també:", "import_phone_number": "Importa un número de telèfon", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "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", @@ -1445,7 +1440,6 @@ "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", @@ -1722,8 +1716,6 @@ "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", @@ -1761,7 +1753,6 @@ "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", @@ -1770,18 +1761,6 @@ "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.", @@ -2205,8 +2184,6 @@ "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.", @@ -2364,6 +2341,7 @@ "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", @@ -2486,15 +2464,7 @@ "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", @@ -2782,8 +2752,6 @@ "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", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verifica el canvi de correu electrònic", "buy_credits": "Compra crèdits", "credits": "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": "Visualitza i gestiona els crèdits", "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ó", - "credit_worth_description": "Un crèdit val 1¢ (USD). <0>Més informació", "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", @@ -3424,13 +3386,10 @@ "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", @@ -3508,11 +3467,6 @@ "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", @@ -3640,7 +3594,6 @@ "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", @@ -3672,9 +3625,6 @@ "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", @@ -3682,8 +3632,6 @@ "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.", @@ -3704,32 +3652,5 @@ "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index 07a69416b68fb8..100ef5ecb1567f 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -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,8 +819,6 @@ "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.", @@ -848,7 +846,6 @@ "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", @@ -879,9 +876,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_phone_numbers": "Telefonní čísla Cal AI", "connect_phone_number": "Připojit telefonní číslo", - "cal_ai_phone_numbers_description": "Spravujte svá telefonní čísla", + "cal_ai_phone_numbers_description": "Spravujte svá telefonní čísla Cal AI", "import_number": "Importovat číslo", "this_action_will_also": "Tato akce také:", "import_phone_number": "Importovat telefonní číslo", @@ -889,7 +886,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 z Twilio pro použití s telefonem", + "import_phone_number_description": "Importujte své telefonní číslo Twilio pro použití s Phone", "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.", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "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", @@ -1445,7 +1440,6 @@ "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ěč", @@ -1722,8 +1716,6 @@ "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", @@ -1761,7 +1753,6 @@ "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", @@ -1770,18 +1761,6 @@ "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.", @@ -2205,8 +2184,6 @@ "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í.", @@ -2364,6 +2341,7 @@ "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", @@ -2486,15 +2464,7 @@ "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", @@ -2782,8 +2752,6 @@ "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ý", @@ -3235,15 +3203,9 @@ "verify_email_change": "Ověřit změnu e-mailu", "buy_credits": "Koupit kredity", "credits": "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": "Zobrazit a spravovat kredity", "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", - "credit_worth_description": "Jeden kredit má hodnotu 1¢ (USD). <0>Zjistit více", "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", @@ -3424,13 +3386,10 @@ "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", @@ -3508,11 +3467,6 @@ "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í", @@ -3640,7 +3594,6 @@ "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", @@ -3672,9 +3625,6 @@ "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", @@ -3682,8 +3632,6 @@ "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.", @@ -3704,32 +3652,5 @@ "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/da/common.json b/apps/web/public/static/locales/da/common.json index 3d46b3dfd88554..49315f86ad667c 100644 --- a/apps/web/public/static/locales/da/common.json +++ b/apps/web/public/static/locales/da/common.json @@ -819,8 +819,6 @@ "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.", @@ -848,7 +846,6 @@ "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", @@ -879,9 +876,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": "Telefonnumre", + "cal_ai_phone_numbers": "Cal AI-telefonnumre", "connect_phone_number": "Forbind telefonnummer", - "cal_ai_phone_numbers_description": "Administrer dine telefonnumre", + "cal_ai_phone_numbers_description": "Administrer dine Cal AI-telefonnumre", "import_number": "Importer nummer", "this_action_will_also": "Denne handling vil også:", "import_phone_number": "Importer telefonnummer", @@ -889,7 +886,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 Phone", + "import_phone_number_description": "Importer dit Twilio-telefonnummer til brug med Cal AI-telefon", "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.", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "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", @@ -1445,7 +1440,6 @@ "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", @@ -1722,8 +1716,6 @@ "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", @@ -1761,7 +1753,6 @@ "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", @@ -1770,18 +1761,6 @@ "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.", @@ -2205,8 +2184,6 @@ "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.", @@ -2364,6 +2341,7 @@ "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", @@ -2486,15 +2464,7 @@ "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", @@ -2782,8 +2752,6 @@ "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", @@ -3235,15 +3203,9 @@ "verify_email_change": "Bekræft ændring af e-mail", "buy_credits": "Køb credits", "credits": "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": "Se og administrer credits", "view_and_manage_credits_description": "Se og administrer credits til at sende SMS-beskeder. Én credit er 1¢ (USD). <0>Læs mere", - "credit_worth_description": "Én credit er 1¢ (USD) værd. <0>Læs mere", "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", @@ -3424,13 +3386,10 @@ "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", @@ -3508,11 +3467,6 @@ "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", @@ -3640,7 +3594,6 @@ "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", @@ -3672,9 +3625,6 @@ "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", @@ -3682,8 +3632,6 @@ "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.", @@ -3704,32 +3652,5 @@ "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index 61166363922b4f..e9fc67d666aa01 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -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": "Assistent", + "cal_ai_assistant": "Cal KI-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,8 +819,6 @@ "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.", @@ -848,7 +846,6 @@ "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", @@ -881,7 +878,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": "Verwalte deine Telefonnummern", + "cal_ai_phone_numbers_description": "Verwalten Sie Ihre Telefonnummern", "import_number": "Nummer importieren", "this_action_will_also": "Diese Aktion wird auch:", "import_phone_number": "Telefonnummer importieren", @@ -889,7 +886,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": "Importiere deine Twilio-Telefonnummer zur Verwendung mit Telefon", + "import_phone_number_description": "Importieren Sie Ihre Twilio-Telefonnummer zur Verwendung mit Phone", "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.", @@ -900,7 +897,6 @@ "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", @@ -1211,7 +1207,6 @@ "categories": "Kategorien", "pricing": "Preise", "learn_more": "Mehr erfahren", - "try_now": "Jetzt ausprobieren", "privacy_policy": "Datenschutzerklärung", "terms_of_service": "Nutzungsbedingungen", "remove": "Entfernen", @@ -1445,7 +1440,6 @@ "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", @@ -1722,8 +1716,6 @@ "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", @@ -1761,7 +1753,6 @@ "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", @@ -1770,18 +1761,6 @@ "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.", @@ -2205,8 +2184,6 @@ "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.", @@ -2364,6 +2341,7 @@ "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", @@ -2486,15 +2464,7 @@ "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", @@ -2782,8 +2752,6 @@ "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", @@ -3235,15 +3203,9 @@ "verify_email_change": "E-Mail-Änderung bestätigen", "buy_credits": "Credits kaufen", "credits": "Credits", - "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": "Credits 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", - "credit_worth_description": "Ein Credit entspricht 1¢ (USD). <0>Mehr erfahren", "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", @@ -3424,13 +3386,10 @@ "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", @@ -3508,11 +3467,6 @@ "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", @@ -3640,7 +3594,6 @@ "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", @@ -3672,9 +3625,6 @@ "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", @@ -3682,8 +3632,6 @@ "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.", @@ -3704,32 +3652,5 @@ "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/el/common.json b/apps/web/public/static/locales/el/common.json index 60398b22274b19..3261b1c7eb31a7 100644 --- a/apps/web/public/static/locales/el/common.json +++ b/apps/web/public/static/locales/el/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Επαληθεύστε τη διεύθυνση email σας για να εξασφαλίσετε την καλύτερη δυνατή παράδοση email και ημερολογίου", "verify_email_email_header": "Επιβεβαιώστε τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας", "verify_email_button": "Επαλήθευση email", - "cal_ai_assistant": "Βοηθός", + "cal_ai_assistant": "Βοηθός Cal AI", "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,8 +819,6 @@ "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": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον αριθμό τηλεφώνου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Η συνδρομή του αριθμού τηλεφώνου ακυρώθηκε με επιτυχία", "updating": "Ενημέρωση", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Γεια, πώς είστε;", "round_robin_description": "Κυκλική εναλλαγή συναντήσεων μεταξύ πολλών μελών της ομάδας.", "managed_event": "Διαχειριζόμενη εκδήλωση", "username_placeholder": "όνομα χρήστη", @@ -879,9 +876,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_phone_numbers": "Αριθμοί τηλεφώνου Cal AI", "connect_phone_number": "Σύνδεση αριθμού τηλεφώνου", - "cal_ai_phone_numbers_description": "Διαχειριστείτε τους αριθμούς τηλεφώνου σας", + "cal_ai_phone_numbers_description": "Διαχειριστείτε τους αριθμούς τηλεφώνου Cal AI", "import_number": "Εισαγωγή αριθμού", "this_action_will_also": "Αυτή η ενέργεια επίσης θα:", "import_phone_number": "Εισαγωγή αριθμού τηλεφώνου", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Ακύρωση της συνδρομής του αριθμού τηλεφώνου σας", "delete_associated_phone_number": "Διαγραφή του συσχετισμένου αριθμού τηλεφώνου", "unauthorized_create_workflow": "Δεν έχετε εξουσιοδότηση για τη δημιουργία αυτής της ροής εργασίας", - "import_phone_number_description": "Εισαγάγετε τον αριθμό τηλεφώνου Twilio για χρήση με το Τηλέφωνο", + "import_phone_number_description": "Εισαγάγετε τον αριθμό τηλεφώνου Twilio για χρήση με το Phone", "phone_number_cost": "${{price}}/μήνα", "buy_new_number": "Αγορά νέου αριθμού", "buy_number_cost_x_per_month": "Η αγορά ενός αριθμού τηλεφώνου κοστίζει ${{priceInDollars}} ανά μήνα. Θα χρεώνεστε μηνιαία για κάθε ενεργό αριθμό τηλεφώνου.", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "Κατηγορίες", "pricing": "Τιμολόγηση", "learn_more": "Μάθετε περισσότερα", - "try_now": "Δοκιμάστε τώρα", "privacy_policy": "Πολιτική απορρήτου", "terms_of_service": "Όροι χρήσης", "remove": "Αφαίρεση", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "αποστολή μηνύματος WhatsApp στον συμμετέχοντα", "workflows": "Ροές εργασίας", "new_workflow_btn": "Νέα ροή εργασίας", - "how_would_you_like_to_start": "Πώς θα θέλατε να ξεκινήσετε;", "add_new_workflow": "Προσθήκη νέας ροής εργασίας", "reschedule_event_trigger": "όταν επαναπρογραμματίζεται το συμβάν", "trigger": "Έναυσμα", @@ -1722,8 +1716,6 @@ "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": "Πρόσθετες σημειώσεις", @@ -1761,7 +1753,6 @@ "team_url": "URL ομάδας", "team_members": "Μέλη ομάδας", "more": "Περισσότερα", - "cal_ai_workflows": "Ροές εργασίας Cal.ai", "and_count_more": "και {{count}} ακόμη", "more_page_footer": "Θεωρούμε την εφαρμογή για κινητά ως επέκταση της διαδικτυακής εφαρμογής. Εάν εκτελείτε περίπλοκες ενέργειες, παρακαλούμε ανατρέξτε στη διαδικτυακή εφαρμογή.", "workflow_example_1": "Αποστολή υπενθύμισης SMS 24 ώρες πριν την έναρξη της εκδήλωσης στον συμμετέχοντα", @@ -1770,18 +1761,6 @@ "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": "Θα μπορείτε να το επεξεργαστείτε αργότερα.", @@ -2205,8 +2184,6 @@ "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": "Αυτός ο σύνδεσμος ομάδας δεν είναι διαθέσιμος αυτή τη στιγμή. Παρακαλούμε επικοινωνήστε με τον ιδιοκτήτη της ομάδας ή ζητήστε του να τον δημοσιεύσει.", @@ -2364,6 +2341,7 @@ "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": "ημέρες", @@ -2486,15 +2464,7 @@ "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 σας", @@ -2782,8 +2752,6 @@ "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": "Σήμανση ως μη εμφάνιση", @@ -3235,15 +3203,9 @@ "verify_email_change": "Επαλήθευση αλλαγής email", "buy_credits": "Αγορά μονάδων", "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": "Προβολή και διαχείριση μονάδων", "view_and_manage_credits_description": "Προβολή και διαχείριση μονάδων για αποστολή μηνυμάτων SMS. Μία μονάδα αξίζει 1¢ (USD). <0>Μάθετε περισσότερα", - "credit_worth_description": "Μία πίστωση αξίζει 1¢ (USD). <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": "Δεν μπορείτε να δημιουργήσετε έναν οργανισμό καθώς είστε ήδη μέλος ενός οργανισμού", @@ -3424,13 +3386,10 @@ "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 σε", @@ -3508,11 +3467,6 @@ "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": "Προβολή τύπων εκδηλώσεων", @@ -3640,7 +3594,6 @@ "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": "Η ώρα λήξης της εκδήλωσης", @@ -3672,9 +3625,6 @@ "visit": "Επίσκεψη", "location_custom_label_input_label": "Προσαρμοσμένη ετικέτα στη σελίδα κράτησης", "meeting_link": "Σύνδεσμος συνάντησης", - "session_outcome": "Αποτέλεσμα Συνεδρίας", - "call_created": "Δημιουργήθηκε Κλήση", - "voicemail": "Φωνητικό Μήνυμα", "my_bookings": "Οι κρατήσεις μου", "phone": "Τηλέφωνο", "free": "Δωρεάν", @@ -3682,8 +3632,6 @@ "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": "Σκεφτείτε να δημιουργήσετε έναν οργανισμό για να ενοποιήσετε τη χρέωση, τα εργαλεία διαχείρισης και τα αναλυτικά στοιχεία σε όλες τις ομάδες σας.", @@ -3704,32 +3652,5 @@ "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 218524b1fff983..1225495428bfbb 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -900,7 +900,6 @@ "yes_cancel_subscription": "Yes, Cancel Subscription", "cancel_phone_number_subscription_confirmation": "Are you sure you want to cancel this phone number subscription? This action cannot be undone and you will lose access to this phone number.", "add_members": "Add members...", - "add_members_no_ellipsis": "Add members", "no_assigned_members": "No assigned members", "assigned_to": "Assigned to", "you_must_be_logged_in_to": "You must be logged in to {{url}}", @@ -2202,7 +2201,6 @@ "booking_confirmation_failed": "Booking confirmation failed", "not_enough_seats": "Not enough seats", "form_builder_field_already_exists": "A field with this name already exists", - "guests_field_must_be_multiemail": "Guests field must be of type 'Multiple emails'", "show_on_booking_page": "Show on booking page", "visit_cancelled_booking": "You can visit the canceled booking page", "get_started_zapier_templates": "Get started with Zapier templates", @@ -3236,15 +3234,9 @@ "verify_email_change": "Verify email change", "buy_credits": "Buy Credits", "credits": "Credits", - "credits_used": "Credits used", - "total_credits_remaining": "Total remaining", - "credits_per_tip_org": "You receive 1000 credits per month, per team member", - "credits_per_tip_teams": "You receive 750 credits per month, per team member", - "view_and_manage_credits": "View and manage credits for sending SMS messages", + "view_and_manage_credits": "View and manage credits", "view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more", - "credit_worth_description": "One credit is worth 1¢ (USD). <0>Learn more", "buy_additional_credits": "Buy additional credits ($0.01 per credit)", - "view_additional_credits_expense_tip": "You can view Additional credit spending in your expense log", "overview": "Overview", "organization_slug_taken": "Organization slug is already taken", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "You cannot create an organization as you are already a part of an organization", @@ -3425,13 +3417,10 @@ "credit_limit_reached_message": "Your Cal.com team {{teamName}} has run out of credits. As a result, SMS messages are now being sent via email instead. To resume sending SMS, please purchase additional credits.", "credit_limit_reached_message_user": "Your Cal.com account has run out of credits. As a result, SMS messages are now being sent via email instead. To resume sending SMS, please purchase additional credits.", "current_credit_balance": "Current balance: {{balance}} credits", - "current_balance": "Current balance:", "notification_about_your_booking": "Notification about your booking", "monthly_credits": "Monthly credits", "total_credits": "Total credits: {{totalCredits}}", "remaining_credits": "Remaining credits: {{remainingCredits}}", - "remaining": "Remaining", - "total": "Total", "additional_credits": "Additional credits", "routing_form_next_in_queue": "{{count}} next in queue", "routing_form_select_members_to_email": "Send email responses to", @@ -3509,11 +3498,6 @@ "pbac_desc_view_workflows": "View existing workflows and their configurations", "pbac_desc_update_workflows": "Edit and modify workflow settings", "pbac_desc_delete_workflows": "Remove workflows from the system", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Create webhooks", - "pbac_desc_view_webhooks": "View webhooks", - "pbac_desc_update_webhooks": "Update webhooks", - "pbac_desc_delete_webhooks": "Delete webhooks", "pbac_desc_manage_workflows": "Full management access to all workflows", "pbac_desc_create_event_types": "Create event types", "pbac_desc_view_event_types": "View event types", @@ -3705,10 +3689,6 @@ "before_scheduled_start_time": "Before scheduled start time", "cancel_booking_acknowledge_no_show_fee": "I acknowledge that by cancelling the booking within {{timeValue}} {{timeUnit}} of the start time I will be charged the no show fee of {{amount, currency}}", "contact_organizer": "If you have any questions, please contact the organizer.", - "booking_time_option": "Booking time", - "booking_time_option_description": "When the booking is scheduled (start to end)", - "created_at_option": "Created at", - "created_at_option_description": "When the booking was originally created", "call_details": "Call Details", "call_id": "Call ID", "call_information": "Call Information", diff --git a/apps/web/public/static/locales/es-419/common.json b/apps/web/public/static/locales/es-419/common.json index a657a162d5e29f..5c93596438211e 100644 --- a/apps/web/public/static/locales/es-419/common.json +++ b/apps/web/public/static/locales/es-419/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Verifica tu dirección de correo electrónico para garantizar la mejor entrega de correos y calendarios", "verify_email_email_header": "Verifica tu dirección de correo electrónico", "verify_email_button": "Verificar correo electrónico", - "cal_ai_assistant": "Asistente", + "cal_ai_assistant": "Asistente de IA de Cal", "send_cal_video_transcription_emails": "Enviar correos con transcripción de Cal Video", "description_send_cal_video_transcription_emails": "Enviar correos con la transcripción del Cal Video después de que finalice la reunión. (Requiere un plan de pago)", "verify_email_change_description": "Recientemente has solicitado cambiar la dirección de correo electrónico que usas para iniciar sesión en tu cuenta de {{appName}}. Por favor, haz clic en el botón de abajo para confirmar tu nueva dirección de correo electrónico.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "La validación del flujo de trabajo falló", "workflow_validation_empty_fields": "Uno o más pasos del flujo de trabajo tienen contenido de mensaje vacío", "workflow_validation_unverified_contacts": "Uno o más números de teléfono o direcciones de correo electrónico no están verificados", - "supercharge_your_workflows_with_cal_ai": "Potencia tus flujos de trabajo con Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Agentes de IA realistas que programan reuniones, envían recordatorios y hacen seguimiento con tus clientes.", "phone_number_imported_successfully": "Número de teléfono importado y vinculado al agente con éxito", "phone_number_deleted_successfully": "Número de teléfono eliminado con éxito", "delete_phone_number_confirmation": "¿Estás seguro de que quieres eliminar este número de teléfono? Esta acción no se puede deshacer.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Suscripción del número de teléfono cancelada exitosamente", "updating": "Actualizando", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Hola, ¿cómo estás?", "round_robin_description": "Alterna reuniones entre varios miembros del equipo.", "managed_event": "Evento gestionado", "username_placeholder": "nombre de usuario", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "¿Estás seguro de que deseas eliminar este paso del flujo de trabajo?", "do_you_still_want_to_unsubscribe": "¿Aún deseas cancelar la suscripción del número telefónico de este agente?", "the_action_will_disconnect_phone_number": "Esta acción desconectará el número telefónico del agente. El agente no podrá realizar llamadas hasta que se conecte un nuevo número telefónico.", - "cal_ai_phone_numbers": "Números de teléfono", + "cal_ai_phone_numbers": "Números telefónicos de Cal AI", "connect_phone_number": "Conectar número telefónico", - "cal_ai_phone_numbers_description": "Administra tus números de teléfono", + "cal_ai_phone_numbers_description": "Administra tus números telefónicos de Cal AI", "import_number": "Importar número", "this_action_will_also": "Esta acción también:", "import_phone_number": "Importar número telefónico", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Cancelar tu suscripción de número telefónico", "delete_associated_phone_number": "Eliminar el número telefónico asociado", "unauthorized_create_workflow": "No estás autorizado para crear este flujo de trabajo", - "import_phone_number_description": "Importa tu número de teléfono de Twilio para usar con Phone", + "import_phone_number_description": "Importa tu número telefónico de Twilio para usarlo con Phone", "phone_number_cost": "${{price}}/mes", "buy_new_number": "Comprar nuevo número", "buy_number_cost_x_per_month": "Comprar un número telefónico cuesta ${{priceInDollars}} al mes. Se te cobrará mensualmente por cada número telefónico activo.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Sí, cancelar suscripción", "cancel_phone_number_subscription_confirmation": "¿Está seguro de que desea cancelar esta suscripción de número de teléfono? Esta acción no se puede deshacer y perderá acceso a este número de teléfono.", "add_members": "Agregar miembros...", - "add_members_no_ellipsis": "Agregar miembros", "no_assigned_members": "No hay miembros asignados", "assigned_to": "Asignado a", "you_must_be_logged_in_to": "Debes iniciar sesión en {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Categorías", "pricing": "Precios", "learn_more": "Aprender más", - "try_now": "Prueba ahora", "privacy_policy": "Política de privacidad", "terms_of_service": "Términos de servicio", "remove": "Eliminar", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "enviar mensaje de WhatsApp al asistente", "workflows": "Flujos de trabajo", "new_workflow_btn": "Nuevo Flujo de Trabajo", - "how_would_you_like_to_start": "¿Cómo te gustaría comenzar?", "add_new_workflow": "Agregar un nuevo flujo de trabajo", "reschedule_event_trigger": "cuando el evento se reprograma", "trigger": "Disparador", @@ -1722,8 +1716,6 @@ "event_duration_info": "La duración del evento", "event_time_info": "La hora de inicio del evento", "event_type_not_found": "Tipo de evento no encontrado", - "number_to_call_variable": "Número para llamar", - "number_to_call_info": "El número de teléfono del usuario al que estás llamando", "location_variable": "Ubicación", "location_info": "La ubicación del evento", "additional_notes_variable": "Notas adicionales", @@ -1761,7 +1753,6 @@ "team_url": "URL del equipo", "team_members": "Miembros del equipo", "more": "Más", - "cal_ai_workflows": "Flujos de trabajo de Cal.ai", "and_count_more": "y {{count}} más", "more_page_footer": "Consideramos la aplicación móvil como una extensión de la aplicación web. Si estás realizando acciones complicadas, por favor, vuelve a la aplicación web.", "workflow_example_1": "Enviar recordatorio por SMS 24 horas antes de que comience el evento al asistente", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Enviar recordatorio por correo electrónico 1 hora antes de que comiencen los eventos al asistente", "workflow_example_5": "Enviar correo electrónico personalizado cuando el evento se reprograma al anfitrión", "workflow_example_6": "Enviar SMS personalizado cuando se reserva un nuevo evento al anfitrión", - "send_sms_reminder": "Enviar recordatorio por SMS", - "send_sms_reminder_description": "24 horas antes de que comience el evento", - "follow_up_with_no_shows": "Seguimiento a los que no asistieron", - "follow_up_with_no_shows_description": "30 minutos después de que finalice el evento", - "remind_attendees_to_bring_id": "Recordar a los asistentes que traigan identificación", - "remind_attendees_to_bring_id_description": "1 día antes de que comience el evento", - "email_to_remind_booking": "Recordatorio por correo electrónico", - "email_to_remind_booking_description": "1 hora antes de que comience el evento", - "custom_sms_reminder": "Recordatorio SMS personalizado", - "custom_sms_reminder_description": "Cuando se programa el evento", - "custom_email_reminder": "Recordatorio por correo electrónico personalizado", - "custom_email_reminder_description": "Evento reprogramado para el anfitrión", "count_managed_to_limit": "Incluir el conteo de reservas de los tipos de eventos gestionados", "welcome_to_cal_header": "¡Bienvenido a {{appName}}!", "edit_form_later_subtitle": "Podrás editarlo más tarde.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Mostrar en la página de reserva", "visit_cancelled_booking": "Puedes visitar la página de la reserva cancelada", "get_started_zapier_templates": "Comienza con las plantillas de Zapier", - "standard_templates": "Plantillas estándar", - "cal_ai_templates": "Plantillas de Cal.ai", "team_is_unpublished": "{{team}} no está publicado", "org_is_unpublished_description": "Este enlace de la organización no está disponible actualmente. Por favor, contacta al propietario de la organización o pídele que lo publique.", "team_is_unpublished_description": "Este enlace del equipo no está disponible actualmente. Por favor, contacta al propietario del equipo o pídele que lo publique.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "No se pudo cargar la tarjeta para el pago.", "insights": "Perspectivas", "routing_forms": "Formularios de Enrutamiento", + "testing_workflow_info_message": "Al probar este flujo de trabajo, tenga en cuenta que los correos electrónicos y SMS solo se pueden programar con al menos 1 hora de anticipación", "insights_no_data_found_for_filter": "No se encontraron datos para el filtro o las fechas seleccionadas.", "acknowledge_booking_no_show_fee": "Reconozco que si no asisto a este evento, se aplicará una tarifa de no presentación de {{amount, currency}} a mi tarjeta.", "days": "días", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Equipo: {{teamName}}", "insights_user_filter": "Usuario: {{userName}}", "insights_subtitle": "Ver información de reservas a través de tus eventos", - "call_history": "Historial de llamadas", - "call_history_subtitle": "Ver historial de llamadas de tus llamadas con Cal.ai", "location_options": "{{locationCount}} opciones de ubicación", - "channel_type": "Tipo de canal", - "end_reason": "Motivo de finalización", - "session_status": "Estado de la sesión", - "user_sentiment": "Sentimiento del usuario", - "time_header": "Hora", - "from_header": "De", "custom_plan": "Plan Personalizado", "email_embed": "Incrustar Email", "add_times_to_your_email": "Selecciona algunos horarios disponibles e insértalos en tu correo electrónico", @@ -2782,8 +2752,6 @@ "account_already_linked": "La cuenta ya está vinculada", "send_email": "Enviar correo electrónico", "cal_ai_phone_call_action": "Llamar al asistente usando el agente de voz Cal.ai", - "call_to_confirm_booking": "Llamada para confirmar reserva", - "cal_ai_phone_call_action_description": "2 horas antes del inicio del evento", "cal_ai_agent_configuration": "Configuración del agente Cal.ai", "choose_at_least_one_event_type_test_call": "Por favor elige al menos un tipo de evento para hacer una llamada de prueba.", "mark_as_no_show": "Marcar como no presentado", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verificar cambio de correo electrónico", "buy_credits": "Comprar créditos", "credits": "Créditos", - "credits_used": "Créditos utilizados", - "total_credits_remaining": "Total restante", - "credits_per_tip_org": "Recibes 1000 créditos por mes, por miembro del equipo", - "credits_per_tip_teams": "Recibes 750 créditos por mes, por miembro del equipo", - "view_and_manage_credits": "Ver y administrar créditos para enviar mensajes SMS", + "view_and_manage_credits": "Ver y administrar créditos", "view_and_manage_credits_description": "Ver y administrar créditos para enviar mensajes SMS. Un crédito equivale a 1¢ (USD). <0>Más información", - "credit_worth_description": "Un crédito vale 1¢ (USD). <0>Más información", "buy_additional_credits": "Comprar créditos adicionales ($0.01 por crédito)", - "view_additional_credits_expense_tip": "Puedes ver el gasto de créditos adicionales en tu registro de gastos", "overview": "Resumen", "organization_slug_taken": "El slug de la organización ya está en uso", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "No puedes crear una organización ya que ya eres parte de una organización", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Tu equipo de Cal.com {{teamName}} se ha quedado sin créditos. Como resultado, los mensajes SMS ahora se están enviando por correo electrónico. Para reanudar el envío de SMS, por favor compra créditos adicionales.", "credit_limit_reached_message_user": "Tu cuenta de Cal.com se ha quedado sin créditos. Como resultado, los mensajes SMS ahora se envían por correo electrónico. Para reanudar el envío de SMS, por favor compra créditos adicionales.", "current_credit_balance": "Saldo actual: {{balance}} créditos", - "current_balance": "Saldo actual:", "notification_about_your_booking": "Notificación sobre tu reserva", "monthly_credits": "Créditos mensuales", "total_credits": "Créditos totales: {{totalCredits}}", "remaining_credits": "Créditos restantes: {{remainingCredits}}", - "remaining": "Restante", - "total": "Total", "additional_credits": "Créditos adicionales", "routing_form_next_in_queue": "{{count}} siguiente(s) en cola", "routing_form_select_members_to_email": "Enviar respuestas por correo electrónico a", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Ver flujos de trabajo existentes y sus configuraciones", "pbac_desc_update_workflows": "Editar y modificar configuraciones de flujos de trabajo", "pbac_desc_delete_workflows": "Eliminar flujos de trabajo del sistema", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Crear webhooks", - "pbac_desc_view_webhooks": "Ver webhooks", - "pbac_desc_update_webhooks": "Actualizar webhooks", - "pbac_desc_delete_webhooks": "Eliminar webhooks", "pbac_desc_manage_workflows": "Acceso completo de gestión a todos los flujos de trabajo", "pbac_desc_create_event_types": "Crear tipos de eventos", "pbac_desc_view_event_types": "Ver tipos de eventos", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "El nombre del evento desencadenante (por ejemplo, BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "La hora del webhook", "webhook_type": "El slug del tipo de evento", - "set_up_agent": "Configurar agente", "webhook_title": "El nombre del tipo de evento", "webhook_start_time": "La hora de inicio del evento", "webhook_end_time": "La hora de finalización del evento", @@ -3672,9 +3625,6 @@ "visit": "Visitar", "location_custom_label_input_label": "Etiqueta personalizada en la página de reservas", "meeting_link": "Enlace de la reunión", - "session_outcome": "Resultado de la sesión", - "call_created": "Llamada creada", - "voicemail": "Buzón de voz", "my_bookings": "Mis reservas", "phone": "Teléfono", "free": "Gratis", @@ -3682,8 +3632,6 @@ "user_name": "Nombre del usuario", "expand_panel": "Expandir panel", "collapse_panel": "Contraer panel", - "email_verification_required": "Se requiere verificación de correo electrónico para este tipo de evento", - "invalid_verification_code": "Código de verificación proporcionado no válido", "you_have_one_team": "Tienes un equipo", "consider_consolidating_one_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tu equipo.", "consider_consolidating_multi_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tus equipos.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Antes de la hora de inicio programada", "cancel_booking_acknowledge_no_show_fee": "Reconozco que al cancelar la reserva dentro de los {{timeValue}} {{timeUnit}} previos a la hora de inicio, se me cobrará la tarifa por no presentarme de {{amount, currency}}", "contact_organizer": "Si tienes alguna pregunta, comunícate con el organizador.", - "booking_time_option": "Hora de reserva", - "booking_time_option_description": "Cuando se programa la reserva (inicio a fin)", - "created_at_option": "Creado el", - "created_at_option_description": "Cuando se creó originalmente la reserva", - "call_details": "Detalles de la llamada", - "call_id": "ID de llamada", - "call_information": "Información de la llamada", - "sentiment": "Sentimiento", - "disconnect_reason": "Motivo de desconexión", - "call_summary": "Resumen de la llamada", - "transcription": "Transcripción", - "event_details": "Detalles del evento", - "agent": "Agente", - "no_transcript_available": "No hay transcripción disponible", - "testing_sms_workflow_info_message": "Al probar este flujo de trabajo, ten en cuenta que los SMS deben programarse con al menos 15 minutos de anticipación", - "start_from_scratch_title": "Comenzar desde cero", - "start_from_scratch_description": "Crea tu propio flujo de trabajo desde cero.", - "cal_ai_template_title": "Plantilla de Cal.ai", - "cal_ai_template_description": "¡Agentes de IA que programan reuniones, envían recordatorios y hacen seguimiento!", - "voice": "Voz", - "select_voice": "Seleccionar voz", - "select_voice_for_agent": "Selecciona una voz para tu agente", - "choose_a_voice_for_your_agent": "Elige una voz para tu agente", - "trait": "Característica", - "voice_id": "ID de voz", - "use_voice": "Usar voz", - "current_voice": "Voz actual", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 0b40cb3d66ed64..13b5dd93946ea6 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Verifique su dirección de correo electrónico para garantizar la mejor entrega de correo electrónico y calendario", "verify_email_email_header": "Verifique su dirección de correo electrónico", "verify_email_button": "Verificar correo electrónico", - "cal_ai_assistant": "Asistente", + "cal_ai_assistant": "Asistente AI de Cal", "send_cal_video_transcription_emails": "Enviar correos con transcripción de Cal Video", "description_send_cal_video_transcription_emails": "Enviar correos electrónicos con la transcripción del Cal Video después de que finalice la reunión. (Requiere un plan de pago)", "verify_email_change_description": "Recientemente has solicitado cambiar la dirección de correo electrónico que usas para iniciar sesión en tu cuenta de {{appName}}. Por favor, haz clic en el botón de abajo para confirmar tu nueva dirección de correo electrónico.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "La validación del flujo de trabajo falló", "workflow_validation_empty_fields": "Uno o más pasos del flujo de trabajo tienen contenido de mensaje vacío", "workflow_validation_unverified_contacts": "Uno o más números de teléfono o direcciones de correo electrónico no están verificados", - "supercharge_your_workflows_with_cal_ai": "Potencia tus flujos de trabajo con Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Agentes de IA realistas que programan reuniones, envían recordatorios y hacen seguimiento con tus clientes.", "phone_number_imported_successfully": "Número de teléfono importado y vinculado al agente con éxito", "phone_number_deleted_successfully": "Número de teléfono eliminado con éxito", "delete_phone_number_confirmation": "¿Estás seguro de que quieres eliminar este número de teléfono? Esta acción no se puede deshacer.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Suscripción del número de teléfono cancelada correctamente", "updating": "Actualizando", "round_robin": "Petición Firmada por Turnos", - "hi_how_are_you_doing": "Hola, ¿cómo estás?", "round_robin_description": "Ciclo de reuniones entre varios miembros del equipo.", "managed_event": "Evento gestionado", "username_placeholder": "nombre de usuario", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "¿Está seguro que desea eliminar este paso del flujo de trabajo?", "do_you_still_want_to_unsubscribe": "¿Todavía desea dar de baja el número de teléfono de este agente?", "the_action_will_disconnect_phone_number": "Esta acción desconectará el número de teléfono del agente. El agente no podrá realizar llamadas hasta que se conecte un nuevo número de teléfono.", - "cal_ai_phone_numbers": "Números de teléfono", + "cal_ai_phone_numbers": "Números de teléfono de Cal AI", "connect_phone_number": "Conectar número de teléfono", - "cal_ai_phone_numbers_description": "Gestiona tus números de teléfono", + "cal_ai_phone_numbers_description": "Gestione sus números de teléfono de Cal AI", "import_number": "Importar número", "this_action_will_also": "Esta acción también:", "import_phone_number": "Importar número de teléfono", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Cancelar su suscripción de número de teléfono", "delete_associated_phone_number": "Eliminar el número de teléfono asociado", "unauthorized_create_workflow": "No está autorizado para crear este flujo de trabajo", - "import_phone_number_description": "Importa tu número de teléfono de Twilio para usar con Phone", + "import_phone_number_description": "Importe su número de teléfono de Twilio para usar con Phone", "phone_number_cost": "${{price}}/mes", "buy_new_number": "Comprar nuevo número", "buy_number_cost_x_per_month": "Comprar un número de teléfono cuesta ${{priceInDollars}} al mes. Se le cobrará mensualmente por cada número de teléfono activo.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Sí, cancelar suscripción", "cancel_phone_number_subscription_confirmation": "¿Está seguro de que desea cancelar esta suscripción de número de teléfono? Esta acción no se puede deshacer y perderá el acceso a este número de teléfono.", "add_members": "Agregar miembros...", - "add_members_no_ellipsis": "Agregar miembros", "no_assigned_members": "No hay miembros asignados", "assigned_to": "Asignado a", "you_must_be_logged_in_to": "Debes iniciar sesión en {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Categorías", "pricing": "Precios", "learn_more": "Más información", - "try_now": "Prueba ahora", "privacy_policy": "Política de privacidad", "terms_of_service": "Términos de servicio", "remove": "Eliminar", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "enviar un mensae de Whatsapp al asistente", "workflows": "Flujos de trabajo", "new_workflow_btn": "Flujo de trabajo nuevo", - "how_would_you_like_to_start": "¿Cómo te gustaría empezar?", "add_new_workflow": "Agregar un flujo de trabajo nuevo", "reschedule_event_trigger": "cuando el evento se vuelva a programar", "trigger": "Desencadenante", @@ -1722,8 +1716,6 @@ "event_duration_info": "La duración del evento", "event_time_info": "Hora de inicio del evento", "event_type_not_found": "Tipo de evento no encontrado", - "number_to_call_variable": "Número para llamar", - "number_to_call_info": "El número de teléfono del usuario al que estás llamando", "location_variable": "Ubicación", "location_info": "Ubicación del evento", "additional_notes_variable": "Notas adicionales", @@ -1761,7 +1753,6 @@ "team_url": "URL del equipo", "team_members": "Miembros del equipo", "more": "Más", - "cal_ai_workflows": "Flujos de trabajo de Cal.ai", "and_count_more": "y {{count}} más", "more_page_footer": "Consideramos la aplicación móvil como una extensión de la aplicación web. Si está realizando acciones complicadas, consulta la aplicación web.", "workflow_example_1": "Enviar recordatorio por SMS al asistente, 24 horas antes de que el evento comience", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Enviar recordatorio por correo electrónico al asistente, 1 hora antes de que el evento comience", "workflow_example_5": "Enviar correo electrónico personalizado al anfitrión cuando se reprograme el evento", "workflow_example_6": "Enviar SMS personalizado al anfitrión cuando se reserve un evento nuevo", - "send_sms_reminder": "Enviar recordatorio por SMS", - "send_sms_reminder_description": "24 horas antes del inicio del evento", - "follow_up_with_no_shows": "Seguimiento de ausencias", - "follow_up_with_no_shows_description": "30 minutos después de finalizar el evento", - "remind_attendees_to_bring_id": "Recordar a los asistentes que traigan identificación", - "remind_attendees_to_bring_id_description": "1 día antes del inicio del evento", - "email_to_remind_booking": "Recordatorio por email", - "email_to_remind_booking_description": "1 hora antes del inicio del evento", - "custom_sms_reminder": "Recordatorio SMS personalizado", - "custom_sms_reminder_description": "Cuando se programa el evento", - "custom_email_reminder": "Recordatorio por email personalizado", - "custom_email_reminder_description": "Evento reprogramado para el anfitrión", "count_managed_to_limit": "Incluir el conteo de reservas de los tipos de eventos gestionados", "welcome_to_cal_header": "¡Bienvenido a {{appName}}!", "edit_form_later_subtitle": "Podrás editarlo más tarde.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Mostrar en la página de reserva", "visit_cancelled_booking": "Puedes visitar la página de la reserva cancelada", "get_started_zapier_templates": "Comience con las plantillas de Zapier", - "standard_templates": "Plantillas estándar", - "cal_ai_templates": "Plantillas de Cal.ai", "team_is_unpublished": "{{team}} no está publicado", "org_is_unpublished_description": "El enlace de esta organización no está disponible actualmente. Comuníquese con el propietario de la organización o pídale que lo publique.", "team_is_unpublished_description": "Este enlace de {{entity}} no está disponible actualmente. Póngase en contacto con el propietario de {{entity}} o pídale que lo publique.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "No se pudo cargar la tarjeta para el pago.", "insights": "Perspectivas", "routing_forms": "Formularios de enrutamiento", + "testing_workflow_info_message": "Al probar este flujo de trabajo, tenga en cuenta que los correos electrónicos y los SMS solo se pueden programar con al menos 1 hora de anticipación", "insights_no_data_found_for_filter": "No se encontraron datos para el filtro seleccionado o las fechas seleccionadas.", "acknowledge_booking_no_show_fee": "Reconozco que si no asisto a este evento, se aplicará a mi tarjeta una tarifa de {{amount, currency}} por no presentarme.", "days": "días", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Equipo: {{teamName}}", "insights_user_filter": "Usuario: {{userName}}", "insights_subtitle": "Mire información de insights en todos sus eventos", - "call_history": "Historial de llamadas", - "call_history_subtitle": "Ver historial de llamadas de tus llamadas con Cal.ai", "location_options": "{{locationCount}} opciones de ubicación", - "channel_type": "Tipo de canal", - "end_reason": "Motivo de finalización", - "session_status": "Estado de la sesión", - "user_sentiment": "Sentimiento del usuario", - "time_header": "Hora", - "from_header": "De", "custom_plan": "Plan personalizado", "email_embed": "Incrustado en el correo electrónico", "add_times_to_your_email": "Seleccione algunos horarios disponibles e incrústelos en su correo electrónico", @@ -2782,8 +2752,6 @@ "account_already_linked": "La cuenta ya está vinculada", "send_email": "Enviar correo electrónico", "cal_ai_phone_call_action": "Llamar al asistente usando el agente de voz de Cal.ai", - "call_to_confirm_booking": "Llamada para confirmar reserva", - "cal_ai_phone_call_action_description": "2 horas antes del inicio del evento", "cal_ai_agent_configuration": "Configuración del agente Cal.ai", "choose_at_least_one_event_type_test_call": "Por favor, elige al menos un tipo de evento para realizar una llamada de prueba.", "mark_as_no_show": "Marcar como no presentado", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verificar cambio de correo electrónico", "buy_credits": "Comprar créditos", "credits": "Créditos", - "credits_used": "Créditos utilizados", - "total_credits_remaining": "Total restante", - "credits_per_tip_org": "Recibes 1000 créditos por mes, por miembro del equipo", - "credits_per_tip_teams": "Recibes 750 créditos por mes, por miembro del equipo", - "view_and_manage_credits": "Ver y gestionar créditos para enviar mensajes SMS", + "view_and_manage_credits": "Ver y gestionar créditos", "view_and_manage_credits_description": "Ver y gestionar créditos para enviar mensajes SMS. Un crédito equivale a 1¢ (USD). <0>Más información", - "credit_worth_description": "Un crédito vale 1¢ (USD). <0>Más información", "buy_additional_credits": "Comprar créditos adicionales ($0.01 por crédito)", - "view_additional_credits_expense_tip": "Puedes ver el gasto de créditos adicionales en tu registro de gastos", "overview": "Resumen", "organization_slug_taken": "El slug de la organización ya está en uso", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "No puedes crear una organización ya que ya formas parte de una", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Tu equipo de Cal.com {{teamName}} se ha quedado sin créditos. Como resultado, los mensajes SMS ahora se están enviando por correo electrónico. Para reanudar el envío de SMS, por favor compra créditos adicionales.", "credit_limit_reached_message_user": "Tu cuenta de Cal.com se ha quedado sin créditos. Como resultado, los mensajes SMS ahora se envían por correo electrónico. Para reanudar el envío de SMS, por favor compra créditos adicionales.", "current_credit_balance": "Saldo actual: {{balance}} créditos", - "current_balance": "Saldo actual:", "notification_about_your_booking": "Notificación sobre tu reserva", "monthly_credits": "Créditos mensuales", "total_credits": "Créditos totales: {{totalCredits}}", "remaining_credits": "Créditos restantes: {{remainingCredits}}", - "remaining": "Restante", - "total": "Total", "additional_credits": "Créditos adicionales", "routing_form_next_in_queue": "{{count}} siguiente(s) en cola", "routing_form_select_members_to_email": "Enviar respuestas por correo electrónico a", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Ver flujos de trabajo existentes y sus configuraciones", "pbac_desc_update_workflows": "Editar y modificar ajustes de flujos de trabajo", "pbac_desc_delete_workflows": "Eliminar flujos de trabajo del sistema", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Crear webhooks", - "pbac_desc_view_webhooks": "Ver webhooks", - "pbac_desc_update_webhooks": "Actualizar webhooks", - "pbac_desc_delete_webhooks": "Eliminar webhooks", "pbac_desc_manage_workflows": "Acceso completo de gestión a todos los flujos de trabajo", "pbac_desc_create_event_types": "Crear tipos de eventos", "pbac_desc_view_event_types": "Ver tipos de eventos", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "El nombre del evento desencadenante (p. ej., BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "La hora del webhook", "webhook_type": "El slug del tipo de evento", - "set_up_agent": "Configurar agente", "webhook_title": "El nombre del tipo de evento", "webhook_start_time": "La hora de inicio del evento", "webhook_end_time": "La hora de finalización del evento", @@ -3672,9 +3625,6 @@ "visit": "Visitar", "location_custom_label_input_label": "Etiqueta personalizada en la página de reservas", "meeting_link": "Enlace de la reunión", - "session_outcome": "Resultado de la sesión", - "call_created": "Llamada creada", - "voicemail": "Buzón de voz", "my_bookings": "Mis reservas", "phone": "Teléfono", "free": "Gratis", @@ -3682,8 +3632,6 @@ "user_name": "Nombre de usuario", "expand_panel": "Expandir panel", "collapse_panel": "Contraer panel", - "email_verification_required": "Se requiere verificación de correo electrónico para este tipo de evento", - "invalid_verification_code": "Código de verificación proporcionado no válido", "you_have_one_team": "Tienes un equipo", "consider_consolidating_one_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tu equipo.", "consider_consolidating_multi_team_org": "Considera configurar una organización para unificar la facturación, herramientas de administración y análisis en tus equipos.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Antes de la hora de inicio programada", "cancel_booking_acknowledge_no_show_fee": "Reconozco que al cancelar la reserva dentro de los {{timeValue}} {{timeUnit}} previos a la hora de inicio, se me cobrará la tarifa por no presentarme de {{amount, currency}}", "contact_organizer": "Si tienes alguna pregunta, por favor contacta al organizador.", - "booking_time_option": "Hora de reserva", - "booking_time_option_description": "Cuando se programa la reserva (inicio a fin)", - "created_at_option": "Creado el", - "created_at_option_description": "Cuando se creó originalmente la reserva", - "call_details": "Detalles de la llamada", - "call_id": "ID de llamada", - "call_information": "Información de la llamada", - "sentiment": "Sentimiento", - "disconnect_reason": "Motivo de desconexión", - "call_summary": "Resumen de la llamada", - "transcription": "Transcripción", - "event_details": "Detalles del evento", - "agent": "Agente", - "no_transcript_available": "No hay transcripción disponible", - "testing_sms_workflow_info_message": "Al probar este flujo de trabajo, ten en cuenta que los SMS deben programarse con al menos 15 minutos de antelación", - "start_from_scratch_title": "Comenzar desde cero", - "start_from_scratch_description": "Crea tu propio flujo de trabajo desde cero.", - "cal_ai_template_title": "Plantilla de Cal.ai", - "cal_ai_template_description": "¡Agentes de IA que programan reuniones, envían recordatorios y hacen seguimiento!", - "voice": "Voz", - "select_voice": "Seleccionar voz", - "select_voice_for_agent": "Selecciona una voz para tu agente", - "choose_a_voice_for_your_agent": "Elige una voz para tu agente", - "trait": "Característica", - "voice_id": "ID de voz", - "use_voice": "Usar voz", - "current_voice": "Voz actual", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/et/common.json b/apps/web/public/static/locales/et/common.json index d86267b34d9ce1..afcca35bf0d26a 100644 --- a/apps/web/public/static/locales/et/common.json +++ b/apps/web/public/static/locales/et/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Kinnitage oma e-posti aadress, et tagada parim e-kirjade ja kalendri saabumus", "verify_email_email_header": "Kinnitage oma e-posti aadress", "verify_email_button": "Kinnita e-post", - "cal_ai_assistant": "Assistent", + "cal_ai_assistant": "Assistant", "send_cal_video_transcription_emails": "Saada Cal Video transkriptsiooni e-kirjad", "description_send_cal_video_transcription_emails": "Saada pärast koosoleku lõppu Cal Video transkriptsiooniga e-kirjad. (Nõuab tasulist paketti)", "verify_email_change_description": "Olete hiljuti taotlenud oma {{appName}} kontole sisselogimiseks kasutatava e-posti aadressi muutmist. Uue e-posti aadressi kinnitamiseks klõpsake alloleval nupul.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Töövoo valideerimine ebaõnnestus", "workflow_validation_empty_fields": "Ühel või mitmel töövoo sammul puudub sõnumi sisu", "workflow_validation_unverified_contacts": "Üks või mitu telefoninumbrit või e-posti aadressi ei ole kinnitatud", - "supercharge_your_workflows_with_cal_ai": "Tõhusta oma töövooge Cal.ai-ga", - "supercharge_your_workflows_with_cal_ai_description": "Elutruud tehisintellekti agendid, kes broneerivad kohtumisi, saadavad meeldetuletusi ja võtavad klientidega ühendust.", "phone_number_imported_successfully": "Telefoninumber imporditi ja seoti agendiga edukalt", "phone_number_deleted_successfully": "Telefoninumber kustutati edukalt", "delete_phone_number_confirmation": "Kas olete kindel, et soovite selle telefoninumbri kustutada? Seda toimingut ei saa tagasi võtta.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Telefoninumbri tellimus edukalt tühistatud", "updating": "Uuendamine", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Tere, kuidas läheb?", "round_robin_description": "Korraldage koosolekuid mitme meeskonnaliikme vahel.", "managed_event": "Hallatud sündmus", "username_placeholder": "kasutajanimi", @@ -879,7 +876,7 @@ "are_you_sure_you_want_to_delete_workflow_step": "Kas olete kindel, et soovite selle töövoo sammu kustutada?", "do_you_still_want_to_unsubscribe": "Kas soovite siiski selle agendi telefoninumbri tellimuse tühistada?", "the_action_will_disconnect_phone_number": "See toiming katkestab telefoninumbri ühenduse agendiga. Agent ei saa kõnesid teha enne, kui uus telefoninumber on ühendatud.", - "cal_ai_phone_numbers": "Telefoninumbrid", + "cal_ai_phone_numbers": "telefoninumbrid", "connect_phone_number": "Ühenda telefoninumber", "cal_ai_phone_numbers_description": "Halda oma telefoninumbreid", "import_number": "Impordi number", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Tühista oma telefoninumbri tellimus", "delete_associated_phone_number": "Kustuta seotud telefoninumber", "unauthorized_create_workflow": "Teil pole õigust seda töövoogu luua", - "import_phone_number_description": "Impordi oma Twilio telefoninumber, et seda telefoniga kasutada", + "import_phone_number_description": "Impordi oma Twilio telefoninumber, et kasutada seda telefoninumbritega", "phone_number_cost": "${{price}}/kuu", "buy_new_number": "Osta uus number", "buy_number_cost_x_per_month": "Telefoninumbri ostmine maksab ${{priceInDollars}} kuus. Teilt võetakse igakuine tasu iga aktiivse telefoninumbri eest.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Jah, tühista tellimus", "cancel_phone_number_subscription_confirmation": "Kas olete kindel, et soovite selle telefoninumbri tellimuse tühistada? Seda toimingut ei saa tagasi võtta ja kaotate juurdepääsu sellele telefoninumbrile.", "add_members": "Lisa liikmeid...", - "add_members_no_ellipsis": "Lisa liikmeid", "no_assigned_members": "Määratud liikmeid pole", "assigned_to": "Määratud", "you_must_be_logged_in_to": "Peate olema sisse logitud saidile {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategooriad", "pricing": "Hinnakujundus", "learn_more": "Lisateave", - "try_now": "Proovi nüüd", "privacy_policy": "Privaatsuspoliitika", "terms_of_service": "Kasutustingimused", "remove": "Eemalda", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "saada osalejale WhatsAppi sõnum", "workflows": "Töövood", "new_workflow_btn": "Uus töövoog", - "how_would_you_like_to_start": "Kuidas soovite alustada?", "add_new_workflow": "Lisa uus töövoog", "reschedule_event_trigger": "kui sündmus on ümber planeeritud", "trigger": "Päädik", @@ -1722,8 +1716,6 @@ "event_duration_info": "Sündmuse kestus", "event_time_info": "Ürituse algusaeg", "event_type_not_found": "Sündmuse tüüpi ei leitud", - "number_to_call_variable": "Helistatav number", - "number_to_call_info": "Kasutaja telefoninumber, kellele helistate", "location_variable": "Asukoht", "location_info": "Sündmuse koht", "additional_notes_variable": "Lisamärkmed", @@ -1761,7 +1753,6 @@ "team_url": "Meeskonna URL", "team_members": "Meeskonna liikmed", "more": "Veel", - "cal_ai_workflows": "Cal.ai töövood", "and_count_more": "ja veel {{count}}", "more_page_footer": "Me vaatame mobiilirakendust veebirakenduse laiendusena. Keeruliste toimingute tegemisel pöörduge tagasi veebirakenduse poole.", "workflow_example_1": "Saada osalejatele SMS-meeldetuletus 24 tundi enne sündmuse algust", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Saada meili meeldetuletus 1 tund enne sündmuste algust osalejatele", "workflow_example_5": "Saada kohandatud e-kiri, kui sündmus on korraldajale ümber ajastatud", "workflow_example_6": "Saada kohandatud SMS, kui uus sündmus on hostile broneeritud", - "send_sms_reminder": "Saada SMS-meeldetuletus", - "send_sms_reminder_description": "24 tundi enne sündmuse algust", - "follow_up_with_no_shows": "Võta ühendust nendega, kes ei ilmunud kohale", - "follow_up_with_no_shows_description": "30 minutit pärast sündmuse lõppu", - "remind_attendees_to_bring_id": "Tuleta osalejatele meelde, et nad võtaksid ID kaasa", - "remind_attendees_to_bring_id_description": "1 päev enne sündmuse algust", - "email_to_remind_booking": "E-posti meeldetuletus", - "email_to_remind_booking_description": "1 tund enne sündmuse algust", - "custom_sms_reminder": "Kohandatud SMS-meeldetuletus", - "custom_sms_reminder_description": "Kui sündmus on planeeritud", - "custom_email_reminder": "Kohandatud e-posti meeldetuletus", - "custom_email_reminder_description": "Sündmus on ümber planeeritud võõrustajale", "count_managed_to_limit": "Kaasa hallatavate sündmuste tüüpide broneeringute arv", "welcome_to_cal_header": "Tere tulemast rakendusse {{appName}}!", "edit_form_later_subtitle": "Saate seda hiljem muuta.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Näita broneerimislehel", "visit_cancelled_booking": "Saate külastada tühistatud broneeringu lehte", "get_started_zapier_templates": "Alustage Zapieri mallidega", - "standard_templates": "Standardmallid", - "cal_ai_templates": "Cal.ai mallid", "team_is_unpublished": "{{team}} on avaldamata", "org_is_unpublished_description": "See organisatsiooni link pole praegu saadaval. Võtke ühendust organisatsiooni omanikuga või paluge tal see avaldada.", "team_is_unpublished_description": "See meeskonnalink pole praegu saadaval. Võtke ühendust meeskonna omanikuga või paluge tal see avaldada.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Kaarti ei olnud võimalik makse jaoks debiteerida.", "insights": "Teadmisi", "routing_forms": "Suunamise vormid", + "testing_workflow_info_message": "Selle töövoo testimisel pidage meeles, et e-kirju ja SMS-e saab ajastada ainult vähemalt 1 tund ette", "insights_no_data_found_for_filter": "Valitud filtri või valitud kuupäevade kohta andmeid ei leitud.", "acknowledge_booking_no_show_fee": "Ma tean, et kui ma sellel üritusel ei osale, rakendatakse minu kaardile {{summa, valuuta}} mitteilmumise tasu.", "days": "päevad", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Meeskond: {{teamName}}", "insights_user_filter": "Kasutaja: {{userName}}", "insights_subtitle": "Vaadake oma sündmuste broneerimise statistikat", - "call_history": "Kõnede ajalugu", - "call_history_subtitle": "Vaata kõnede ajalugu oma Cal.ai kõnede kohta", "location_options": "{{locationCount}} asukohavalikut", - "channel_type": "Kanali tüüp", - "end_reason": "Lõpetamise põhjus", - "session_status": "Seansi staatus", - "user_sentiment": "Kasutaja meeleolu", - "time_header": "Aeg", - "from_header": "Saatja", "custom_plan": "Kohandatud plaan", "email_embed": "E-posti manustamine", "add_times_to_your_email": "Valige mõned vabad ajad ja manustage need oma e-kirja", @@ -2782,8 +2752,6 @@ "account_already_linked": "Konto on juba lingitud", "send_email": "Saada email", "cal_ai_phone_call_action": "Helista osalejale Cal.ai häälagendi abil", - "call_to_confirm_booking": "Helista broneeringu kinnitamiseks", - "cal_ai_phone_call_action_description": "2 tundi enne sündmuse algust", "cal_ai_agent_configuration": "Cal.ai agendi konfiguratsioon", "choose_at_least_one_event_type_test_call": "Palun valige vähemalt üks sündmuse tüüp, et teha testkõne.", "mark_as_no_show": "Märgi mitteilmuvaks", @@ -3235,15 +3203,9 @@ "verify_email_change": "Kinnita e-posti aadressi muutmine", "buy_credits": "Osta krediiti", "credits": "Krediidid", - "credits_used": "Kasutatud krediidid", - "total_credits_remaining": "Kokku järelejäänud", - "credits_per_tip_org": "Saate 1000 krediiti kuus iga meeskonnaliikme kohta", - "credits_per_tip_teams": "Saate 750 krediiti kuus iga meeskonnaliikme kohta", - "view_and_manage_credits": "Vaata ja halda SMS-sõnumite saatmise krediite", + "view_and_manage_credits": "Vaata ja halda krediite", "view_and_manage_credits_description": "Vaata ja halda krediite SMS-sõnumite saatmiseks. Üks krediit on väärt 1¢ (USD). <0>Lisateave", - "credit_worth_description": "Üks krediit on väärt 1¢ (USD). <0>Lisateave", "buy_additional_credits": "Osta lisakrediite (0,01 $ krediidi kohta)", - "view_additional_credits_expense_tip": "Saate vaadata lisakrediidi kulutusi oma kululogis", "overview": "Ülevaade", "organization_slug_taken": "Organisatsiooni lühilink on juba kasutusel", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Te ei saa luua organisatsiooni, kuna olete juba osa organisatsioonist", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Teie Cal.com meeskonnal {{teamName}} on krediidid otsas. Selle tulemusena saadetakse SMS-sõnumid nüüd e-kirjadena. SMS-ide saatmise jätkamiseks ostke palun lisakrediite.", "credit_limit_reached_message_user": "Teie Cal.com konto krediit on otsa saanud. Seetõttu saadetakse SMS-sõnumid nüüd e-posti teel. SMS-ide saatmise jätkamiseks ostke palun lisakrediiti.", "current_credit_balance": "Praegune saldo: {{balance}} krediiti", - "current_balance": "Praegune saldo:", "notification_about_your_booking": "Teavitus teie broneeringu kohta", "monthly_credits": "Igakuised krediidid", "total_credits": "Kokku krediite: {{totalCredits}}", "remaining_credits": "Allesjäänud krediidid: {{remainingCredits}}", - "remaining": "Järelejäänud", - "total": "Kokku", "additional_credits": "Lisakrediidid", "routing_form_next_in_queue": "{{count}} järgmine järjekorras", "routing_form_select_members_to_email": "Saada e-posti vastused", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Vaata olemasolevaid töövooge ja nende konfiguratsioone", "pbac_desc_update_workflows": "Muuda ja redigeeri töövoo seadeid", "pbac_desc_delete_workflows": "Eemalda töövood süsteemist", - "pbac_resource_webhook": "Veebihaak", - "pbac_desc_create_webhooks": "Loo veebihaake", - "pbac_desc_view_webhooks": "Vaata veebihaake", - "pbac_desc_update_webhooks": "Uuenda veebihaake", - "pbac_desc_delete_webhooks": "Kustuta veebihaake", "pbac_desc_manage_workflows": "Täielik haldusõigus kõikidele töövoogudele", "pbac_desc_create_event_types": "Loo sündmuse tüüpe", "pbac_desc_view_event_types": "Vaata sündmuse tüüpe", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Käivitussündmuse nimi (nt BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Veebikonksu loomise aeg", "webhook_type": "Sündmuse tüübi lühinimi", - "set_up_agent": "Seadista agent", "webhook_title": "Sündmuse tüübi nimi", "webhook_start_time": "Sündmuse algusaeg", "webhook_end_time": "Sündmuse lõpuaeg", @@ -3672,9 +3625,6 @@ "visit": "Külastus", "location_custom_label_input_label": "Kohandatud silt broneerimislehel", "meeting_link": "Koosoleku link", - "session_outcome": "Seansi tulemus", - "call_created": "Kõne loodud", - "voicemail": "Kõnepost", "my_bookings": "Minu broneeringud", "phone": "Telefon", "free": "Tasuta", @@ -3682,8 +3632,6 @@ "user_name": "Kasutaja nimi", "expand_panel": "Laienda paneeli", "collapse_panel": "Ahenda paneel", - "email_verification_required": "Selle sündmuse tüübi jaoks on vajalik e-posti kinnitamine", - "invalid_verification_code": "Esitatud kinnituskood on vigane", "you_have_one_team": "Teil on üks meeskond", "consider_consolidating_one_team_org": "Kaaluge organisatsiooni loomist, et ühtlustada arveldamist, haldustööriistu ja analüütikat oma meeskonnas.", "consider_consolidating_multi_team_org": "Kaaluge organisatsiooni loomist, et ühtlustada arveldamist, haldustööriistu ja analüütikat oma meeskondade vahel.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Enne planeeritud algusaega", "cancel_booking_acknowledge_no_show_fee": "Mõistan, et tühistades broneeringu {{timeValue}} {{timeUnit}} jooksul enne algusaega, võetakse minult puudumise tasu summas {{amount, currency}}", "contact_organizer": "Kui teil on küsimusi, võtke palun ühendust korraldajaga.", - "booking_time_option": "Broneerimise aeg", - "booking_time_option_description": "Kui broneering on ajastatud (algusest lõpuni)", - "created_at_option": "Loodud", - "created_at_option_description": "Kui broneering algselt loodi", - "call_details": "Kõne üksikasjad", - "call_id": "Kõne ID", - "call_information": "Kõne teave", - "sentiment": "Hinnang", - "disconnect_reason": "Katkestamise põhjus", - "call_summary": "Kõne kokkuvõte", - "transcription": "Transkriptsioon", - "event_details": "Sündmuse üksikasjad", - "agent": "Agent", - "no_transcript_available": "Transkriptsioon pole saadaval", - "testing_sms_workflow_info_message": "Selle töövoo testimisel arvestage, et SMS-id tuleb ajastada vähemalt 15 minutit ette", - "start_from_scratch_title": "Alusta nullist", - "start_from_scratch_description": "Loo oma töövoog nullist.", - "cal_ai_template_title": "Cal.ai mall", - "cal_ai_template_description": "Tehisintellekti agendid, kes broneerivad kohtumisi, saadavad meeldetuletusi ja teevad järeltegevusi!", - "voice": "Hääl", - "select_voice": "Vali hääl", - "select_voice_for_agent": "Vali oma agendile hääl", - "choose_a_voice_for_your_agent": "Vali oma agendile hääl", - "trait": "Omadus", - "voice_id": "Hääle ID", - "use_voice": "Kasuta häält", - "current_voice": "Praegune hääl", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 95bac1798f17f0..d56ba54cde3d88 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Egiaztatu zure email helbidea mezuak eta egutegiko eguneratzeak ahalik eta hobekien jasoko dituzula bermatzeko", "verify_email_email_header": "Egiaztatu zure email helbidea", "verify_email_button": "Egiaztatu emaila", - "cal_ai_assistant": "Laguntzailea", + "cal_ai_assistant": "laguntzailea", "send_cal_video_transcription_emails": "Cal Video transkripzio emailak bidali", "description_send_cal_video_transcription_emails": "Bidali Cal Video-ren transkripzioa duen emaila bilera amaitu ondoren. (Ordainpeko plana behar da)", "verify_email_change_description": "Berriki eskatu duzu {{appName}} kontuan saioa hasteko erabiltzen duzun posta elektronikoa aldatzea. Mesedez, egin klik beheko botoian zure posta elektroniko berria baieztatzeko.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Lan-fluxuaren balidazioak huts egin du", "workflow_validation_empty_fields": "Lan-fluxuaren urrats batek edo gehiagok mezu eduki hutsa dute", "workflow_validation_unverified_contacts": "Telefono zenbaki edo helbide elektroniko bat edo gehiago ez daude egiaztatuta", - "supercharge_your_workflows_with_cal_ai": "Indartu zure lan-fluxuak Cal.ai-rekin", - "supercharge_your_workflows_with_cal_ai_description": "Benetakoak diruditen AI agenteak, bilerak antolatzen, oroigarriak bidaltzen eta zure bezeroekin jarraipena egiten dutenak.", "phone_number_imported_successfully": "Telefono zenbakia inportatu eta agentari arrakastaz lotu zaio", "phone_number_deleted_successfully": "Telefono zenbakia arrakastaz ezabatu da", "delete_phone_number_confirmation": "Ziur zaude telefono zenbaki hau ezabatu nahi duzula? Ekintza hau ezin da desegin.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Telefono zenbakiaren harpidetza behar bezala ezeztatu da", "updating": "Eguneratzen", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Kaixo, zer moduz zaude?", "round_robin_description": "Txandakatu bilerak taldekideen artean.", "managed_event": "Kudeatutako gertaera", "username_placeholder": "erabiltzaile izena", @@ -879,7 +876,7 @@ "are_you_sure_you_want_to_delete_workflow_step": "Ziur zaude workflow pauso hau ezabatu nahi duzula?", "do_you_still_want_to_unsubscribe": "Oraindik telefono zenbakia agente honetatik harpidetza kendu nahi duzu?", "the_action_will_disconnect_phone_number": "Ekintza honek telefono zenbakia agentetik deskonektatuko du. Agenteak ezin izango du deirik egin telefono zenbaki berri bat konektatu arte.", - "cal_ai_phone_numbers": "Telefono zenbakiak", + "cal_ai_phone_numbers": "telefono zenbakiak", "connect_phone_number": "Konektatu telefono zenbakia", "cal_ai_phone_numbers_description": "Kudeatu zure telefono zenbakiak", "import_number": "Inportatu zenbakia", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Ezeztatu zure telefono zenbakiaren harpidetza", "delete_associated_phone_number": "Ezabatu lotutako telefono zenbakia", "unauthorized_create_workflow": "Ez duzu workflow hau sortzeko baimenik", - "import_phone_number_description": "Inportatu zure Twilio telefono zenbakia telefonoarekin erabiltzeko", + "import_phone_number_description": "Inportatu zure Twilio telefono zenbakia Phone-rekin erabiltzeko", "phone_number_cost": "${{price}}/hilabeteko", "buy_new_number": "Erosi zenbaki berria", "buy_number_cost_x_per_month": "Telefono zenbaki bat erosteak ${{priceInDollars}} balio du hilabeteko. Hilero kobratuko zaizu aktibo dagoen telefono zenbaki bakoitzeko.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Bai, ezeztatu harpidetza", "cancel_phone_number_subscription_confirmation": "Ziur zaude telefono zenbaki honen harpidetza ezeztatu nahi duzula? Ekintza hau ezin da desegin eta telefono zenbaki honetarako sarbidea galduko duzu.", "add_members": "Gehitu kideak...", - "add_members_no_ellipsis": "Gehitu kideak", "no_assigned_members": "Ez dago esleitutako kiderik", "assigned_to": "Honi esleituta", "you_must_be_logged_in_to": "{{url}}-n saioa hasi behar duzu", @@ -1211,7 +1207,6 @@ "categories": "Kategoriak", "pricing": "Prezioak", "learn_more": "Gehiago ikasi", - "try_now": "Probatu orain", "privacy_policy": "Pribatutasun politika", "terms_of_service": "Erabilera-baldintzak", "remove": "Ezabatu", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "bidali Whatsapp mezua partaideari", "workflows": "Lan-fluxuak", "new_workflow_btn": "Lan-fluxu berria", - "how_would_you_like_to_start": "Nola hasi nahi duzu?", "add_new_workflow": "Gehitu workflow berria", "reschedule_event_trigger": "gertaera berrantolatzen denean", "trigger": "Aktibatzailea", @@ -1722,8 +1716,6 @@ "event_duration_info": "Ekitaldiaren iraupena", "event_time_info": "Gertaeraren hasiera-ordua", "event_type_not_found": "Ez da EventType aurkitu", - "number_to_call_variable": "Deitzeko zenbakia", - "number_to_call_info": "Deitzen ari zaren erabiltzailearen telefono zenbakia", "location_variable": "Kokapena", "location_info": "Gertaeraren kokapena", "additional_notes_variable": "Ohar gehigarriak", @@ -1761,7 +1753,6 @@ "team_url": "Taldearen URLa", "team_members": "Taldekideak", "more": "Gehiago", - "cal_ai_workflows": "Cal.ai lan-fluxuak", "and_count_more": "eta {{count}} gehiago", "more_page_footer": "Mugikorrerako aplikazioa web aplikazioaren luzapen gisa ikusten dugu. Ekintza konplexuak egiten ari bazara, mesedez itzuli web aplikaziora.", "workflow_example_1": "Bidali SMS gogorarazlea gertaera hasi baino 24 ordu lehenago partaideari", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Bidali email gogorarazlea gertaerak hasi baino ordubete lehenago partaideari", "workflow_example_5": "Bidali mezu elektroniko pertsonalizatua gertaera berriz programatzen denean antolatzaileari", "workflow_example_6": "Bidali SMS pertsonalizatua gertaera berria erreserbatzen denean antolatzaileari", - "send_sms_reminder": "Bidali SMS oroigarria", - "send_sms_reminder_description": "Ekitaldia hasi baino 24 ordu lehenago", - "follow_up_with_no_shows": "Jarraipena egin ez agertuei", - "follow_up_with_no_shows_description": "Ekitaldia amaitu eta 30 minutura", - "remind_attendees_to_bring_id": "Gogoratu parte-hartzaileei identifikazioa ekartzeko", - "remind_attendees_to_bring_id_description": "Ekitaldia hasi baino egun bat lehenago", - "email_to_remind_booking": "Posta elektroniko bidezko oroigarria", - "email_to_remind_booking_description": "Ekitaldia hasi baino ordubete lehenago", - "custom_sms_reminder": "SMS oroigarri pertsonalizatua", - "custom_sms_reminder_description": "Ekitaldia programatzen denean", - "custom_email_reminder": "Posta elektroniko bidezko oroigarri pertsonalizatua", - "custom_email_reminder_description": "Ekitaldia antolatzailearentzat berriz programatzen denean", "count_managed_to_limit": "Gehitu kudeatutako gertaera moten erreserbak zenbaketara", "welcome_to_cal_header": "Ongi etorri {{appName}}-era!", "edit_form_later_subtitle": "Geroago editatu ahal izango duzu.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Erakutsi erreserba orrian", "visit_cancelled_booking": "Bertan behera utzitako erreserba orria bisitatu dezakezu", "get_started_zapier_templates": "Hasi Zapier txantiloiekin", - "standard_templates": "Txantiloi Estandarrak", - "cal_ai_templates": "Cal.ai Txantiloiak", "team_is_unpublished": "{{team}} ez dago argitaratuta", "org_is_unpublished_description": "Erakunde honen esteka ez dago eskuragarri momentu honetan. Mesedez, jarri harremanetan erakundearen jabearekin edo eskatu argitaratzeko.", "team_is_unpublished_description": "Talde honen esteka ez dago eskuragarri momentu honetan. Mesedez, jarri harremanetan taldearen jabearekin edo eskatu argitaratzeko.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Ezin izan da txartela kobratu ordainketarako.", "insights": "Ikuspegiak", "routing_forms": "Bideratze-formularioak", + "testing_workflow_info_message": "Lan-fluxu hau probatzean, kontuan izan e-mailak eta SMSak gutxienez ordubete lehenago soilik programa daitezkeela", "insights_no_data_found_for_filter": "Ez da daturik aurkitu hautatutako iragazkirako edo hautatutako datetan.", "acknowledge_booking_no_show_fee": "Onartzen dut ekitaldi honetara ez joatekotan nire txartelean {{amount, currency}}-ko ez agertzeagatiko tarifa kobratuko zaidala.", "days": "egun", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Taldea: {{teamName}}", "insights_user_filter": "Erabiltzailea: {{userName}}", "insights_subtitle": "Ikusi zure ekitaldien erreserben informazioa", - "call_history": "Deien Historia", - "call_history_subtitle": "Ikusi deien historia zure Cal.ai dei guztietan", "location_options": "{{locationCount}} kokapen aukera", - "channel_type": "Kanal Mota", - "end_reason": "Amaiera Arrazoia", - "session_status": "Saioaren Egoera", - "user_sentiment": "Erabiltzailearen Sentimendua", - "time_header": "Ordua", - "from_header": "Nondik", "custom_plan": "Plan pertsonalizatua", "email_embed": "Posta elektronikoan txertatu", "add_times_to_your_email": "Hautatu eskuragarri dauden ordu batzuk eta txertatu zure posta elektronikoan", @@ -2782,8 +2752,6 @@ "account_already_linked": "Kontua dagoeneko lotuta dago", "send_email": "Bidali emaila", "cal_ai_phone_call_action": "Deitu parte-hartzaileari Cal.ai Ahots Agentea erabiliz", - "call_to_confirm_booking": "Deitu erreserba baieztatzeko", - "cal_ai_phone_call_action_description": "Gertaera hasi baino 2 ordu lehenago", "cal_ai_agent_configuration": "Cal.ai agente konfigurazioa", "choose_at_least_one_event_type_test_call": "Mesedez, aukeratu gutxienez gertaera mota bat proba-deia egiteko.", "mark_as_no_show": "Markatu ez-agertutzat", @@ -3235,15 +3203,9 @@ "verify_email_change": "Egiaztatu posta elektronikoaren aldaketa", "buy_credits": "Erosi kredituak", "credits": "Kredituak", - "credits_used": "Kreditu erabiliak", - "total_credits_remaining": "Geratzen diren guztira", - "credits_per_tip_org": "Hilero 1000 kreditu jasotzen dituzu, taldekide bakoitzeko", - "credits_per_tip_teams": "Hilero 750 kreditu jasotzen dituzu, taldekide bakoitzeko", - "view_and_manage_credits": "Ikusi eta kudeatu kredituak SMS mezuak bidaltzeko", + "view_and_manage_credits": "Ikusi eta kudeatu kredituak", "view_and_manage_credits_description": "Ikusi eta kudeatu SMS mezuak bidaltzeko kredituak. Kreditu bat 1¢ (USD) balio du. <0>Informazio gehiago", - "credit_worth_description": "Kreditu bat 1¢ (USD) balio du. <0>Gehiago ikasi", "buy_additional_credits": "Erosi kreditu gehigarriak (0,01$ kreditu bakoitzeko)", - "view_additional_credits_expense_tip": "Kreditu gehigarrien gastua zure gastu-erregistroan ikusi dezakezu", "overview": "Ikuspegi orokorra", "organization_slug_taken": "Erakundearen slug-a dagoeneko hartuta dago", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Ezin duzu erakunde bat sortu dagoeneko erakunde baten parte zarelako", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Zure Cal.com {{teamName}} taldeari kredituak agortu zaizkio. Ondorioz, SMS mezuak orain posta elektroniko bidez bidaltzen ari dira. SMS bidalketa berriro hasteko, erosi kreditu gehigarriak.", "credit_limit_reached_message_user": "Zure Cal.com kontuaren kredituak agortu dira. Ondorioz, SMS mezuak orain posta elektroniko bidez bidaltzen ari dira. SMS bidalketak berriz hasteko, erosi kreditu gehigarriak.", "current_credit_balance": "Uneko saldoa: {{balance}} kreditu", - "current_balance": "Oraingo saldoa:", "notification_about_your_booking": "Zure erreserba buruzko jakinarazpena", "monthly_credits": "Hileko kredituak", "total_credits": "Kreditu totalak: {{totalCredits}}", "remaining_credits": "Geratzen diren kredituak: {{remainingCredits}}", - "remaining": "Geratzen dena", - "total": "Guztira", "additional_credits": "Kreditu gehigarriak", "routing_form_next_in_queue": "{{count}} hurrengo ilaran", "routing_form_select_members_to_email": "Bidali posta elektroniko erantzunak honi", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Ikusi dauden lan-fluxuak eta haien konfigurazioak", "pbac_desc_update_workflows": "Editatu eta aldatu lan-fluxuen ezarpenak", "pbac_desc_delete_workflows": "Kendu lan-fluxuak sistematik", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Sortu webhook-ak", - "pbac_desc_view_webhooks": "Ikusi webhook-ak", - "pbac_desc_update_webhooks": "Webhookak eguneratu", - "pbac_desc_delete_webhooks": "Webhookak ezabatu", "pbac_desc_manage_workflows": "Lan-fluxu guztien kudeaketa-sarbide osoa", "pbac_desc_create_event_types": "Gertaera motak sortu", "pbac_desc_view_event_types": "Gertaera motak ikusi", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Abiarazle-gertaeraren izena (adib., BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Webhook-aren denbora", "webhook_type": "Gertaera motaren slug-a", - "set_up_agent": "Agentea konfiguratu", "webhook_title": "Gertaera motaren izena", "webhook_start_time": "Gertaeraren hasiera-ordua", "webhook_end_time": "Gertaeraren amaiera-ordua", @@ -3672,9 +3625,6 @@ "visit": "Bisitatu", "location_custom_label_input_label": "Etiketa pertsonalizatua erreserba orrian", "meeting_link": "Bilera esteka", - "session_outcome": "Saioaren emaitza", - "call_created": "Deia sortuta", - "voicemail": "Ahots-mezua", "my_bookings": "Nire erreserbak", "phone": "Telefonoa", "free": "Doakoa", @@ -3682,8 +3632,6 @@ "user_name": "Erabiltzailearen izena", "expand_panel": "Zabaldu panela", "collapse_panel": "Tolestu panela", - "email_verification_required": "Posta elektronikoaren egiaztapena beharrezkoa da gertaera mota honetarako", - "invalid_verification_code": "Emandako egiaztapen-kodea baliogabea da", "you_have_one_team": "Talde bat duzu", "consider_consolidating_one_team_org": "Pentsatu erakunde bat sortzeaz zure taldearen fakturazioa, administrazio-tresnak eta analitika bateratzeko.", "consider_consolidating_multi_team_org": "Pentsatu erakunde bat sortzeaz zure taldeen fakturazioa, administrazio-tresnak eta analitika bateratzeko.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Programatutako hasiera ordua baino lehen", "cancel_booking_acknowledge_no_show_fee": "Onartzen dut hasiera ordua baino {{timeValue}} {{timeUnit}} lehenago erreserba ezeztatuz gero, {{amount, currency}}-ko ez agertzeko tasa kobratuko zaidala", "contact_organizer": "Galderarik baduzu, jarri harremanetan antolatzailearekin.", - "booking_time_option": "Erreserba ordua", - "booking_time_option_description": "Erreserba programatuta dagoenean (hasieratik amaierara)", - "created_at_option": "Sortze data", - "created_at_option_description": "Erreserba jatorriz noiz sortu zen", - "call_details": "Deiaren xehetasunak", - "call_id": "Dei ID", - "call_information": "Deiaren informazioa", - "sentiment": "Sentimendua", - "disconnect_reason": "Deskonexio arrazoia", - "call_summary": "Deiaren laburpena", - "transcription": "Transkripzioa", - "event_details": "Gertaeraren xehetasunak", - "agent": "Agentea", - "no_transcript_available": "Ez dago transkripziorik eskuragarri", - "testing_sms_workflow_info_message": "Lan-fluxu hau probatzean, kontuan izan SMSak gutxienez 15 minutu aurretik programatu behar direla", - "start_from_scratch_title": "Hutsetik hasi", - "start_from_scratch_description": "Sortu zure lan-fluxua hutsetik.", - "cal_ai_template_title": "Cal.ai txantiloia", - "cal_ai_template_description": "Bilerak erreserbatu, oroigarriak bidali eta jarraipena egiten duten AI agenteak!", - "voice": "Ahotsa", - "select_voice": "Hautatu ahotsa", - "select_voice_for_agent": "Hautatu ahotsa zure agentearentzat", - "choose_a_voice_for_your_agent": "Aukeratu ahotsa zure agentearentzat", - "trait": "Ezaugarria", - "voice_id": "Ahots ID", - "use_voice": "Erabili ahotsa", - "current_voice": "Uneko ahotsa", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/fi/common.json b/apps/web/public/static/locales/fi/common.json index 9c69b049f860a1..a61edb2765ebf9 100644 --- a/apps/web/public/static/locales/fi/common.json +++ b/apps/web/public/static/locales/fi/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Vahvista sähköpostiosoitteesi varmistaaksesi parhaan sähköposti- ja kalenteritoimituksen", "verify_email_email_header": "Vahvista sähköpostiosoitteesi", "verify_email_button": "Vahvista sähköposti", - "cal_ai_assistant": "Avustaja", + "cal_ai_assistant": "-avustaja", "send_cal_video_transcription_emails": "Lähetä Cal-videotransskriptiosähköpostit", "description_send_cal_video_transcription_emails": "Lähetä sähköpostit Cal-videon tekstityksellä tapaamisen päätyttyä. (Vaatii maksullisen tilauksen)", "verify_email_change_description": "Olet pyytänyt muutosta sähköpostiosoitteeseen, jota käytät {{appName}}-tiliisi kirjautumiseen. Vahvista uusi sähköpostiosoitteesi klikkaamalla alla olevaa painiketta.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Työnkulun validointi epäonnistui", "workflow_validation_empty_fields": "Yhdessä tai useammassa työnkulun vaiheessa on tyhjiä viestisisältöjä", "workflow_validation_unverified_contacts": "Yksi tai useampi puhelinnumero tai sähköpostiosoite ei ole vahvistettu", - "supercharge_your_workflows_with_cal_ai": "Tehosta työnkulkujasi Cal.ai:n avulla", - "supercharge_your_workflows_with_cal_ai_description": "Aidon tuntuiset tekoälyagentit, jotka varaavat tapaamisia, lähettävät muistutuksia ja seuraavat asiakkaitasi.", "phone_number_imported_successfully": "Puhelinnumero tuotu ja liitetty agenttiin onnistuneesti", "phone_number_deleted_successfully": "Puhelinnumero poistettu onnistuneesti", "delete_phone_number_confirmation": "Haluatko varmasti poistaa tämän puhelinnumeron? Tätä toimintoa ei voi kumota.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Puhelinnumeron tilaus peruutettu onnistuneesti", "updating": "Päivitetään", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Hei, miten voit?", "round_robin_description": "Kierrätä tapaamisia tiimin jäsenten välillä.", "managed_event": "Hallinnoitu tapahtuma", "username_placeholder": "käyttäjätunnus", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Haluatko varmasti poistaa tämän työnkulun vaiheen?", "do_you_still_want_to_unsubscribe": "Haluatko silti peruuttaa tämän puhelinnumeron tilauksen tältä agentilta?", "the_action_will_disconnect_phone_number": "Tämä toiminto katkaisee puhelinnumeron yhteyden agenttiin. Agentti ei voi soittaa puheluita ennen kuin uusi puhelinnumero on yhdistetty.", - "cal_ai_phone_numbers": "Puhelinnumerot", + "cal_ai_phone_numbers": "-puhelinnumerot", "connect_phone_number": "Yhdistä puhelinnumero", - "cal_ai_phone_numbers_description": "Hallitse puhelinnumeroitasi", + "cal_ai_phone_numbers_description": "Hallitse -puhelinnumeroitasi", "import_number": "Tuo numero", "this_action_will_also": "Tämä toiminto myös:", "import_phone_number": "Tuo puhelinnumero", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Peruuta puhelinnumerotilauksesi", "delete_associated_phone_number": "Poista liitetty puhelinnumero", "unauthorized_create_workflow": "Sinulla ei ole oikeuksia luoda tätä työnkulkua", - "import_phone_number_description": "Tuo Twilio-puhelinnumerosi käytettäväksi puhelimessa", + "import_phone_number_description": "Tuo Twilio-puhelinnumerosi käytettäväksi Phonen kanssa", "phone_number_cost": "${{price}}/kuukausi", "buy_new_number": "Osta uusi numero", "buy_number_cost_x_per_month": "Puhelinnumeron ostaminen maksaa ${{priceInDollars}} kuukaudessa. Sinulta veloitetaan kuukausittain jokaisesta aktiivisesta puhelinnumerosta.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Kyllä, peruuta tilaus", "cancel_phone_number_subscription_confirmation": "Haluatko varmasti peruuttaa tämän puhelinnumerotilauksen? Tätä toimintoa ei voi kumota ja menetät pääsyn tähän puhelinnumeroon.", "add_members": "Lisää jäseniä...", - "add_members_no_ellipsis": "Lisää jäseniä", "no_assigned_members": "Ei määritettyjä jäseniä", "assigned_to": "Määritetty", "you_must_be_logged_in_to": "Sinun täytyy kirjautua sisään {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategoriat", "pricing": "Hinnoittelu", "learn_more": "Lue lisää", - "try_now": "Kokeile nyt", "privacy_policy": "Tietosuojakäytäntö", "terms_of_service": "Käyttöehdot", "remove": "Poista", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "lähetä WhatsApp-viesti osallistujalle", "workflows": "Työnkulut", "new_workflow_btn": "Uusi työnkulku", - "how_would_you_like_to_start": "Miten haluaisit aloittaa?", "add_new_workflow": "Lisää uusi työnkulku", "reschedule_event_trigger": "kun tapahtuma siirretään", "trigger": "Liipaisin", @@ -1722,8 +1716,6 @@ "event_duration_info": "Tapahtuman kesto", "event_time_info": "Tapahtuman alkamisaika", "event_type_not_found": "Tapahtumatyyppiä ei löytynyt", - "number_to_call_variable": "Soitettava numero", - "number_to_call_info": "Käyttäjän puhelinnumero, johon soitat", "location_variable": "Sijainti", "location_info": "Tapahtuman sijainti", "additional_notes_variable": "Lisätiedot", @@ -1761,7 +1753,6 @@ "team_url": "Tiimin URL", "team_members": "Tiimin jäsenet", "more": "Lisää", - "cal_ai_workflows": "Cal.ai-työnkulut", "and_count_more": "ja {{count}} lisää", "more_page_footer": "Pidämme mobiilisovellusta verkkosovelluksen jatkeena. Jos suoritat monimutkaisia toimintoja, käytä verkkosovellusta.", "workflow_example_1": "Lähetä SMS-muistutus osallistujalle 24 tuntia ennen tapahtuman alkua", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Lähetä sähköpostimuistutus osallistujalle tunti ennen tapahtuman alkua", "workflow_example_5": "Lähetä mukautettu sähköposti järjestäjälle, kun tapahtuma siirretään", "workflow_example_6": "Lähetä mukautettu SMS järjestäjälle, kun uusi tapahtuma varataan", - "send_sms_reminder": "Lähetä tekstiviestimuistutus", - "send_sms_reminder_description": "24 tuntia ennen tapahtuman alkua", - "follow_up_with_no_shows": "Seuraa poisjääneitä", - "follow_up_with_no_shows_description": "30 min tapahtuman päättymisen jälkeen", - "remind_attendees_to_bring_id": "Muistuta osallistujia ottamaan henkilöllisyystodistus mukaan", - "remind_attendees_to_bring_id_description": "1 päivä ennen tapahtuman alkua", - "email_to_remind_booking": "Sähköpostimuistutus", - "email_to_remind_booking_description": "1 tunti ennen tapahtuman alkua", - "custom_sms_reminder": "Mukautettu tekstiviestimuistutus", - "custom_sms_reminder_description": "Kun tapahtuma on aikataulutettu", - "custom_email_reminder": "Mukautettu sähköpostimuistutus", - "custom_email_reminder_description": "Tapahtuma on aikataulutettu uudelleen järjestäjälle", "count_managed_to_limit": "Sisällytä varausmäärät hallinnoiduista tapahtumatyypeistä", "welcome_to_cal_header": "Tervetuloa {{appName}}iin!", "edit_form_later_subtitle": "Voit muokata tätä myöhemmin.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Näytä varaussivulla", "visit_cancelled_booking": "Voit tarkastella peruutettua varausta", "get_started_zapier_templates": "Aloita Zapier-malleilla", - "standard_templates": "Vakiomallit", - "cal_ai_templates": "Cal.ai-mallit", "team_is_unpublished": "{{team}} ei ole julkaistu", "org_is_unpublished_description": "Tämä organisaation linkki ei ole tällä hetkellä käytettävissä. Ota yhteyttä organisaation omistajaan tai pyydä heitä julkaisemaan se.", "team_is_unpublished_description": "Tämä tiimin linkki ei ole tällä hetkellä käytettävissä. Ota yhteyttä tiimin omistajaan tai pyydä heitä julkaisemaan se.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Kortilta ei voitu veloittaa maksua.", "insights": "Analytiikka", "routing_forms": "Reitityskaavakkeet", + "testing_workflow_info_message": "Huomioi tätä työnkulkua testatessasi, että sähköpostit ja tekstiviestit voidaan ajastaa vain vähintään 1 tunnin päähän", "insights_no_data_found_for_filter": "Valituilla suodattimilla tai päivämäärillä ei löytynyt tietoja.", "acknowledge_booking_no_show_fee": "Hyväksyn, että mikäli en saavu paikalle, kortiltani veloitetaan {{amount, currency}} saapumattajättämismaksu.", "days": "päivää", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Tiimi: {{teamName}}", "insights_user_filter": "Käyttäjä: {{userName}}", "insights_subtitle": "Tarkastele varausten tilastoja tapahtumistasi", - "call_history": "Puheluhistoria", - "call_history_subtitle": "Tarkastele puheluhistoriaa Cal.ai-puheluistasi", "location_options": "{{locationCount}} sijaintivaihtoehtoa", - "channel_type": "Kanavatyyppi", - "end_reason": "Päättymissyy", - "session_status": "Istunnon tila", - "user_sentiment": "Käyttäjän mielipide", - "time_header": "Aika", - "from_header": "Lähettäjä", "custom_plan": "Räätälöity paketti", "email_embed": "Sähköpostiupotus", "add_times_to_your_email": "Valitse muutama vapaa aika ja upota ne sähköpostiisi", @@ -2782,8 +2752,6 @@ "account_already_linked": "Tili on jo yhdistetty", "send_email": "Lähetä sähköposti", "cal_ai_phone_call_action": "Soita osallistujalle Cal.ai-ääniagentin avulla", - "call_to_confirm_booking": "Soita vahvistaaksesi varauksen", - "cal_ai_phone_call_action_description": "2 tuntia ennen tapahtuman alkua", "cal_ai_agent_configuration": "Cal.ai-agentin määritykset", "choose_at_least_one_event_type_test_call": "Valitse vähintään yksi tapahtumatyyppi testauspuhelua varten.", "mark_as_no_show": "Merkitse saapumatta jääneeksi", @@ -3235,15 +3203,9 @@ "verify_email_change": "Vahvista sähköpostin muutos", "buy_credits": "Osta krediittejä", "credits": "Krediitit", - "credits_used": "Käytetyt krediitit", - "total_credits_remaining": "Jäljellä yhteensä", - "credits_per_tip_org": "Saat 1000 krediittiä kuukaudessa per tiimin jäsen", - "credits_per_tip_teams": "Saat 750 krediittiä kuukaudessa per tiimin jäsen", - "view_and_manage_credits": "Tarkastele ja hallinnoi krediittejä tekstiviestien lähettämistä varten", + "view_and_manage_credits": "Tarkastele ja hallitse krediittejä", "view_and_manage_credits_description": "Tarkastele ja hallitse krediittejä tekstiviestien lähettämistä varten. Yksi krediitti on arvoltaan 1¢ (USD). <0>Lue lisää", - "credit_worth_description": "Yksi krediitti on arvoltaan 1¢ (USD). <0>Lue lisää", "buy_additional_credits": "Osta lisää krediittejä (0,01 $ per krediitti)", - "view_additional_credits_expense_tip": "Voit tarkastella lisäkrediittien käyttöä kuluraportistasi", "overview": "Yleiskatsaus", "organization_slug_taken": "Organisaation lyhytnimi on jo käytössä", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Et voi luoda organisaatiota, koska olet jo osa organisaatiota", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Cal.com-tiimisi {{teamName}} krediitit ovat loppuneet. Tämän seurauksena tekstiviestit lähetetään nyt sähköpostitse. Jatkaaksesi tekstiviestien lähettämistä, osta lisää krediittejä.", "credit_limit_reached_message_user": "Cal.com-tilisi krediitit ovat loppuneet. Tämän seurauksena tekstiviestit lähetetään nyt sähköpostitse. Jatkaaksesi tekstiviestien lähettämistä, osta lisää krediittejä.", "current_credit_balance": "Nykyinen saldo: {{balance}} krediittiä", - "current_balance": "Nykyinen saldo:", "notification_about_your_booking": "Ilmoitus varauksestasi", "monthly_credits": "Kuukausittaiset krediitit", "total_credits": "Krediittejä yhteensä: {{totalCredits}}", "remaining_credits": "Jäljellä olevat krediitit: {{remainingCredits}}", - "remaining": "Jäljellä", - "total": "Yhteensä", "additional_credits": "Lisäkrediitit", "routing_form_next_in_queue": "{{count}} seuraavana jonossa", "routing_form_select_members_to_email": "Lähetä sähköpostivastaukset henkilöille", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Tarkastele olemassa olevia työnkulkuja ja niiden määrityksiä", "pbac_desc_update_workflows": "Muokkaa työnkulkujen asetuksia", "pbac_desc_delete_workflows": "Poista työnkulkuja järjestelmästä", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Luo webhookeja", - "pbac_desc_view_webhooks": "Tarkastele webhookeja", - "pbac_desc_update_webhooks": "Päivitä webhookit", - "pbac_desc_delete_webhooks": "Poista webhookit", "pbac_desc_manage_workflows": "Täydet hallintaoikeudet kaikkiin työnkulkuihin", "pbac_desc_create_event_types": "Luo tapahtumatyyppejä", "pbac_desc_view_event_types": "Näytä tapahtumatyypit", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Laukaisutapahtuman nimi (esim. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Webhookin luomisaika", "webhook_type": "Tapahtumatyypin tunniste", - "set_up_agent": "Määritä agentti", "webhook_title": "Tapahtumatyypin nimi", "webhook_start_time": "Tapahtuman alkamisaika", "webhook_end_time": "Tapahtuman päättymisaika", @@ -3672,9 +3625,6 @@ "visit": "Käynti", "location_custom_label_input_label": "Mukautettu nimike varaussivulla", "meeting_link": "Tapaamislinkki", - "session_outcome": "Istunnon tulos", - "call_created": "Puhelu luotu", - "voicemail": "Vastaaja", "my_bookings": "Omat varaukset", "phone": "Puhelin", "free": "Ilmainen", @@ -3682,8 +3632,6 @@ "user_name": "Käyttäjän nimi", "expand_panel": "Laajenna paneeli", "collapse_panel": "Supista paneeli", - "email_verification_required": "Tämä tapahtumatyyppi vaatii sähköpostin vahvistuksen", - "invalid_verification_code": "Virheellinen vahvistuskoodi", "you_have_one_team": "Sinulla on yksi tiimi", "consider_consolidating_one_team_org": "Harkitse organisaation perustamista yhdistääksesi laskutuksen, hallintatyökalut ja analytiikan tiimisi kesken.", "consider_consolidating_multi_team_org": "Harkitse organisaation perustamista yhdistääksesi laskutuksen, hallintatyökalut ja analytiikan tiimiesi kesken.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Ennen sovittua aloitusaikaa", "cancel_booking_acknowledge_no_show_fee": "Ymmärrän, että peruuttaessani varauksen {{timeValue}} {{timeUnit}} sisällä aloitusajasta minulta veloitetaan peruutusmaksu {{amount, currency}}", "contact_organizer": "Jos sinulla on kysyttävää, ota yhteyttä järjestäjään.", - "booking_time_option": "Varausaika", - "booking_time_option_description": "Kun varaus on ajoitettu (alusta loppuun)", - "created_at_option": "Luotu", - "created_at_option_description": "Kun varaus alun perin luotiin", - "call_details": "Puhelun tiedot", - "call_id": "Puhelun tunnus", - "call_information": "Puhelun tiedot", - "sentiment": "Tunnelma", - "disconnect_reason": "Katkaisun syy", - "call_summary": "Puhelun yhteenveto", - "transcription": "Litterointi", - "event_details": "Tapahtuman tiedot", - "agent": "Agentti", - "no_transcript_available": "Litterointia ei saatavilla", - "testing_sms_workflow_info_message": "Kun testaat tätä työnkulkua, huomioi että tekstiviestit täytyy ajoittaa vähintään 15 minuuttia etukäteen", - "start_from_scratch_title": "Aloita alusta", - "start_from_scratch_description": "Luo oma työnkulkusi alusta alkaen.", - "cal_ai_template_title": "Cal.ai-malli", - "cal_ai_template_description": "Tekoälyagentit, jotka varaavat tapaamisia, lähettävät muistutuksia ja tekevät seurantaa!", - "voice": "Ääni", - "select_voice": "Valitse ääni", - "select_voice_for_agent": "Valitse ääni agentillesi", - "choose_a_voice_for_your_agent": "Valitse ääni agentillesi", - "trait": "Ominaisuus", - "voice_id": "Äänitunniste", - "use_voice": "Käytä ääntä", - "current_voice": "Nykyinen ääni", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lisää uudet merkkijonot tämän yläpuolelle ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 13a6affc9bba26..2df93bb575ef99 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Vérifiez votre adresse e-mail pour garantir la meilleure délivrabilité des e-mails et des calendriers", "verify_email_email_header": "Vérifiez votre adresse e-mail", "verify_email_button": "Vérifier l'e-mail", - "cal_ai_assistant": "Assistant", + "cal_ai_assistant": "Assistant Cal AI", "send_cal_video_transcription_emails": "Envoyer les e-mails de transcription vidéo Cal", "description_send_cal_video_transcription_emails": "Envoyer des e-mails avec la transcription de la vidéo Cal après la fin de la réunion. (Nécessite un abonnement payant)", "verify_email_change_description": "Vous avez récemment demandé à changer l'adresse e-mail que vous utilisez pour vous connecter à votre compte {{appName}}. Veuillez cliquer sur le bouton ci-dessous pour confirmer votre nouvelle adresse e-mail.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "La validation du workflow a échoué", "workflow_validation_empty_fields": "Un ou plusieurs étapes du workflow ont un contenu de message vide", "workflow_validation_unverified_contacts": "Un ou plusieurs numéros de téléphone ou adresses e-mail ne sont pas vérifiés", - "supercharge_your_workflows_with_cal_ai": "Optimisez vos flux de travail avec Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Des agents IA réalistes qui réservent des réunions, envoient des rappels et assurent le suivi avec vos clients.", "phone_number_imported_successfully": "Numéro de téléphone importé et lié à l'agent avec succès", "phone_number_deleted_successfully": "Numéro de téléphone supprimé avec succès", "delete_phone_number_confirmation": "Êtes-vous sûr de vouloir supprimer ce numéro de téléphone ? Cette action ne peut pas être annulée.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Abonnement au numéro de téléphone annulé avec succès", "updating": "Mise à jour en cours", "round_robin": "Round-robin", - "hi_how_are_you_doing": "Bonjour, comment allez-vous ?", "round_robin_description": "Alternez vos rendez-vous entre plusieurs membres d'équipe.", "managed_event": "Événement géré", "username_placeholder": "nom d'utilisateur", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Êtes-vous sûr de vouloir supprimer cette étape du workflow ?", "do_you_still_want_to_unsubscribe": "Voulez-vous toujours désabonner ce numéro de téléphone de cet agent ?", "the_action_will_disconnect_phone_number": "Cette action déconnectera le numéro de téléphone de l'agent. L'agent ne pourra pas passer d'appels tant qu'un nouveau numéro de téléphone ne sera pas connecté.", - "cal_ai_phone_numbers": "Numéros de téléphone", + "cal_ai_phone_numbers": "Numéros de téléphone Cal AI", "connect_phone_number": "Connecter un numéro de téléphone", - "cal_ai_phone_numbers_description": "Gérez vos numéros de téléphone", + "cal_ai_phone_numbers_description": "Gérez vos numéros de téléphone Cal AI", "import_number": "Importer un numéro", "this_action_will_also": "Cette action va également :", "import_phone_number": "Importer un numéro de téléphone", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Oui, annuler l'abonnement", "cancel_phone_number_subscription_confirmation": "Êtes-vous sûr de vouloir annuler cet abonnement au numéro de téléphone ? Cette action ne peut pas être annulée et vous perdrez l'accès à ce numéro de téléphone.", "add_members": "Ajouter des membres...", - "add_members_no_ellipsis": "Ajouter des membres", "no_assigned_members": "Aucun membre assigné", "assigned_to": "Assigné à", "you_must_be_logged_in_to": "Vous devez être connecté(e) à {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Catégories", "pricing": "Prix", "learn_more": "En savoir plus", - "try_now": "Essayer maintenant", "privacy_policy": "Politique de confidentialité", "terms_of_service": "Conditions d'utilisation", "remove": "Supprimer", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "envoyer un message WhatsApp au participant", "workflows": "Workflows", "new_workflow_btn": "Nouveau workflow", - "how_would_you_like_to_start": "Comment souhaitez-vous commencer ?", "add_new_workflow": "Ajouter un nouveau workflow", "reschedule_event_trigger": "lorsque l'événement est replanifié", "trigger": "Déclencheur", @@ -1722,8 +1716,6 @@ "event_duration_info": "La durée de l'événement", "event_time_info": "Heure de début de l'événement", "event_type_not_found": "Type d'événement introuvable", - "number_to_call_variable": "Numéro à appeler", - "number_to_call_info": "Le numéro de téléphone de l'utilisateur que vous appelez", "location_variable": "Lieu", "location_info": "Lieu de l'événement", "additional_notes_variable": "Notes supplémentaires", @@ -1761,7 +1753,6 @@ "team_url": "Lien de l'équipe", "team_members": "Membres de l'équipe", "more": "Plus", - "cal_ai_workflows": "Flux de travail Cal.ai", "and_count_more": "et {{count}} de plus", "more_page_footer": "Nous considérons l'application mobile comme une extension de l'application web. Si vous effectuez des actions complexes, veuillez vous référer à l'application web.", "workflow_example_1": "Envoyer un rappel par SMS 24 heures avant le début de l'événement", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Envoyer un rappel par e-mail aux participants 1 heure avant le début des événements", "workflow_example_5": "Envoyer un e-mail personnalisé à l'hôte lorsque l'événement est replanifié", "workflow_example_6": "Envoyer un SMS personnalisé à l'hôte lorsqu'un nouvel événement est réservé", - "send_sms_reminder": "Envoyer un rappel par SMS", - "send_sms_reminder_description": "24 heures avant le début de l'événement", - "follow_up_with_no_shows": "Suivi des absents", - "follow_up_with_no_shows_description": "30 minutes après la fin de l'événement", - "remind_attendees_to_bring_id": "Rappeler aux participants d'apporter une pièce d'identité", - "remind_attendees_to_bring_id_description": "1 jour avant le début de l'événement", - "email_to_remind_booking": "Rappel par e-mail", - "email_to_remind_booking_description": "1 heure avant le début de l'événement", - "custom_sms_reminder": "Rappel SMS personnalisé", - "custom_sms_reminder_description": "Lorsque l'événement est programmé", - "custom_email_reminder": "Rappel par e-mail personnalisé", - "custom_email_reminder_description": "L'événement est replanifié pour l'hôte", "count_managed_to_limit": "Inclure les compteurs de réservation des types d'événements gérés", "welcome_to_cal_header": "Bienvenue sur {{appName}} !", "edit_form_later_subtitle": "Vous pourrez modifier cela ultérieurement.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Afficher sur la page de réservation", "visit_cancelled_booking": "Vous pouvez consulter la page de réservation annulée", "get_started_zapier_templates": "Démarrer avec les modèles Zapier", - "standard_templates": "Modèles standard", - "cal_ai_templates": "Modèles Cal.ai", "team_is_unpublished": "{{team}} n'est pas publiée", "org_is_unpublished_description": "Ce lien d'organisation n'est actuellement pas disponible. Veuillez contacter le propriétaire de l'organisation ou lui demander de le publier.", "team_is_unpublished_description": "Ce lien d'équipe n'est actuellement pas disponible. Veuillez contacter le propriétaire de l'équipe ou lui demander de le publier.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Impossible de débiter la carte pour le paiement.", "insights": "Statistiques", "routing_forms": "Formulaires de routage", + "testing_workflow_info_message": "Lors du test de ce workflow, sachez que les e-mails et les SMS ne peuvent être programmés qu'au moins 1 heure à l'avance", "insights_no_data_found_for_filter": "Aucune donnée trouvée pour le filtre ou les dates sélectionnés.", "acknowledge_booking_no_show_fee": "Je reconnais que si je ne participe pas à cet événement, des frais d'absence de {{amount, currency}} seront appliqués à ma carte.", "days": "jours", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Équipe : {{teamName}}", "insights_user_filter": "Utilisateur : {{userName}}", "insights_subtitle": "Visualisez les statistiques de réservation à travers vos événements", - "call_history": "Historique des appels", - "call_history_subtitle": "Consultez l'historique des appels pour tous vos appels Cal.ai", "location_options": "{{locationCount}} options de lieu", - "channel_type": "Type de canal", - "end_reason": "Raison de fin", - "session_status": "Statut de la session", - "user_sentiment": "Sentiment de l'utilisateur", - "time_header": "Heure", - "from_header": "De", "custom_plan": "Plan personnalisé", "email_embed": "Intégration aux e-mails", "add_times_to_your_email": "Sélectionnez quelques créneaux disponibles et intégrez-les dans votre e-mail.", @@ -2782,8 +2752,6 @@ "account_already_linked": "Le compte est déjà associé", "send_email": "Envoyer un e-mail", "cal_ai_phone_call_action": "Appeler le participant en utilisant l'agent vocal Cal.ai", - "call_to_confirm_booking": "Appel pour confirmer la réservation", - "cal_ai_phone_call_action_description": "2 heures avant le début de l'événement", "cal_ai_agent_configuration": "Configuration de l'agent Cal.ai", "choose_at_least_one_event_type_test_call": "Veuillez choisir au moins un type d'événement pour effectuer un appel test.", "mark_as_no_show": "Marquer comme absence", @@ -3235,15 +3203,9 @@ "verify_email_change": "Vérifier le changement d'e-mail", "buy_credits": "Acheter des crédits", "credits": "Crédits", - "credits_used": "Crédits utilisés", - "total_credits_remaining": "Total restant", - "credits_per_tip_org": "Vous recevez 1000 crédits par mois, par membre de l'équipe", - "credits_per_tip_teams": "Vous recevez 750 crédits par mois, par membre de l'équipe", - "view_and_manage_credits": "Consulter et gérer les crédits pour l'envoi de SMS", + "view_and_manage_credits": "Voir et gérer les crédits", "view_and_manage_credits_description": "Consultez et gérez les crédits pour l'envoi de SMS. Un crédit vaut 1¢ (USD). <0>En savoir plus", - "credit_worth_description": "Un crédit vaut 1¢ (USD). <0>En savoir plus", "buy_additional_credits": "Acheter des crédits supplémentaires (0,01 $ par crédit)", - "view_additional_credits_expense_tip": "Vous pouvez consulter les dépenses de crédits supplémentaires dans votre journal des dépenses", "overview": "Aperçu", "organization_slug_taken": "L'identifiant de l'organisation est déjà pris", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Vous ne pouvez pas créer une organisation car vous faites déjà partie d'une organisation", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Votre équipe Cal.com {{teamName}} n'a plus de crédits. Par conséquent, les SMS sont maintenant envoyés par e-mail. Pour reprendre l'envoi de SMS, veuillez acheter des crédits supplémentaires.", "credit_limit_reached_message_user": "Votre compte Cal.com n'a plus de crédits. Par conséquent, les messages SMS sont désormais envoyés par e-mail. Pour reprendre l'envoi de SMS, veuillez acheter des crédits supplémentaires.", "current_credit_balance": "Solde actuel : {{balance}} crédits", - "current_balance": "Solde actuel :", "notification_about_your_booking": "Notification concernant votre réservation", "monthly_credits": "Crédits mensuels", "total_credits": "Total des crédits : {{totalCredits}}", "remaining_credits": "Crédits restants : {{remainingCredits}}", - "remaining": "Restant", - "total": "Total", "additional_credits": "Crédits supplémentaires", "routing_form_next_in_queue": "{{count}} suivant(s) dans la file d'attente", "routing_form_select_members_to_email": "Envoyer les réponses par e-mail à", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Voir les workflows existants et leurs configurations", "pbac_desc_update_workflows": "Éditer et modifier les paramètres des workflows", "pbac_desc_delete_workflows": "Supprimer des workflows du système", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Créer des webhooks", - "pbac_desc_view_webhooks": "Consulter les webhooks", - "pbac_desc_update_webhooks": "Mettre à jour les webhooks", - "pbac_desc_delete_webhooks": "Supprimer les webhooks", "pbac_desc_manage_workflows": "Accès complet à la gestion de tous les workflows", "pbac_desc_create_event_types": "Créer des types d'événements", "pbac_desc_view_event_types": "Voir les types d'événements", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Le nom de l'événement déclencheur (par ex., BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "L'heure du webhook", "webhook_type": "Le slug du type d'événement", - "set_up_agent": "Configurer l'agent", "webhook_title": "Le nom du type d'événement", "webhook_start_time": "L'heure de début de l'événement", "webhook_end_time": "L'heure de fin de l'événement", @@ -3672,9 +3625,6 @@ "visit": "Visiter", "location_custom_label_input_label": "Étiquette personnalisée sur la page de réservation", "meeting_link": "Lien de réunion", - "session_outcome": "Résultat de la session", - "call_created": "Appel créé", - "voicemail": "Messagerie vocale", "my_bookings": "Mes réservations", "phone": "Téléphone", "free": "Gratuit", @@ -3682,8 +3632,6 @@ "user_name": "Nom d'utilisateur", "expand_panel": "Développer le panneau", "collapse_panel": "Réduire le panneau", - "email_verification_required": "La vérification de l'e-mail est requise pour ce type d'événement", - "invalid_verification_code": "Code de vérification fourni invalide", "you_have_one_team": "Vous avez une équipe", "consider_consolidating_one_team_org": "Envisagez de créer une organisation pour unifier la facturation, les outils d'administration et les analyses pour votre équipe.", "consider_consolidating_multi_team_org": "Envisagez de créer une organisation pour unifier la facturation, les outils d'administration et les analyses pour vos équipes.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Avant l'heure de début prévue", "cancel_booking_acknowledge_no_show_fee": "Je reconnais qu'en annulant la réservation dans les {{timeValue}} {{timeUnit}} précédant l'heure de début, des frais de non-présentation de {{amount, currency}} me seront facturés", "contact_organizer": "Si vous avez des questions, veuillez contacter l'organisateur.", - "booking_time_option": "Heure de réservation", - "booking_time_option_description": "Quand la réservation est programmée (début à fin)", - "created_at_option": "Créé le", - "created_at_option_description": "Quand la réservation a été initialement créée", - "call_details": "Détails de l'appel", - "call_id": "ID d'appel", - "call_information": "Informations d'appel", - "sentiment": "Sentiment", - "disconnect_reason": "Raison de déconnexion", - "call_summary": "Résumé de l'appel", - "transcription": "Transcription", - "event_details": "Détails de l'événement", - "agent": "Agent", - "no_transcript_available": "Aucune transcription disponible", - "testing_sms_workflow_info_message": "Lors du test de ce workflow, sachez que les SMS doivent être programmés au moins 15 minutes à l'avance", - "start_from_scratch_title": "Commencer de zéro", - "start_from_scratch_description": "Créez votre propre workflow à partir de zéro.", - "cal_ai_template_title": "Modèle Cal.ai", - "cal_ai_template_description": "Agents IA qui réservent des réunions, envoient des rappels et assurent le suivi !", - "voice": "Voix", - "select_voice": "Sélectionner une voix", - "select_voice_for_agent": "Sélectionner une voix pour votre agent", - "choose_a_voice_for_your_agent": "Choisissez une voix pour votre agent", - "trait": "Caractéristique", - "voice_id": "ID de voix", - "use_voice": "Utiliser la voix", - "current_voice": "Voix actuelle", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index c48d0ecbe162e0..534e3b86e75aee 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "יש לאמת את כתובת הדוא״ל כדי להבטיח שתוכל/י לקבל הודעות לכתובת הדוא״ל וללוח השנה בצורה הטובה ביותר", "verify_email_email_header": "יש לאמת את כתובת הדוא״ל שלך", "verify_email_button": "אימות דוא\"ל", - "cal_ai_assistant": "עוזר", + "cal_ai_assistant": "מסייע בינה מלאכותית ל־Cal", "send_cal_video_transcription_emails": "שלח תמלולי וידאו של Cal במייל", "description_send_cal_video_transcription_emails": "שליחת מיילים עם תמלול הווידאו של Cal לאחר סיום הפגישה. (נדרשת חבילה בתשלום)", "verify_email_change_description": "בדקות האחרונות ביקשת לשנות את כתובת הדוא״ל שמשמשת אותך לכניסה לחשבון שלך אצל {{appName}}. נא ללחוץ על הכפתור להלן כדי לאשר את כתובת הדוא״ל החדשה שלך.", @@ -819,8 +819,6 @@ "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": "האם אתה בטוח שברצונך למחוק את מספר הטלפון הזה? לא ניתן לבטל פעולה זו.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "מנוי מספר הטלפון בוטל בהצלחה", "updating": "מעדכן", "round_robin": "לפי תורות", - "hi_how_are_you_doing": "היי, מה שלומך?", "round_robin_description": "פגישות מחזוריות בין חברי צוות מרובים.", "managed_event": "אירוע מנוהל", "username_placeholder": "שם משתמש", @@ -879,9 +876,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_phone_numbers": "מספרי טלפון של Cal AI", "connect_phone_number": "חבר מספר טלפון", - "cal_ai_phone_numbers_description": "נהל את מספרי הטלפון שלך", + "cal_ai_phone_numbers_description": "נהל את מספרי הטלפון של שלך", "import_number": "ייבא מספר", "this_action_will_also": "פעולה זו גם:", "import_phone_number": "ייבא מספר טלפון", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "בטל את המנוי למספר הטלפון שלך", "delete_associated_phone_number": "מחק את מספר הטלפון המשויך", "unauthorized_create_workflow": "אינך מורשה ליצור את תהליך העבודה הזה", - "import_phone_number_description": "ייבא את מספר הטלפון שלך מ-Twilio לשימוש עם טלפון", + "import_phone_number_description": "ייבא את מספר הטלפון של Twilio שלך לשימוש עם Phone", "phone_number_cost": "${{price}}/חודש", "buy_new_number": "קנה מספר חדש", "buy_number_cost_x_per_month": "קניית מספר טלפון עולה ${{priceInDollars}} לחודש. תחויב מדי חודש עבור כל מספר טלפון פעיל.", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "קטגוריות", "pricing": "תמחור", "learn_more": "מידע נוסף", - "try_now": "נסה/י עכשיו", "privacy_policy": "מדיניות פרטיות", "terms_of_service": "תנאי שימוש", "remove": "הסר", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "שליחת Whatsapp למשתתף", "workflows": "תהליכים", "new_workflow_btn": "תהליך עבודה חדש", - "how_would_you_like_to_start": "איך תרצה/י להתחיל?", "add_new_workflow": "הוסף תהליך חדש", "reschedule_event_trigger": "כאשר נקבע מועד חדש לאירוע", "trigger": "גורם מפעיל", @@ -1722,8 +1716,6 @@ "event_duration_info": "משך האירוע", "event_time_info": "שעת ההתחלה של האירוע", "event_type_not_found": "EventType לא נמצא", - "number_to_call_variable": "מספר לחיוג", - "number_to_call_info": "מספר הטלפון של המשתמש שאליו אתה מתקשר", "location_variable": "מיקום", "location_info": "מיקום האירוע", "additional_notes_variable": "הערות נוספות", @@ -1761,7 +1753,6 @@ "team_url": "כתובת ה-URL של הצוות", "team_members": "חברי הצוות", "more": "עוד", - "cal_ai_workflows": "תהליכי עבודה של Cal.ai", "and_count_more": "ועוד {{count}}", "more_page_footer": "אנחנו רואים באפליקציה לנייד הרחבה של אפליקציית האינטרנט. אם עליך לבצע פעולות מורכבות, כדאי לחזור לאפליקציית האינטרנט.", "workflow_example_1": "לשלוח תזכורת באמצעות SMS למשתתף/ת 24 שעות לפני תחילת האירוע", @@ -1770,18 +1761,6 @@ "workflow_example_4": "לשלוח למשתתף/ת תזכורת בדוא\"ל שעה אחת לפני תחילת האירועים", "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": "יום לפני תחילת האירוע", - "email_to_remind_booking": "תזכורת בדוא\"ל", - "email_to_remind_booking_description": "שעה לפני תחילת האירוע", - "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": "תוכל/י לערוך זאת מאוחר יותר.", @@ -2205,8 +2184,6 @@ "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}} או לבקש מהם לפרסם אותו.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "לא ניתן לחייב את הכרטיס לתשלום.", "insights": "תובנות", "routing_forms": "טפסי ניתוב", + "testing_workflow_info_message": "במהלך בדיקת תהליך העבודה הזה, קח/י בחשבון שניתן לתזמן הודעות דוא\"ל ו-SMS לפחות שעה אחת מראש", "insights_no_data_found_for_filter": "לא נמצאו נתונים עבור המסנן שנבחר או התאריכים שנבחרו.", "acknowledge_booking_no_show_fee": "מובן לי שאם לא אשתתף באירוע הזה, דמי אי-הגעה בסך {{amount, currency}} ינוכו מהכרטיס שלי.", "days": "ימים", @@ -2486,15 +2464,7 @@ "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": "בחר/י כמה מועדים פנויים והטבע/י אותם בדוא\"ל", @@ -2782,8 +2752,6 @@ "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": "סמן כהיעדרות", @@ -3235,15 +3203,9 @@ "verify_email_change": "אימות שינוי כתובת דוא\"ל", "buy_credits": "קנה נקודות זכות", "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": "צפה ונהל נקודות זכות", "view_and_manage_credits_description": "צפה ונהל נקודות זכות לשליחת הודעות SMS. נקודת זכות אחת שווה 1¢ (דולר). <0>למידע נוסף", - "credit_worth_description": "קרדיט אחד שווה 1¢ (דולר ארה\"ב). <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": "אינך יכול ליצור ארגון מכיוון שאתה כבר חלק מארגון", @@ -3424,13 +3386,10 @@ "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": "שלח תגובות בדוא\"ל אל", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "צפייה בתהליכים קיימים ובהגדרותיהם", "pbac_desc_update_workflows": "עריכה ושינוי הגדרות תהליכים", "pbac_desc_delete_workflows": "הסרת תהליכים מהמערכת", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "יצירת רכיבי webhook", - "pbac_desc_view_webhooks": "צפייה ברכיבי webhook", - "pbac_desc_update_webhooks": "עדכון webhooks", - "pbac_desc_delete_webhooks": "מחיקת webhooks", "pbac_desc_manage_workflows": "גישת ניהול מלאה לכל התהליכים", "pbac_desc_create_event_types": "יצירת סוגי אירועים", "pbac_desc_view_event_types": "צפייה בסוגי אירועים", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "שם אירוע הטריגר (לדוגמה, BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "זמן יצירת ה-webhook", "webhook_type": "מזהה סוג האירוע", - "set_up_agent": "הגדרת סוכן", "webhook_title": "שם סוג האירוע", "webhook_start_time": "זמן התחלת האירוע", "webhook_end_time": "זמן סיום האירוע", @@ -3672,9 +3625,6 @@ "visit": "ביקור", "location_custom_label_input_label": "תווית מותאמת אישית בדף ההזמנה", "meeting_link": "קישור לפגישה", - "session_outcome": "תוצאת הפגישה", - "call_created": "שיחה נוצרה", - "voicemail": "תא קולי", "my_bookings": "ההזמנות שלי", "phone": "טלפון", "free": "חינם", @@ -3682,8 +3632,6 @@ "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": "שקול להקים ארגון כדי לאחד חיובים, כלי ניהול ואנליטיקה עבור הצוותים שלך.", @@ -3704,32 +3652,5 @@ "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": "סוכני בינה מלאכותית שקובעים פגישות, שולחים תזכורות, ועוקבים אחרי!", - "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/hu/common.json b/apps/web/public/static/locales/hu/common.json index 3a12df1e1488fb..be4d1dc2926d2e 100644 --- a/apps/web/public/static/locales/hu/common.json +++ b/apps/web/public/static/locales/hu/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Igazold vissza az e-mail címed, hogy biztosíthassuk Neked a legjobb email és naptár funkciókat", "verify_email_email_header": "Igazold vissza az e-mail címed", "verify_email_button": "E-mail cím megerősítése", - "cal_ai_assistant": "Asszisztens", + "cal_ai_assistant": "asszisztens", "send_cal_video_transcription_emails": "Cal videó átiratok küldése e-mailben", "description_send_cal_video_transcription_emails": "A megbeszélés után e-mailben elküldi a Cal videó átiratát. (Fizetős előfizetés szükséges)", "verify_email_change_description": "Nemrég kérte a(z) {{appName}} fiókjába való bejelentkezéshez használt e-mail cím módosítását. Kérjük, kattintson az alábbi gombra az új e-mail cím megerősítéséhez.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "A munkafolyamat érvényesítése sikertelen", "workflow_validation_empty_fields": "Egy vagy több munkafolyamat lépésnek üres az üzenettartalma", "workflow_validation_unverified_contacts": "Egy vagy több telefonszám vagy e-mail cím nincs ellenőrizve", - "supercharge_your_workflows_with_cal_ai": "Turbózd fel munkafolyamataidat a Cal.ai-jal", - "supercharge_your_workflows_with_cal_ai_description": "Élethű AI ügynökök, amelyek találkozókat foglalnak, emlékeztetőket küldenek és nyomon követik ügyfeleidet.", "phone_number_imported_successfully": "A telefonszám importálása és ügynökhöz kapcsolása sikeres", "phone_number_deleted_successfully": "A telefonszám törlése sikeres", "delete_phone_number_confirmation": "Biztosan törölni szeretné ezt a telefonszámot? Ez a művelet nem vonható vissza.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "A telefonszám-előfizetés sikeresen lemondva", "updating": "Frissítés folyamatban", "round_robin": "Körmérkőzés", - "hi_how_are_you_doing": "Szia, hogy vagy?", "round_robin_description": "Ciklikus találkozókat több csapattag között.", "managed_event": "Kezelt esemény", "username_placeholder": "felhasználónév", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Biztosan törölni szeretné ezt a munkafolyamat lépést?", "do_you_still_want_to_unsubscribe": "Továbbra is le szeretné iratkozni a telefonszámot erről az ügynökről?", "the_action_will_disconnect_phone_number": "Ez a művelet leválasztja a telefonszámot az ügynökről. Az ügynök nem tud hívásokat kezdeményezni, amíg új telefonszámot nem csatlakoztat.", - "cal_ai_phone_numbers": "Telefonszámok", + "cal_ai_phone_numbers": "telefonszámok", "connect_phone_number": "Telefonszám csatlakoztatása", - "cal_ai_phone_numbers_description": "Telefonszámaid kezelése", + "cal_ai_phone_numbers_description": "Kezelje telefonszámait", "import_number": "Szám importálása", "this_action_will_also": "Ez a művelet továbbá:", "import_phone_number": "Telefonszám importálása", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Telefonszám előfizetés lemondása", "delete_associated_phone_number": "A kapcsolódó telefonszám törlése", "unauthorized_create_workflow": "Nincs jogosultsága létrehozni ezt a munkafolyamatot", - "import_phone_number_description": "Importáld Twilio telefonszámodat a Telefon funkcióval való használathoz", + "import_phone_number_description": "Importálja Twilio telefonszámát a Phone használatához", "phone_number_cost": "${{price}}/hónap", "buy_new_number": "Új szám vásárlása", "buy_number_cost_x_per_month": "Egy telefonszám vásárlása havi ${{priceInDollars}} összegbe kerül. Minden aktív telefonszámért havonta díjat számítunk fel.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Igen, előfizetés lemondása", "cancel_phone_number_subscription_confirmation": "Biztosan le szeretné mondani ezt a telefonszám előfizetést? Ez a művelet nem vonható vissza, és elveszíti a hozzáférést ehhez a telefonszámhoz.", "add_members": "Tagok hozzáadása...", - "add_members_no_ellipsis": "Tagok hozzáadása", "no_assigned_members": "Nincsenek hozzárendelt tagok", "assigned_to": "Hozzárendelve", "you_must_be_logged_in_to": "Be kell jelentkeznie a következő címen: {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategóriák", "pricing": "Árazás", "learn_more": "Tudj meg többet", - "try_now": "Próbáld ki most", "privacy_policy": "Adatvédelmi Irányelvek", "terms_of_service": "Szolgáltatás feltételei", "remove": "Eltávolítás", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "küldjön WhatsApp üzenetet a résztvevőnek", "workflows": "Munkafolyamatok", "new_workflow_btn": "Új munkafolyamat", - "how_would_you_like_to_start": "Hogyan szeretnéd kezdeni?", "add_new_workflow": "Új munkafolyamat hozzáadása", "reschedule_event_trigger": "amikor az időpont módosítva lett", "trigger": "Kiváltó", @@ -1722,8 +1716,6 @@ "event_duration_info": "Az esemény időtartama", "event_time_info": "Az esemény kezdési időpontja", "event_type_not_found": "Eseménytípus nem található", - "number_to_call_variable": "Hívandó szám", - "number_to_call_info": "A felhasználó telefonszáma, akit hívsz", "location_variable": "Helyszín", "location_info": "Az esemény helyszíne", "additional_notes_variable": "Egyéb megjegyzések", @@ -1761,7 +1753,6 @@ "team_url": "Csapat URL-je", "team_members": "Csapattagok", "more": "Több", - "cal_ai_workflows": "Cal.ai munkafolyamatok", "and_count_more": "és még {{count}}", "more_page_footer": "A mobilalkalmazást a webes alkalmazás kiterjesztésének tekintjük. Ha bonyolult műveleteket hajt végre, nézzen vissza a webalkalmazáshoz.", "workflow_example_1": "SMS-emlékeztető küldése 24 órával az esemény kezdete előtt a résztvevőknek", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Emlékeztető küldése e-mailben 1 órával az események kezdete előtt a résztvevők számára", "workflow_example_5": "Egyéni e-mail küldése, ha az eseményt átütemezték a házigazdának", "workflow_example_6": "Egyéni SMS küldése, ha új eseményt lefoglaltak a házigazdának", - "send_sms_reminder": "SMS emlékeztető küldése", - "send_sms_reminder_description": "24 órával az esemény kezdete előtt", - "follow_up_with_no_shows": "Utánkövetés a meg nem jelenteknél", - "follow_up_with_no_shows_description": "30 perccel az esemény vége után", - "remind_attendees_to_bring_id": "Emlékeztesd a résztvevőket, hogy hozzanak igazolványt", - "remind_attendees_to_bring_id_description": "1 nappal az esemény kezdete előtt", - "email_to_remind_booking": "E-mail emlékeztető", - "email_to_remind_booking_description": "1 órával az esemény kezdete előtt", - "custom_sms_reminder": "Egyéni SMS emlékeztető", - "custom_sms_reminder_description": "Amikor az esemény be van ütemezve", - "custom_email_reminder": "Egyéni e-mail emlékeztető", - "custom_email_reminder_description": "Esemény átütemezve a házigazdának", "count_managed_to_limit": "Tartalmazza a foglalási számokat a kezelt eseménytípusokból", "welcome_to_cal_header": "Üdvözli a {{appName}}!", "edit_form_later_subtitle": "Ezt később is szerkesztheted.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Megjelenítés a foglalási oldalon", "visit_cancelled_booking": "Megtekintheti a lemondott foglalás oldalát", "get_started_zapier_templates": "Kezdje el a Zapier-sablonokat", - "standard_templates": "Általános sablonok", - "cal_ai_templates": "Cal.ai sablonok", "team_is_unpublished": "{{team}} nincs közzétéve", "org_is_unpublished_description": "Ez a szervezeti link jelenleg nem érhető el. Kérjük, lépjen kapcsolatba a szervezet tulajdonosával, vagy kérje meg, hogy tegye közzé.", "team_is_unpublished_description": "Ez a csapatlink jelenleg nem érhető el. Kérjük, lépjen kapcsolatba a csapat tulajdonosával, vagy kérje meg, hogy tegye közzé.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Nem sikerült megterhelni a kártyát a fizetésért.", "insights": "Betekintés", "routing_forms": "Irányító űrlapok", + "testing_workflow_info_message": "A munkafolyamat tesztelésekor ügyeljen arra, hogy az e-mailek és SMS-ek legalább 1 órával későbbre ütemezhetők", "insights_no_data_found_for_filter": "Nem található adat a kiválasztott szűrőhöz vagy a kiválasztott dátumokhoz.", "acknowledge_booking_no_show_fee": "Tudomásul veszem, hogy ha nem veszek részt ezen az eseményen, {{amount, currency}} meg nem jelenési díj kerül felszámításra.", "days": "nap", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Csapat: {{teamName}}", "insights_user_filter": "Felhasználó: {{userName}}", "insights_subtitle": "Tekintse meg az eseményekre vonatkozó foglalási statisztikákat", - "call_history": "Hívástörténet", - "call_history_subtitle": "Tekintse meg a Cal.ai hívások teljes hívástörténetét", "location_options": "{{locationCount}} helybeállítás", - "channel_type": "Csatorna típusa", - "end_reason": "Befejezés oka", - "session_status": "Munkamenet állapota", - "user_sentiment": "Felhasználói hangulat", - "time_header": "Idő", - "from_header": "Feladó", "custom_plan": "Egyedi csomag", "email_embed": "E-mail beágyazás", "add_times_to_your_email": "Válasszon ki néhány elérhető időpontot, és ágyazza be e-mailjeibe", @@ -2782,8 +2752,6 @@ "account_already_linked": "A fiók már össze van kapcsolva", "send_email": "Email küldése", "cal_ai_phone_call_action": "Résztvevő hívása Cal.ai hangsegéddel", - "call_to_confirm_booking": "Hívás a foglalás megerősítéséhez", - "cal_ai_phone_call_action_description": "2 órával az esemény kezdete előtt", "cal_ai_agent_configuration": "Cal.ai ügynök konfigurálása", "choose_at_least_one_event_type_test_call": "Kérjük, válasszon legalább egy eseménytípust a teszthívás indításához.", "mark_as_no_show": "Megjelölés nem megjelentként", @@ -3235,15 +3203,9 @@ "verify_email_change": "E-mail-cím módosításának megerősítése", "buy_credits": "Kreditek vásárlása", "credits": "Kreditek", - "credits_used": "Felhasznált kreditek", - "total_credits_remaining": "Összes fennmaradó", - "credits_per_tip_org": "Havonta 1000 kreditet kap csapattaganként", - "credits_per_tip_teams": "Havonta 750 kreditet kap csapattaganként", - "view_and_manage_credits": "SMS-küldéshez használt kreditek megtekintése és kezelése", + "view_and_manage_credits": "Kreditek megtekintése és kezelése", "view_and_manage_credits_description": "SMS üzenetek küldéséhez szükséges kreditek megtekintése és kezelése. Egy kredit értéke 1¢ (USD). <0>További információ", - "credit_worth_description": "Egy kredit értéke 1¢ (USD). <0>További információ", "buy_additional_credits": "További kreditek vásárlása (0,01 USD kreditenként)", - "view_additional_credits_expense_tip": "A további kreditköltéseket a kiadási naplóban tekintheti meg", "overview": "Áttekintés", "organization_slug_taken": "A szervezet slug-ja már foglalt", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Nem hozhat létre szervezetet, mivel már tagja egy szervezetnek", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "A Cal.com {{teamName}} csapatod kreditjei elfogytak. Ennek eredményeként az SMS üzeneteket most e-mailben küldjük. Az SMS küldés folytatásához vásárolj további krediteket.", "credit_limit_reached_message_user": "A Cal.com fiókod kreditjei elfogytak. Ennek következtében az SMS-üzenetek most e-mailben kerülnek kiküldésre. Az SMS-küldés folytatásához kérjük, vásárolj további krediteket.", "current_credit_balance": "Jelenlegi egyenleg: {{balance}} kredit", - "current_balance": "Jelenlegi egyenleg:", "notification_about_your_booking": "Értesítés a foglalásodról", "monthly_credits": "Havi kreditek", "total_credits": "Összes kredit: {{totalCredits}}", "remaining_credits": "Fennmaradó kreditek: {{remainingCredits}}", - "remaining": "Fennmaradó", - "total": "Összesen", "additional_credits": "További kreditek", "routing_form_next_in_queue": "{{count}} következő a sorban", "routing_form_select_members_to_email": "E-mail válaszok küldése a következőknek", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Meglévő munkafolyamatok és konfigurációik megtekintése", "pbac_desc_update_workflows": "Munkafolyamat-beállítások szerkesztése és módosítása", "pbac_desc_delete_workflows": "Munkafolyamatok eltávolítása a rendszerből", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Webhookok létrehozása", - "pbac_desc_view_webhooks": "Webhookok megtekintése", - "pbac_desc_update_webhooks": "Webhookok frissítése", - "pbac_desc_delete_webhooks": "Webhookok törlése", "pbac_desc_manage_workflows": "Teljes kezelési hozzáférés minden munkafolyamathoz", "pbac_desc_create_event_types": "Eseménytípusok létrehozása", "pbac_desc_view_event_types": "Eseménytípusok megtekintése", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "A kiváltó esemény neve (pl. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "A webhook időpontja", "webhook_type": "Az eseménytípus azonosítója", - "set_up_agent": "Ügynök beállítása", "webhook_title": "Az eseménytípus neve", "webhook_start_time": "Az esemény kezdési időpontja", "webhook_end_time": "Az esemény befejezési időpontja", @@ -3672,9 +3625,6 @@ "visit": "Látogatás", "location_custom_label_input_label": "Egyéni címke a foglalási oldalon", "meeting_link": "Találkozó link", - "session_outcome": "Munkamenet eredménye", - "call_created": "Hívás létrehozva", - "voicemail": "Hangposta", "my_bookings": "Foglalásaim", "phone": "Telefon", "free": "Ingyenes", @@ -3682,8 +3632,6 @@ "user_name": "Felhasználónév", "expand_panel": "Panel kibontása", "collapse_panel": "Panel összecsukása", - "email_verification_required": "Ehhez az eseménytípushoz e-mail ellenőrzés szükséges", - "invalid_verification_code": "Érvénytelen ellenőrző kód", "you_have_one_team": "Egy csapatod van", "consider_consolidating_one_team_org": "Fontold meg egy szervezet létrehozását a számlázás, az adminisztrációs eszközök és az elemzések egyesítéséhez a csapatodban.", "consider_consolidating_multi_team_org": "Fontold meg egy szervezet létrehozását a számlázás, az adminisztrációs eszközök és az elemzések egyesítéséhez a csapataidban.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "A tervezett kezdési idő előtt", "cancel_booking_acknowledge_no_show_fee": "Tudomásul veszem, hogy a foglalás lemondásával a kezdési időpont előtti {{timeValue}} {{timeUnit}}on belül felszámításra kerül a meg nem jelenési díj: {{amount, currency}}", "contact_organizer": "Ha bármilyen kérdése van, kérjük, vegye fel a kapcsolatot a szervezővel.", - "booking_time_option": "Foglalási idő", - "booking_time_option_description": "Amikor a foglalás ütemezve van (kezdettől a végéig)", - "created_at_option": "Létrehozva", - "created_at_option_description": "Amikor a foglalás eredetileg létrejött", - "call_details": "Hívás részletei", - "call_id": "Hívás azonosító", - "call_information": "Hívás információ", - "sentiment": "Hangulat", - "disconnect_reason": "Megszakítás oka", - "call_summary": "Hívás összefoglaló", - "transcription": "Átírás", - "event_details": "Esemény részletei", - "agent": "Ügynök", - "no_transcript_available": "Nincs elérhető átirat", - "testing_sms_workflow_info_message": "Ezen munkafolyamat tesztelésekor vegye figyelembe, hogy az SMS-eket legalább 15 perccel előre kell ütemezni", - "start_from_scratch_title": "Kezdés az alapoktól", - "start_from_scratch_description": "Hozza létre saját munkafolyamatát az alapoktól.", - "cal_ai_template_title": "Cal.ai sablon", - "cal_ai_template_description": "AI ügynökök, amelyek találkozókat foglalnak, emlékeztetőket küldenek és nyomon követik az eseményeket!", - "voice": "Hang", - "select_voice": "Hang kiválasztása", - "select_voice_for_agent": "Válassz hangot az ügynöködnek", - "choose_a_voice_for_your_agent": "Válassz hangot az ügynöködnek", - "trait": "Jellemző", - "voice_id": "Hang azonosító", - "use_voice": "Hang használata", - "current_voice": "Jelenlegi hang", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index 72b732ae1b40db..9ea26167e21059 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Verifica il tuo indirizzo e-mail per garantire la consegna delle e-mail e delle notifiche del calendario", "verify_email_email_header": "Verifica il tuo indirizzo e-mail", "verify_email_button": "Verifica email", - "cal_ai_assistant": "Assistente", + "cal_ai_assistant": "Assistente IA di Cal", "send_cal_video_transcription_emails": "Invia email con trascrizione video Cal", "description_send_cal_video_transcription_emails": "Invia email con la trascrizione del video Cal dopo la fine della riunione. (Richiede un piano a pagamento)", "verify_email_change_description": "Hai recentemente chiesto di cambiare l'indirizzo email che utilizzi per accedere al tuo account {{appName}}. Fai clic sul pulsante qui sotto pe confermare il tuo nuovo indirizzo email.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Validazione del flusso di lavoro fallita", "workflow_validation_empty_fields": "Uno o più passaggi del flusso di lavoro hanno contenuti del messaggio vuoti", "workflow_validation_unverified_contacts": "Uno o più numeri di telefono o indirizzi email non sono verificati", - "supercharge_your_workflows_with_cal_ai": "Potenzia i tuoi flussi di lavoro con Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Agenti AI realistici che prenotano incontri, inviano promemoria e seguono i tuoi clienti.", "phone_number_imported_successfully": "Numero di telefono importato e collegato all'agente con successo", "phone_number_deleted_successfully": "Numero di telefono eliminato con successo", "delete_phone_number_confirmation": "Sei sicuro di voler eliminare questo numero di telefono? Questa azione non può essere annullata.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Abbonamento al numero di telefono annullato con successo", "updating": "Aggiornamento in corso", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Ciao, come stai?", "round_robin_description": "Ciclo di riunioni tra più membri del team.", "managed_event": "Evento gestito", "username_placeholder": "username", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Sei sicuro di voler eliminare questo passaggio del flusso di lavoro?", "do_you_still_want_to_unsubscribe": "Vuoi ancora disattivare l'abbonamento del numero di telefono da questo agente?", "the_action_will_disconnect_phone_number": "Questa azione disconnetterà il numero di telefono dall'agente. L'agente non sarà in grado di effettuare chiamate finché non verrà collegato un nuovo numero di telefono.", - "cal_ai_phone_numbers": "Numeri di telefono", + "cal_ai_phone_numbers": "Numeri di telefono Cal AI", "connect_phone_number": "Collega numero di telefono", - "cal_ai_phone_numbers_description": "Gestisci i tuoi numeri di telefono", + "cal_ai_phone_numbers_description": "Gestisci i tuoi numeri di telefono Cal AI", "import_number": "Importa numero", "this_action_will_also": "Questa azione inoltre:", "import_phone_number": "Importa numero di telefono", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Sì, annulla abbonamento", "cancel_phone_number_subscription_confirmation": "Sei sicuro di voler annullare questo abbonamento al numero di telefono? Questa azione non può essere annullata e perderai l'accesso a questo numero di telefono.", "add_members": "Aggiungi membri...", - "add_members_no_ellipsis": "Aggiungi membri", "no_assigned_members": "Nessun membro assegnato", "assigned_to": "Assegnato a", "you_must_be_logged_in_to": "Devi effettuare l'accesso a {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Categorie", "pricing": "Prezzi", "learn_more": "Ulteriori informazioni", - "try_now": "Prova ora", "privacy_policy": "Informativa sulla privacy", "terms_of_service": "Termini del servizio", "remove": "Rimuovi", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "invia un messaggio Whatsapp al partecipante", "workflows": "Flussi di lavoro", "new_workflow_btn": "Nuovo flusso di lavoro", - "how_would_you_like_to_start": "Come vorresti iniziare?", "add_new_workflow": "Aggiungi un nuovo flusso di lavoro", "reschedule_event_trigger": "quando un evento viene riprogrammato", "trigger": "Attivatore", @@ -1722,8 +1716,6 @@ "event_duration_info": "La durata dell'evento", "event_time_info": "Ora di inizio dell'evento", "event_type_not_found": "EventType non trovato", - "number_to_call_variable": "Numero da chiamare", - "number_to_call_info": "Il numero di telefono dell'utente che stai chiamando", "location_variable": "Luogo", "location_info": "Luogo dell'evento", "additional_notes_variable": "Note aggiuntive", @@ -1761,7 +1753,6 @@ "team_url": "URL del team", "team_members": "Membri del team", "more": "Altro", - "cal_ai_workflows": "Flussi di lavoro Cal.ai", "and_count_more": "e altri {{count}}", "more_page_footer": "Consideriamo l'applicazione mobile un'estensione dell'applicazione web. Se stai eseguendo delle azioni complesse, ti invitiamo a fare riferimento all'applicazione web.", "workflow_example_1": "Invia al partecipante un promemoria via SMS 24 ore prima dell'inizio dell'evento", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Invia al partecipante un promemoria via e-mail un'ora prima dell'inizio dell'evento", "workflow_example_5": "Invia all'organizzatore un'e-mail personalizzata se l'evento viene riprogrammato", "workflow_example_6": "Invia all'organizzatore un SMS personalizzato quando viene prenotato un nuovo evento", - "send_sms_reminder": "Invia promemoria SMS", - "send_sms_reminder_description": "24 ore prima dell'inizio dell'evento", - "follow_up_with_no_shows": "Segui chi non si presenta", - "follow_up_with_no_shows_description": "30 minuti dopo la fine dell'evento", - "remind_attendees_to_bring_id": "Ricorda ai partecipanti di portare un documento d'identità", - "remind_attendees_to_bring_id_description": "1 giorno prima dell'inizio dell'evento", - "email_to_remind_booking": "Promemoria via email", - "email_to_remind_booking_description": "1 ora prima dell'inizio dell'evento", - "custom_sms_reminder": "Promemoria SMS personalizzato", - "custom_sms_reminder_description": "Quando l'evento viene programmato", - "custom_email_reminder": "Promemoria email personalizzato", - "custom_email_reminder_description": "L'evento viene riprogrammato per l'organizzatore", "count_managed_to_limit": "Includi i conteggi delle prenotazioni dai tipi di eventi gestiti", "welcome_to_cal_header": "Benvenuto in {{appName}}!", "edit_form_later_subtitle": "Potrai modificarlo in seguito.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Mostra nella pagina di prenotazione", "visit_cancelled_booking": "Puoi visitare la pagina della prenotazione annullata", "get_started_zapier_templates": "Inizia con i modelli Zapier", - "standard_templates": "Template standard", - "cal_ai_templates": "Template Cal.ai", "team_is_unpublished": "{{team}} non è pubblicato", "org_is_unpublished_description": "Il link di questa organizzazione non è attualmente disponibile. Contatta il proprietario dell'organizzazione o chiedigli di pubblicarlo.", "team_is_unpublished_description": "Questo link di {{entity}} non è attualmente disponibile. Contatta il proprietario di {{entity}} o chiedigli di pubblicarlo.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Non è stato possibile addebitare la carta per il pagamento.", "insights": "Approfondimenti", "routing_forms": "Moduli di Routing", + "testing_workflow_info_message": "Durante il test di questo flusso di lavoro, tieni presente che le e-mail e i messaggi SMS devono essere programmati con almeno 1 ora di anticipo", "insights_no_data_found_for_filter": "Nessun dato trovato per il filtro selezionato o le date selezionate.", "acknowledge_booking_no_show_fee": "Confermo che in caso di mia mancata partecipazione a questo evento, una penale per mancata presentazione di {{amount, currency}} verrà addebitata sulla mia carta.", "days": "giorni", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Team: {{teamName}}", "insights_user_filter": "Utente: {{userName}}", "insights_subtitle": "Visualizza insight sulle prenotazioni per tutti i tuoi eventi", - "call_history": "Cronologia chiamate", - "call_history_subtitle": "Visualizza la cronologia delle chiamate effettuate con Cal.ai", "location_options": "{{locationCount}} opzioni di luogo", - "channel_type": "Tipo di canale", - "end_reason": "Motivo di fine", - "session_status": "Stato della sessione", - "user_sentiment": "Sentiment dell'utente", - "time_header": "Ora", - "from_header": "Da", "custom_plan": "Piano personalizzato", "email_embed": "Incorporamento nell'e-mail", "add_times_to_your_email": "Seleziona alcuni orari disponibili e incorporali nella tua e-mail", @@ -2782,8 +2752,6 @@ "account_already_linked": "L'account è già collegato", "send_email": "Invia e-mail", "cal_ai_phone_call_action": "Chiama il partecipante usando l'agente vocale Cal.ai", - "call_to_confirm_booking": "Chiama per confermare la prenotazione", - "cal_ai_phone_call_action_description": "2 ore prima dell'inizio dell'evento", "cal_ai_agent_configuration": "Configurazione agente Cal.ai", "choose_at_least_one_event_type_test_call": "Seleziona almeno un tipo di evento per effettuare una chiamata di prova.", "mark_as_no_show": "Contrassegna come Mancata presentazione", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verifica la modifica dell'email", "buy_credits": "Acquista crediti", "credits": "Crediti", - "credits_used": "Crediti utilizzati", - "total_credits_remaining": "Totale rimanente", - "credits_per_tip_org": "Ricevi 1000 crediti al mese per ogni membro del team", - "credits_per_tip_teams": "Ricevi 750 crediti al mese per ogni membro del team", - "view_and_manage_credits": "Visualizza e gestisci i crediti per l'invio di messaggi SMS", + "view_and_manage_credits": "Visualizza e gestisci crediti", "view_and_manage_credits_description": "Visualizza e gestisci i crediti per l'invio di messaggi SMS. Un credito vale 1¢ (USD). <0>Scopri di più", - "credit_worth_description": "Un credito vale 1¢ (USD). <0>Scopri di più", "buy_additional_credits": "Acquista crediti aggiuntivi ($0.01 per credito)", - "view_additional_credits_expense_tip": "Puoi visualizzare la spesa di crediti aggiuntivi nel registro delle spese", "overview": "Panoramica", "organization_slug_taken": "Lo slug dell'organizzazione è già stato preso", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Non puoi creare un'organizzazione poiché fai già parte di un'organizzazione", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Il tuo team Cal.com {{teamName}} ha esaurito i crediti. Di conseguenza, i messaggi SMS vengono ora inviati tramite email. Per riprendere l'invio di SMS, acquista crediti aggiuntivi.", "credit_limit_reached_message_user": "Il tuo account Cal.com ha esaurito i crediti. Di conseguenza, i messaggi SMS vengono ora inviati tramite email. Per riprendere l'invio di SMS, acquista crediti aggiuntivi.", "current_credit_balance": "Saldo attuale: {{balance}} crediti", - "current_balance": "Saldo attuale:", "notification_about_your_booking": "Notifica sulla tua prenotazione", "monthly_credits": "Crediti mensili", "total_credits": "Crediti totali: {{totalCredits}}", "remaining_credits": "Crediti rimanenti: {{remainingCredits}}", - "remaining": "Rimanente", - "total": "Totale", "additional_credits": "Crediti aggiuntivi", "routing_form_next_in_queue": "{{count}} successivi in coda", "routing_form_select_members_to_email": "Invia risposte email a", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Visualizza flussi di lavoro esistenti e le loro configurazioni", "pbac_desc_update_workflows": "Modifica e cambia le impostazioni dei flussi di lavoro", "pbac_desc_delete_workflows": "Rimuovi flussi di lavoro dal sistema", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Crea webhook", - "pbac_desc_view_webhooks": "Visualizza webhook", - "pbac_desc_update_webhooks": "Aggiorna webhook", - "pbac_desc_delete_webhooks": "Elimina webhook", "pbac_desc_manage_workflows": "Accesso completo alla gestione di tutti i flussi di lavoro", "pbac_desc_create_event_types": "Crea tipi di evento", "pbac_desc_view_event_types": "Visualizza tipi di evento", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Il nome dell'evento trigger (es. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "L'ora del webhook", "webhook_type": "Lo slug del tipo di evento", - "set_up_agent": "Configura Agente", "webhook_title": "Il nome del tipo di evento", "webhook_start_time": "L'ora di inizio dell'evento", "webhook_end_time": "L'ora di conclusione dell'evento", @@ -3672,9 +3625,6 @@ "visit": "Visita", "location_custom_label_input_label": "Etichetta personalizzata sulla pagina di prenotazione", "meeting_link": "Link della riunione", - "session_outcome": "Esito della sessione", - "call_created": "Chiamata creata", - "voicemail": "Segreteria telefonica", "my_bookings": "Le mie prenotazioni", "phone": "Telefono", "free": "Gratuito", @@ -3682,8 +3632,6 @@ "user_name": "Nome utente", "expand_panel": "Espandi pannello", "collapse_panel": "Comprimi pannello", - "email_verification_required": "La verifica dell'email è richiesta per questo tipo di evento", - "invalid_verification_code": "Codice di verifica fornito non valido", "you_have_one_team": "Hai un team", "consider_consolidating_one_team_org": "Considera la creazione di un'organizzazione per unificare fatturazione, strumenti di amministrazione e analisi per il tuo team.", "consider_consolidating_multi_team_org": "Considera la creazione di un'organizzazione per unificare fatturazione, strumenti di amministrazione e analisi per i tuoi team.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Prima dell'orario di inizio programmato", "cancel_booking_acknowledge_no_show_fee": "Riconosco che cancellando la prenotazione entro {{timeValue}} {{timeUnit}} dall'orario di inizio mi verrà addebitata la penale di mancata presenza di {{amount, currency}}", "contact_organizer": "Per qualsiasi domanda, contatta l'organizzatore.", - "booking_time_option": "Orario di prenotazione", - "booking_time_option_description": "Quando la prenotazione è programmata (inizio e fine)", - "created_at_option": "Creato il", - "created_at_option_description": "Quando la prenotazione è stata originariamente creata", - "call_details": "Dettagli chiamata", - "call_id": "ID chiamata", - "call_information": "Informazioni chiamata", - "sentiment": "Sentimento", - "disconnect_reason": "Motivo disconnessione", - "call_summary": "Riepilogo chiamata", - "transcription": "Trascrizione", - "event_details": "Dettagli evento", - "agent": "Agente", - "no_transcript_available": "Nessuna trascrizione disponibile", - "testing_sms_workflow_info_message": "Quando testi questo flusso di lavoro, tieni presente che gli SMS devono essere programmati con almeno 15 minuti di anticipo", - "start_from_scratch_title": "Inizia da zero", - "start_from_scratch_description": "Crea il tuo flusso di lavoro da zero.", - "cal_ai_template_title": "Template Cal.ai", - "cal_ai_template_description": "Agenti AI che prenotano riunioni, inviano promemoria e fanno follow-up!", - "voice": "Voce", - "select_voice": "Seleziona voce", - "select_voice_for_agent": "Seleziona una voce per il tuo agente", - "choose_a_voice_for_your_agent": "Scegli una voce per il tuo agente", - "trait": "Caratteristica", - "voice_id": "ID voce", - "use_voice": "Usa voce", - "current_voice": "Voce attuale", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index 3edadaafa41c0a..806af7514310d4 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "メール配信とカレンダー通知が確実に機能するようにするには、メールアドレスをご確認ください。", "verify_email_email_header": "メールアドレスをご確認ください", "verify_email_button": "メールを確認", - "cal_ai_assistant": "アシスタント", + "cal_ai_assistant": "Cal AIアシスタント", "send_cal_video_transcription_emails": "Cal Video文字起こしメールを送信", "description_send_cal_video_transcription_emails": "ミーティング終了後、Cal Videoの文字起こしをメールで送信します。(有料プランが必要です)", "verify_email_change_description": "最近、{{appName}}アカウントにログインするためのメールアドレスの変更をリクエストされました。新しいメールアドレスを確認するには、以下のボタンをクリックしてください。", @@ -819,8 +819,6 @@ "workflow_validation_failed": "ワークフローの検証に失敗しました", "workflow_validation_empty_fields": "1つ以上のワークフローステップにメッセージ内容が空のものがあります", "workflow_validation_unverified_contacts": "1つ以上の電話番号またはメールアドレスが確認されていません", - "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": "この電話番号を削除してもよろしいですか?この操作は元に戻せません。", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "電話番号のサブスクリプションが正常にキャンセルされました", "updating": "更新中", "round_robin": "ラウンドロビン", - "hi_how_are_you_doing": "こんにちは、お元気ですか?", "round_robin_description": "複数のチームメンバー間のミーティングを定期化する。", "managed_event": "管理済みのイベント", "username_placeholder": "ユーザー名", @@ -879,9 +876,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_phone_numbers": "Cal AI電話番号", "connect_phone_number": "電話番号を接続する", - "cal_ai_phone_numbers_description": "電話番号を管理する", + "cal_ai_phone_numbers_description": "Cal AI電話番号を管理する", "import_number": "番号をインポート", "this_action_will_also": "この操作は以下も実行します:", "import_phone_number": "電話番号をインポート", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "電話番号のサブスクリプションをキャンセルする", "delete_associated_phone_number": "関連付けられた電話番号を削除する", "unauthorized_create_workflow": "このワークフローを作成する権限がありません", - "import_phone_number_description": "電話機能で使用するTwilio電話番号をインポートする", + "import_phone_number_description": "Phoneで使用するためにTwilioの電話番号をインポートする", "phone_number_cost": "月額${{price}}", "buy_new_number": "新しい番号を購入", "buy_number_cost_x_per_month": "電話番号の購入は月額${{priceInDollars}}かかります。アクティブな電話番号ごとに毎月課金されます。", @@ -900,7 +897,6 @@ "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}}にログインする必要があります", @@ -1211,7 +1207,6 @@ "categories": "カテゴリー", "pricing": "料金設定", "learn_more": "詳細情報", - "try_now": "今すぐ試す", "privacy_policy": "プライバシーポリシー", "terms_of_service": "利用規約", "remove": "削除する", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "出席者に Whatsapp メッセージを送信", "workflows": "ワークフロー", "new_workflow_btn": "新しいワークフロー", - "how_would_you_like_to_start": "どのように始めますか?", "add_new_workflow": "新しいワークフローを追加", "reschedule_event_trigger": "イベントのスケジュール変更時に", "trigger": "トリガー", @@ -1722,8 +1716,6 @@ "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": "備考", @@ -1761,7 +1753,6 @@ "team_url": "チーム URL", "team_members": "チームメンバー", "more": "詳細情報", - "cal_ai_workflows": "Cal.aiワークフロー", "and_count_more": "他{{count}}件", "more_page_footer": "私たちは、モバイルアプリケーションを Web アプリケーションの延長として捉えています。複雑な操作を行う場合には、Web アプリケーションを参照してください。", "workflow_example_1": "イベントの開始 24 時間前に出席者に SMS で通知を送信する", @@ -1770,18 +1761,6 @@ "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": "参加者に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": "これは後で編集できます。", @@ -2205,8 +2184,6 @@ "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}} の所有者に問い合わせるか、リンクを公開するように依頼してください。", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "支払いのためにカードを請求できませんでした。", "insights": "インサイト", "routing_forms": "ルーティングフォーム", + "testing_workflow_info_message": "このワークフローをテストする場合、少なくとも 1 時間前でなければメールと SMS のスケジュール設定ができないことにご注意ください", "insights_no_data_found_for_filter": "選択したフィルターまたは日付のデータが見つかりませんでした。", "acknowledge_booking_no_show_fee": "このイベントに出席しなかった場合、不参加費用として {{amount, currency}} が私のカードに請求されることに同意します。", "days": "日", @@ -2486,15 +2464,7 @@ "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": "出席できる時間帯をいくつか選んで、メールに埋め込みます", @@ -2782,8 +2752,6 @@ "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": "テスト通話を行うには、少なくとも1つのイベントタイプを選択してください。", "mark_as_no_show": "ノーショーとしてマーク", @@ -3235,15 +3203,9 @@ "verify_email_change": "メールアドレスの変更を確認", "buy_credits": "クレジットを購入", "credits": "クレジット", - "credits_used": "使用済みクレジット", - "total_credits_remaining": "残りの合計", - "credits_per_tip_org": "チームメンバー1人あたり月1000クレジットを受け取ります", - "credits_per_tip_teams": "チームメンバー1人あたり月750クレジットを受け取ります", - "view_and_manage_credits": "SMSメッセージ送信用のクレジットを表示・管理", + "view_and_manage_credits": "クレジットの表示と管理", "view_and_manage_credits_description": "SMSメッセージ送信用のクレジットを表示・管理します。1クレジットは1¢(USD)に相当します。<0>詳細を見る", - "credit_worth_description": "1クレジットは1¢(USD)の価値があります。<0>詳細を見る", "buy_additional_credits": "追加クレジットを購入(1クレジットあたり$0.01)", - "view_additional_credits_expense_tip": "追加クレジットの支出は経費ログで確認できます", "overview": "概要", "organization_slug_taken": "組織のスラッグはすでに使用されています", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "すでに組織に所属しているため、新しい組織を作成することはできません", @@ -3424,13 +3386,10 @@ "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": "メール返信の送信先", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "既存のワークフローとその設定を表示する", "pbac_desc_update_workflows": "ワークフロー設定を編集および変更する", "pbac_desc_delete_workflows": "システムからワークフローを削除する", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Webhookを作成", - "pbac_desc_view_webhooks": "Webhookを表示", - "pbac_desc_update_webhooks": "ウェブフックを更新する", - "pbac_desc_delete_webhooks": "ウェブフックを削除する", "pbac_desc_manage_workflows": "すべてのワークフローへの完全な管理アクセス", "pbac_desc_create_event_types": "イベントタイプを作成する", "pbac_desc_view_event_types": "イベントタイプを表示する", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "トリガーイベントの名前(例:BOOKING_CREATED、BOOKING_CANCELLED)", "webhook_created_at": "ウェブフックの時間", "webhook_type": "イベントタイプのスラッグ", - "set_up_agent": "エージェントを設定する", "webhook_title": "イベントタイプの名前", "webhook_start_time": "イベントの開始時間", "webhook_end_time": "イベントの終了時間", @@ -3672,9 +3625,6 @@ "visit": "訪問", "location_custom_label_input_label": "予約ページのカスタムラベル", "meeting_link": "ミーティングリンク", - "session_outcome": "セッション結果", - "call_created": "通話作成済み", - "voicemail": "ボイスメール", "my_bookings": "自分の予約", "phone": "電話番号", "free": "無料", @@ -3682,8 +3632,6 @@ "user_name": "ユーザー名", "expand_panel": "パネルを展開", "collapse_panel": "パネルを折りたたむ", - "email_verification_required": "このイベントタイプにはメール認証が必要です", - "invalid_verification_code": "無効な認証コードが提供されました", "you_have_one_team": "あなたは1つのチームを持っています", "consider_consolidating_one_team_org": "組織を設定して、チーム全体の請求、管理ツール、分析を統合することを検討してください。", "consider_consolidating_multi_team_org": "組織を設定して、チーム全体の請求、管理ツール、分析を統合することを検討してください。", @@ -3704,32 +3652,5 @@ "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/km/common.json b/apps/web/public/static/locales/km/common.json index d0bd5c693bf0e1..f78923589b5a01 100644 --- a/apps/web/public/static/locales/km/common.json +++ b/apps/web/public/static/locales/km/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "ផ្ទៀងផ្ទាត់អាសយដ្ឋានអ៊ីមែលរបស់អ្នក ដើម្បីធានានូវលទ្ធភាពចែកចាយអ៊ីមែល និងប្រតិទិនដ៏ល្អបំផុត", "verify_email_email_header": "ផ្ទៀងផ្ទាត់អាសយដ្ឋានអ៊ីមែលរបស់អ្នក។", "verify_email_button": "ផ្ទៀងផ្ទាត់អ៊ីមែល", - "cal_ai_assistant": "ជំនួយការ", + "cal_ai_assistant": "Assistant", "send_cal_video_transcription_emails": "ផ្ញើអ៊ីមែលចម្លងវីដេអូ Cal", "description_send_cal_video_transcription_emails": "ផ្ញើអ៊ីមែលជាមួយនឹងការចម្លងវីដេអូ Cal បន្ទាប់ពីការប្រជុំបានបញ្ចប់។ (តម្រូវឱ្យមានគម្រោងបង់ប្រាក់)", "verify_email_change_description": "អ្នកបានស្នើសុំផ្លាស់ប្តូរអាសយដ្ឋានអ៊ីមែលដែលអ្នកប្រើដើម្បីចូលទៅក្នុងគណនី {{appName}} របស់អ្នក។ សូមចុចប៊ូតុងខាងក្រោមដើម្បីបញ្ជាក់អាសយដ្ឋានអ៊ីមែលថ្មីរបស់អ្នក។", @@ -819,8 +819,6 @@ "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": "តើអ្នកប្រាកដថាចង់លុបលេខទូរស័ព្ទនេះមែនទេ? សកម្មភាពនេះមិនអាចត្រឡប់ក្រោយវិញបានទេ។", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "ការជាវលេខទូរស័ព្ទត្រូវបានលុបចោលដោយជោគជ័យ", "updating": "កំពុងធ្វើបច្ចុប្បន្នភាព", "round_robin": "Round Robin", - "hi_how_are_you_doing": "សួស្តី តើអ្នកសុខសប្បាយទេ?", "round_robin_description": "វេនប្រជុំរវាងសមាជិកក្រុមច្រើននាក់។", "managed_event": "ព្រឹត្តិការណ៍ដែលគ្រប់គ្រង", "username_placeholder": "ឈ្មោះអ្នកប្រើ", @@ -879,9 +876,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_phone_numbers": "លេខទូរស័ព្ទ Cal AI", "connect_phone_number": "ភ្ជាប់លេខទូរស័ព្ទ", - "cal_ai_phone_numbers_description": "គ្រប់គ្រងលេខទូរស័ព្ទរបស់អ្នក", + "cal_ai_phone_numbers_description": "គ្រប់គ្រងលេខទូរស័ព្ទ របស់អ្នក", "import_number": "នាំចូលលេខ", "this_action_will_also": "សកម្មភាពនេះក៏នឹង៖", "import_phone_number": "នាំចូលលេខទូរស័ព្ទ", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "បោះបង់ការជាវលេខទូរស័ព្ទរបស់អ្នក", "delete_associated_phone_number": "លុបលេខទូរស័ព្ទដែលពាក់ព័ន្ធ", "unauthorized_create_workflow": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យបង្កើតលំហូរការងារនេះទេ", - "import_phone_number_description": "នាំចូលលេខទូរស័ព្ទ Twilio របស់អ្នកដើម្បីប្រើជាមួយទូរស័ព្ទ", + "import_phone_number_description": "នាំចូលលេខទូរស័ព្ទ Twilio របស់អ្នកដើម្បីប្រើជាមួយ Phone", "phone_number_cost": "${{price}}/ខែ", "buy_new_number": "ទិញលេខថ្មី", "buy_number_cost_x_per_month": "ការទិញលេខទូរស័ព្ទមានតម្លៃ ${{priceInDollars}} ក្នុងមួយខែ។ អ្នកនឹងត្រូវបានគិតថ្លៃប្រចាំខែសម្រាប់លេខទូរស័ព្ទសកម្មនីមួយៗ។", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "ប្រភេទ", "pricing": "តម្លៃ", "learn_more": "ស្វែងយល់បន្ថែម", - "try_now": "សាកល្បងឥឡូវនេះ", "privacy_policy": "គោលការណ៍ភាពឯកជន", "terms_of_service": "លក្ខខណ្ឌសេវាកម្ម", "remove": "យកចេញ", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "ផ្ញើសារតាម WhatsApp ទៅអ្នកចូលរួម", "workflows": "លំហូរការងារ", "new_workflow_btn": "លំហូរការងារថ្មី", - "how_would_you_like_to_start": "តើអ្នកចង់ចាប់ផ្តើមដោយរបៀបណា?", "add_new_workflow": "បន្ថែមលំហូរការងារថ្មី", "reschedule_event_trigger": "ពេលព្រឹត្តិការណ៍ត្រូវបានកំណត់ពេលថ្មី", "trigger": "ការបញ្ចុះបញ្ចូល", @@ -1722,8 +1716,6 @@ "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": "កំណត់ចំណាំបន្ថែម", @@ -1761,7 +1753,6 @@ "team_url": "URL ក្រុម", "team_members": "សមាជិកក្រុម", "more": "ច្រើនទៀត", - "cal_ai_workflows": "លំហូរការងារ Cal.ai", "and_count_more": "និង {{count}} ទៀត", "more_page_footer": "យើងមើលឃើញកម្មវិធីចល័តជាការបន្ថែមនៃកម្មវិធីគេហទំព័រ។ ប្រសិនបើអ្នកកំពុងធ្វើសកម្មភាពស្មុគស្មាញណាមួយ សូមយោងទៅវិញទៅមកនៃកម្មវិធីគេហទំព័រ។", "workflow_example_1": "ផ្ញើការរំលឹកតាមសារ SMS 24 ម៉ោងមុនព្រឹត្តិការណ៍ចាប់ផ្តើមទៅអ្នកចូលរួម", @@ -1770,18 +1761,6 @@ "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": "អ្នកនឹងអាចកែសម្រួលវានៅពេលក្រោយ។", @@ -2205,8 +2184,6 @@ "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": "តំណភ្ជាប់ក្រុមនេះមិនមានសម្រាប់ឥឡូវទេ។ សូមទាក់ទងម្ចាស់ក្រុមឬស្នើសុំឱ្យពួកគេបោះពុម្ពផ្សាយវា។", @@ -2364,6 +2341,7 @@ "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": "ថ្ងៃ", @@ -2486,15 +2464,7 @@ "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": "ជ្រើសរើសពេលវេលាដែលអាចប្រើបានប៉ុន្មាន និងបញ្ចូលវានៅក្នុងអ៊ីមែលរបស់អ្នក", @@ -2782,8 +2752,6 @@ "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": "សម្គាល់ថាមិនបានមក", @@ -3235,15 +3203,9 @@ "verify_email_change": "ផ្ទៀងផ្ទាត់ការផ្លាស់ប្តូរអ៊ីមែល", "buy_credits": "ទិញក្រេឌីត", "credits": "ក្រេឌីត", - "credits_used": "ក្រេឌីតដែលបានប្រើ", - "total_credits_remaining": "សរុបនៅសល់", - "credits_per_tip_org": "អ្នកទទួលបាន ១០០០ ក្រេឌីតក្នុងមួយខែ ក្នុងសមាជិកក្រុមមួយនាក់", - "credits_per_tip_teams": "អ្នកទទួលបាន ៧៥០ ក្រេឌីតក្នុងមួយខែ ក្នុងសមាជិកក្រុមមួយនាក់", - "view_and_manage_credits": "មើលនិងគ្រប់គ្រងក្រេឌីតសម្រាប់ការផ្ញើសារ SMS", + "view_and_manage_credits": "មើល និងគ្រប់គ្រងក្រេឌីត", "view_and_manage_credits_description": "មើល និងគ្រប់គ្រងក្រេឌីតសម្រាប់ការផ្ញើសារ SMS។ ក្រេឌីតមួយមានតម្លៃ 1¢ (USD)។ <0>ស្វែងយល់បន្ថែម", - "credit_worth_description": "ក្រេឌីតមួយមានតម្លៃ ១¢ (USD)។ <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": "អ្នកមិនអាចបង្កើតអង្គការបានទេ ដោយសារអ្នកជាផ្នែកមួយនៃអង្គការរួចហើយ", @@ -3424,13 +3386,10 @@ "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": "ផ្ញើការឆ្លើយតបតាមអ៊ីមែលទៅកាន់", @@ -3508,11 +3467,6 @@ "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": "មើលប្រភេទព្រឹត្តិការណ៍", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "ឈ្មោះនៃព្រឹត្តិការណ៍ជំរុញ (ឧ. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "ពេលវេលានៃ webhook", "webhook_type": "ឈ្មោះសម្គាល់ប្រភេទព្រឹត្តិការណ៍", - "set_up_agent": "រៀបចំភ្នាក់ងារ", "webhook_title": "ឈ្មោះប្រភេទព្រឹត្តិការណ៍", "webhook_start_time": "ពេលវេលាចាប់ផ្តើមនៃព្រឹត្តិការណ៍", "webhook_end_time": "ពេលវេលាបញ្ចប់នៃព្រឹត្តិការណ៍", @@ -3672,9 +3625,6 @@ "visit": "ទស្សនា", "location_custom_label_input_label": "ស្លាកផ្ទាល់ខ្លួននៅលើទំព័រកក់", "meeting_link": "តំណភ្ជាប់ការប្រជុំ", - "session_outcome": "លទ្ធផលនៃវគ្គ", - "call_created": "ការហៅបានបង្កើត", - "voicemail": "សារសំឡេង", "my_bookings": "ការកក់របស់ខ្ញុំ", "phone": "ទូរស័ព្ទ", "free": "ឥតគិតថ្លៃ", @@ -3682,8 +3632,6 @@ "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": "សូមពិចារណាបង្កើតអង្គភាពមួយដើម្បីបង្រួបបង្រួមការចេញវិក្កយបត្រ ឧបករណ៍គ្រប់គ្រង និងការវិភាគនៅក្នុងក្រុមរបស់អ្នក។", @@ -3704,32 +3652,5 @@ "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 ដែលកក់ការប្រជុំ ផ្ញើការរំលឹក និងតាមដានបន្ត!", - "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index cda7c69b5d7d22..ea3cc833b5c112 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -819,8 +819,6 @@ "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": "이 전화번호를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "전화번호 구독 취소 완료", "updating": "업데이트 중", "round_robin": "라운드 로빈", - "hi_how_are_you_doing": "안녕하세요, 어떻게 지내세요?", "round_robin_description": "여러 팀 구성원 간의 주기적인 회의", "managed_event": "관리형 이벤트", "username_placeholder": "username", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "전화번호 구독 취소하기", "delete_associated_phone_number": "연결된 전화번호 삭제하기", "unauthorized_create_workflow": "이 워크플로를 생성할 권한이 없습니다", - "import_phone_number_description": "Phone과 함께 사용할 Twilio 전화번호 가져오기", + "import_phone_number_description": "Phone에서 사용할 Twilio 전화번호 가져오기", "phone_number_cost": "월 ${{price}}", "buy_new_number": "새 번호 구매하기", "buy_number_cost_x_per_month": "전화번호 구매 비용은 월 ${{priceInDollars}}입니다. 활성화된 각 전화번호에 대해 매월 요금이 청구됩니다.", @@ -900,7 +897,6 @@ "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}}에 로그인해야 합니다.", @@ -1211,7 +1207,6 @@ "categories": "카테고리", "pricing": "가격", "learn_more": "자세히 알아보기", - "try_now": "지금 시도하기", "privacy_policy": "개인정보 보호정책", "terms_of_service": "서비스 약관", "remove": "제거", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "참석자에게 Whatsapp 발송", "workflows": "워크플로", "new_workflow_btn": "새 워크플로", - "how_would_you_like_to_start": "어떻게 시작하시겠습니까?", "add_new_workflow": "새 워크플로 추가", "reschedule_event_trigger": "이벤트 일정 변경 시", "trigger": "트리거", @@ -1722,8 +1716,6 @@ "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": "추가 참고 사항", @@ -1761,7 +1753,6 @@ "team_url": "팀 URL", "team_members": "팀원", "more": "더 보기", - "cal_ai_workflows": "Cal.ai 워크플로우", "and_count_more": "외 {{count}}개", "more_page_footer": "모바일 애플리케이션은 웹 애플리케이션의 확장으로 취급됩니다. 복잡한 작업을 수행하는 경우 웹 애플리케이션을 다시 참조하십시오.", "workflow_example_1": "이벤트가 시작되기 24시간 전에 참석자에게 SMS 알림 보내기", @@ -1770,18 +1761,6 @@ "workflow_example_4": "이벤트가 시작되기 1시간 전에 참석자에게 이메일 알림 보내기", "workflow_example_5": "이벤트 일정이 변경되면 호스트에게 사용자 정의 SMS 보내기", "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": "나중에 편집할 수 있습니다.", @@ -2205,8 +2184,6 @@ "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}} 소유자에게 연락하거나 게시하라고 요청하십시오.", @@ -2364,6 +2341,7 @@ "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": "일", @@ -2486,15 +2464,7 @@ "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": "사용 가능한 시간을 몇 개 선택하여 이메일에 포함시키세요", @@ -2782,8 +2752,6 @@ "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": "노쇼로 표시", @@ -3235,15 +3203,9 @@ "verify_email_change": "이메일 변경 확인", "buy_credits": "크레딧 구매", "credits": "크레딧", - "credits_used": "사용된 크레딧", - "total_credits_remaining": "총 남은 크레딧", - "credits_per_tip_org": "팀원 1명당 매월 1000 크레딧을 받습니다", - "credits_per_tip_teams": "팀원 1명당 매월 750 크레딧을 받습니다", - "view_and_manage_credits": "SMS 메시지 발송을 위한 크레딧 보기 및 관리", + "view_and_manage_credits": "크레딧 보기 및 관리", "view_and_manage_credits_description": "SMS 메시지 전송을 위한 크레딧을 보고 관리하세요. 1크레딧은 1¢(USD)의 가치가 있습니다. <0>자세히 알아보기", - "credit_worth_description": "1 크레딧은 1¢(USD)의 가치가 있습니다. <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": "이미 조직의 일원이므로 새로운 조직을 만들 수 없습니다", @@ -3424,13 +3386,10 @@ "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": "이메일 응답을 보낼 대상 선택", @@ -3508,11 +3467,6 @@ "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": "이벤트 타입 보기", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "트리거 이벤트 이름(예: BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "웹훅 생성 시간", "webhook_type": "이벤트 유형 슬러그", - "set_up_agent": "에이전트 설정", "webhook_title": "이벤트 유형 이름", "webhook_start_time": "이벤트 시작 시간", "webhook_end_time": "이벤트 종료 시간", @@ -3672,9 +3625,6 @@ "visit": "방문", "location_custom_label_input_label": "예약 페이지의 커스텀 라벨", "meeting_link": "회의 링크", - "session_outcome": "세션 결과", - "call_created": "통화 생성됨", - "voicemail": "음성 메시지", "my_bookings": "내 예약", "phone": "전화번호", "free": "무료", @@ -3682,8 +3632,6 @@ "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": "여러 팀 전체의 결제, 관리 도구 및 분석을 통합하기 위해 조직 설정을 고려해 보세요.", @@ -3704,32 +3652,5 @@ "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index 9dcb7a963ff5ea..49ce3c919ea885 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -819,8 +819,6 @@ "workflow_validation_failed": "Workflow-validatie mislukt", "workflow_validation_empty_fields": "Een of meer workflow-stappen hebben lege berichtinhoud", "workflow_validation_unverified_contacts": "Een of meer telefoonnummers of e-mailadressen zijn niet geverifieerd", - "supercharge_your_workflows_with_cal_ai": "Versterk je workflows met Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Levensechte AI-agenten die afspraken boeken, herinneringen sturen en opvolgen met je klanten.", "phone_number_imported_successfully": "Telefoonnummer succesvol geïmporteerd en gekoppeld aan agent", "phone_number_deleted_successfully": "Telefoonnummer succesvol verwijderd", "delete_phone_number_confirmation": "Weet je zeker dat je dit telefoonnummer wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Telefoonnummer-abonnement succesvol geannuleerd", "updating": "Bijwerken", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Hoi, hoe gaat het met je?", "round_robin_description": "Afspraken wisselen tussen meerdere teamleden.", "managed_event": "Beheerde gebeurtenis", "username_placeholder": "gebruikersnaam", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Weet u zeker dat u deze werkstroomstap wilt verwijderen?", "do_you_still_want_to_unsubscribe": "Wilt u het telefoonnummer nog steeds afmelden bij deze agent?", "the_action_will_disconnect_phone_number": "Deze actie zal het telefoonnummer loskoppelen van de agent. De agent kan geen oproepen meer maken totdat er een nieuw telefoonnummer is verbonden.", - "cal_ai_phone_numbers": "Telefoonnummers", + "cal_ai_phone_numbers": "telefoonnummers", "connect_phone_number": "Telefoonnummer verbinden", - "cal_ai_phone_numbers_description": "Beheer je telefoonnummers", + "cal_ai_phone_numbers_description": "Beheer uw telefoonnummers", "import_number": "Nummer importeren", "this_action_will_also": "Deze actie zal ook:", "import_phone_number": "Telefoonnummer importeren", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Uw telefoonnummerabonnement opzeggen", "delete_associated_phone_number": "Het bijbehorende telefoonnummer verwijderen", "unauthorized_create_workflow": "U bent niet gemachtigd om deze werkstroom aan te maken", - "import_phone_number_description": "Importeer je Twilio-telefoonnummer om te gebruiken met Telefoon", + "import_phone_number_description": "Importeer uw Twilio telefoonnummer om te gebruiken met Phone", "phone_number_cost": "${{price}}/maand", "buy_new_number": "Nieuw nummer kopen", "buy_number_cost_x_per_month": "Een telefoonnummer kopen kost ${{priceInDollars}} per maand. U wordt maandelijks gefactureerd voor elk actief telefoonnummer.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Ja, abonnement opzeggen", "cancel_phone_number_subscription_confirmation": "Weet u zeker dat u dit telefoonnummer-abonnement wilt opzeggen? Deze actie kan niet ongedaan worden gemaakt en u verliest toegang tot dit telefoonnummer.", "add_members": "Leden toevoegen...", - "add_members_no_ellipsis": "Leden toevoegen", "no_assigned_members": "Geen toegewezen leden", "assigned_to": "Toegewezen aan", "you_must_be_logged_in_to": "Je moet ingelogd zijn op {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Categorieën", "pricing": "Prijzen", "learn_more": "Meer informatie", - "try_now": "Nu proberen", "privacy_policy": "Privacybeleid", "terms_of_service": "Gebruiksvoorwaarden", "remove": "Verwijderen", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "WhatsApp-bericht naar deelnemer versturen", "workflows": "Werkstromen", "new_workflow_btn": "Nieuwe werkstroom", - "how_would_you_like_to_start": "Hoe wil je beginnen?", "add_new_workflow": "Een nieuwe werkstroom toevoegen", "reschedule_event_trigger": "wanneer de gebeurtenis verplaatst wordt", "trigger": "Activatie", @@ -1722,8 +1716,6 @@ "event_duration_info": "De duur van het evenement", "event_time_info": "De begintijd van de gebeurtenis", "event_type_not_found": "EvenementType niet gevonden", - "number_to_call_variable": "Te bellen nummer", - "number_to_call_info": "Het telefoonnummer van de gebruiker die je belt", "location_variable": "Locatie", "location_info": "De locatie van de gebeurtenis", "additional_notes_variable": "Aanvullende notities", @@ -1761,7 +1753,6 @@ "team_url": "Team-URL", "team_members": "Teamleden", "more": "Meer", - "cal_ai_workflows": "Cal.ai Workflows", "and_count_more": "en nog {{count}}", "more_page_footer": "We zien de mobiele app als een verlengstuk van de webapp. Voor ingewikkelde acties raden we u aan de webapp te gebruiken.", "workflow_example_1": "24 uur voor het begin van de gebeurtenis een sms-herinnering naar deelnemer versturen", @@ -1770,18 +1761,6 @@ "workflow_example_4": "1 uur voor het begin van gebeurtenissen een e-mailherinnering naar deelnemer versturen", "workflow_example_5": "Aangepaste e-mail versturen naar organisator wanneer gebeurtenis wordt verplaatst", "workflow_example_6": "Aangepaste sms versturen naar organisator wanneer een nieuwe gebeurtenis wordt geboekt", - "send_sms_reminder": "SMS-herinnering versturen", - "send_sms_reminder_description": "24 uur voor aanvang van de afspraak", - "follow_up_with_no_shows": "Opvolgen bij no-shows", - "follow_up_with_no_shows_description": "30 minuten na afloop van de afspraak", - "remind_attendees_to_bring_id": "Herinner deelnemers om ID mee te nemen", - "remind_attendees_to_bring_id_description": "1 dag voor aanvang van de afspraak", - "email_to_remind_booking": "E-mailherinnering", - "email_to_remind_booking_description": "1 uur voor aanvang van de afspraak", - "custom_sms_reminder": "Aangepaste SMS-herinnering", - "custom_sms_reminder_description": "Wanneer afspraak is gepland", - "custom_email_reminder": "Aangepaste e-mailherinnering", - "custom_email_reminder_description": "Afspraak is verzet naar host", "count_managed_to_limit": "Inclusief boekingsaantallen van beheerde evenementtypen", "welcome_to_cal_header": "Welkom bij {{appName}}!", "edit_form_later_subtitle": "Je kunt dit later bewerken.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Weergeven op boekingspagina", "visit_cancelled_booking": "Je kunt de pagina van de geannuleerde boeking bezoeken", "get_started_zapier_templates": "Aan de slag met Zapier-sjablonen", - "standard_templates": "Standaard sjablonen", - "cal_ai_templates": "Cal.ai sjablonen", "team_is_unpublished": "{{team}} is niet gepubliceerd", "org_is_unpublished_description": "Deze organisatiekoppeling is momenteel niet beschikbaar. Neem contact op met de organisatie-eigenaar of vraag om het te publiceren.", "team_is_unpublished_description": "Deze {{entity}}-link is momenteel niet beschikbaar. Neem contact op met de {{entity}}-eigenaar of vraag hem het te publiceren.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Kon de kaart niet belasten voor betaling.", "insights": "Inzichten", "routing_forms": "Routeringsformulieren", + "testing_workflow_info_message": "Let er bij het testen van deze werkstroom op dat e-mails en sms minimaal 1 uur van tevoren kunnen gepland moeten worden", "insights_no_data_found_for_filter": "Geen gegevens gevonden voor het geselecteerde filter of de geselecteerde datums.", "acknowledge_booking_no_show_fee": "Ik erken dat als ik deze gebeurtenis niet bijwoon, er {{amount, currency}} aan afwezigheidsvergoeding van mijn kaart worden afgeschreven.", "days": "dagen", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Team: {{teamName}}", "insights_user_filter": "Gebruiker: {{userName}}", "insights_subtitle": "Bekijk boekingsinsights voor al uw gebeurtenissen", - "call_history": "Belgeschiedenis", - "call_history_subtitle": "Bekijk belgeschiedenis van al je Cal.ai gesprekken", "location_options": "{{locationCount}} locatieopties", - "channel_type": "Kanaaltype", - "end_reason": "Reden van beëindiging", - "session_status": "Sessiestatus", - "user_sentiment": "Gebruikerssentiment", - "time_header": "Tijd", - "from_header": "Van", "custom_plan": "Aangepast abonnement", "email_embed": "E-mail insluiten", "add_times_to_your_email": "Selecteer enkele beschikbare tijden en voeg ze toe aan uw e-mail", @@ -2782,8 +2752,6 @@ "account_already_linked": "Account is al gekoppeld", "send_email": "E-mail versturen", "cal_ai_phone_call_action": "Bel deelnemer met Cal.ai spraakassistent", - "call_to_confirm_booking": "Bel om boeking te bevestigen", - "cal_ai_phone_call_action_description": "2 uur voor aanvang van het evenement", "cal_ai_agent_configuration": "Cal.ai agent configuratie", "choose_at_least_one_event_type_test_call": "Kies ten minste één evenementtype om een testgesprek te maken.", "mark_as_no_show": "Markeren als no-show", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verifieer e-mailwijziging", "buy_credits": "Credits kopen", "credits": "Credits", - "credits_used": "Gebruikte credits", - "total_credits_remaining": "Totaal resterend", - "credits_per_tip_org": "Je ontvangt 1000 credits per maand, per teamlid", - "credits_per_tip_teams": "Je ontvangt 750 credits per maand, per teamlid", - "view_and_manage_credits": "Bekijk en beheer credits voor het versturen van sms-berichten", + "view_and_manage_credits": "Credits bekijken en beheren", "view_and_manage_credits_description": "Bekijk en beheer credits voor het versturen van SMS-berichten. Eén credit is 1¢ (USD) waard. <0>Meer informatie", - "credit_worth_description": "Eén credit is 1¢ (USD) waard. <0>Meer informatie", "buy_additional_credits": "Extra credits kopen ($0.01 per credit)", - "view_additional_credits_expense_tip": "Je kunt extra credituitgaven bekijken in je uitgavenlogboek", "overview": "Overzicht", "organization_slug_taken": "Organisatie-slug is al in gebruik", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Je kunt geen organisatie aanmaken omdat je al deel uitmaakt van een organisatie", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Je Cal.com-team {{teamName}} heeft geen credits meer. Hierdoor worden SMS-berichten nu via e-mail verzonden. Om weer SMS-berichten te kunnen versturen, koop je extra credits.", "credit_limit_reached_message_user": "Je Cal.com-account heeft geen credits meer. Hierdoor worden sms-berichten nu via e-mail verzonden. Om weer sms-berichten te kunnen versturen, koop je extra credits.", "current_credit_balance": "Huidige saldo: {{balance}} credits", - "current_balance": "Huidige saldo:", "notification_about_your_booking": "Melding over je boeking", "monthly_credits": "Maandelijkse credits", "total_credits": "Totaal credits: {{totalCredits}}", "remaining_credits": "Resterende credits: {{remainingCredits}}", - "remaining": "Resterend", - "total": "Totaal", "additional_credits": "Extra credits", "routing_form_next_in_queue": "{{count}} volgende in wachtrij", "routing_form_select_members_to_email": "Stuur e-mailreacties naar", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Bestaande werkstromen en hun configuraties bekijken", "pbac_desc_update_workflows": "Werkstroominstellingen bewerken en wijzigen", "pbac_desc_delete_workflows": "Werkstromen uit het systeem verwijderen", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Webhooks maken", - "pbac_desc_view_webhooks": "Webhooks bekijken", - "pbac_desc_update_webhooks": "Webhooks bijwerken", - "pbac_desc_delete_webhooks": "Webhooks verwijderen", "pbac_desc_manage_workflows": "Volledige beheertoegang tot alle werkstromen", "pbac_desc_create_event_types": "Gebeurtenistypes aanmaken", "pbac_desc_view_event_types": "Gebeurtenistypes bekijken", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "De naam van de trigger-gebeurtenis (bijv. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Het tijdstip van de webhook", "webhook_type": "De slug van het gebeurtenistype", - "set_up_agent": "Agent instellen", "webhook_title": "De naam van het gebeurtenistype", "webhook_start_time": "De starttijd van de gebeurtenis", "webhook_end_time": "De eindtijd van de gebeurtenis", @@ -3672,9 +3625,6 @@ "visit": "Bezoek", "location_custom_label_input_label": "Aangepast label op boekingspagina", "meeting_link": "Vergaderlink", - "session_outcome": "Sessieresultaat", - "call_created": "Gesprek aangemaakt", - "voicemail": "Voicemail", "my_bookings": "Mijn boekingen", "phone": "Telefoon", "free": "Gratis", @@ -3682,8 +3632,6 @@ "user_name": "Gebruikersnaam", "expand_panel": "Paneel uitklappen", "collapse_panel": "Paneel inklappen", - "email_verification_required": "E-mailverificatie is vereist voor dit afspraaktype", - "invalid_verification_code": "Ongeldige verificatiecode opgegeven", "you_have_one_team": "Je hebt één team", "consider_consolidating_one_team_org": "Overweeg een organisatie op te zetten om facturering, beheerderstools en analyses voor je team te verenigen.", "consider_consolidating_multi_team_org": "Overweeg een organisatie op te zetten om facturering, beheerderstools en analyses voor je teams te verenigen.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Voor geplande starttijd", "cancel_booking_acknowledge_no_show_fee": "Ik erken dat door de boeking te annuleren binnen {{timeValue}} {{timeUnit}} voor de starttijd, mij de no-show kosten van {{amount, currency}} in rekening worden gebracht", "contact_organizer": "Als je vragen hebt, neem dan contact op met de organisator.", - "booking_time_option": "Boekingstijd", - "booking_time_option_description": "Wanneer de boeking is gepland (begin tot eind)", - "created_at_option": "Aangemaakt op", - "created_at_option_description": "Wanneer de boeking oorspronkelijk is aangemaakt", - "call_details": "Gespreksdetails", - "call_id": "Gesprek-ID", - "call_information": "Gespreksinformatie", - "sentiment": "Sentiment", - "disconnect_reason": "Reden voor verbreken", - "call_summary": "Gespreksoverzicht", - "transcription": "Transcriptie", - "event_details": "Afspraakdetails", - "agent": "Agent", - "no_transcript_available": "Geen transcript beschikbaar", - "testing_sms_workflow_info_message": "Houd er bij het testen van deze workflow rekening mee dat sms'en minimaal 15 minuten van tevoren moeten worden gepland", - "start_from_scratch_title": "Begin vanaf nul", - "start_from_scratch_description": "Maak je eigen workflow vanaf nul.", - "cal_ai_template_title": "Cal.ai sjabloon", - "cal_ai_template_description": "AI-agents die afspraken boeken, herinneringen sturen en opvolgen!", - "voice": "Stem", - "select_voice": "Selecteer stem", - "select_voice_for_agent": "Selecteer een stem voor je agent", - "choose_a_voice_for_your_agent": "Kies een stem voor je agent", - "trait": "Eigenschap", - "voice_id": "Stem-ID", - "use_voice": "Gebruik stem", - "current_voice": "Huidige stem", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/no/common.json b/apps/web/public/static/locales/no/common.json index 909a79542749e9..985954b7c02346 100644 --- a/apps/web/public/static/locales/no/common.json +++ b/apps/web/public/static/locales/no/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Bekreft e-postadressen din for å garantere best mulig levering av e-post og kalender", "verify_email_email_header": "Bekreft e-postadressen din", "verify_email_button": "Bekreft e-post", - "cal_ai_assistant": "Assistent", + "cal_ai_assistant": "Cal AI-assistent", "send_cal_video_transcription_emails": "Send Cal Video-transkripsjonseposter", "description_send_cal_video_transcription_emails": "Send e-poster med transkripsjonen av Cal Video etter at møtet er avsluttet. (Krever et betalt abonnement)", "verify_email_change_description": "Du har nylig bedt om å endre e-postadressen du bruker til å logge inn på din {{appName}}-konto. Vennligst klikk på knappen nedenfor for å bekrefte din nye e-postadresse.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Arbeidsflytvalidering mislyktes", "workflow_validation_empty_fields": "Ett eller flere arbeidsflyttrinn har tomt meldingsinnhold", "workflow_validation_unverified_contacts": "Ett eller flere telefonnumre eller e-postadresser er ikke verifisert", - "supercharge_your_workflows_with_cal_ai": "Gi arbeidsflyten din superkrefter med Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Livaktige KI-agenter som booker møter, sender påminnelser og følger opp kundene dine.", "phone_number_imported_successfully": "Telefonnummer importert og koblet til agent", "phone_number_deleted_successfully": "Telefonnummer slettet", "delete_phone_number_confirmation": "Er du sikker på at du vil slette dette telefonnummeret? Denne handlingen kan ikke angres.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Telefonnummerabonnement kansellert vellykket", "updating": "Oppdaterer", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Hei, hvordan går det?", "round_robin_description": "Varier møter mellom flere teammedlemmer.", "managed_event": "Administrert arrangement", "username_placeholder": "brukernavn", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Er du sikker på at du vil slette dette arbeidsflyt-trinnet?", "do_you_still_want_to_unsubscribe": "Vil du fortsatt avslutte abonnementet på telefonnummeret fra denne agenten?", "the_action_will_disconnect_phone_number": "Denne handlingen vil koble telefonnummeret fra agenten. Agenten vil ikke kunne ringe før et nytt telefonnummer er tilkoblet.", - "cal_ai_phone_numbers": "Telefonnumre", + "cal_ai_phone_numbers": "Cal AI-telefonnumre", "connect_phone_number": "Koble til telefonnummer", - "cal_ai_phone_numbers_description": "Administrer telefonnumrene dine", + "cal_ai_phone_numbers_description": "Administrer dine Cal AI-telefonnumre", "import_number": "Importer nummer", "this_action_will_also": "Denne handlingen vil også:", "import_phone_number": "Importer telefonnummer", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Kanseller ditt telefonnummer-abonnement", "delete_associated_phone_number": "Slett det tilknyttede telefonnummeret", "unauthorized_create_workflow": "Du har ikke tillatelse til å opprette denne arbeidsflyten", - "import_phone_number_description": "Importer Twilio-telefonnummeret ditt for bruk med telefon", + "import_phone_number_description": "Importer ditt Twilio-telefonnummer for bruk med Phone", "phone_number_cost": "${{price}}/måned", "buy_new_number": "Kjøp nytt nummer", "buy_number_cost_x_per_month": "Å kjøpe et telefonnummer koster ${{priceInDollars}} per måned. Du vil bli belastet månedlig for hvert aktivt telefonnummer.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Ja, kanseller abonnement", "cancel_phone_number_subscription_confirmation": "Er du sikker på at du vil kansellere dette telefonnummerabonnementet? Denne handlingen kan ikke angres, og du vil miste tilgangen til dette telefonnummeret.", "add_members": "Legg til medlemmer...", - "add_members_no_ellipsis": "Legg til medlemmer", "no_assigned_members": "Ingen tildelte medlemmer", "assigned_to": "Tildelt til", "you_must_be_logged_in_to": "Du må være logget inn på {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategorier", "pricing": "Priser", "learn_more": "Finn ut mer", - "try_now": "Prøv nå", "privacy_policy": "Personvernerklæring", "terms_of_service": "Vilkår for bruk", "remove": "Fjern", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "send WhatsApp-melding til deltaker", "workflows": "Arbeidsflyter", "new_workflow_btn": "Ny Arbeidsflyt", - "how_would_you_like_to_start": "Hvordan vil du starte?", "add_new_workflow": "Legg til en ny arbeidsflyt", "reschedule_event_trigger": "når hendelsen får ny tid", "trigger": "Utløser", @@ -1722,8 +1716,6 @@ "event_duration_info": "Varighet på arrangementet", "event_time_info": "Start-tidspunkt for hendelsen", "event_type_not_found": "Arrangementstype ikke funnet", - "number_to_call_variable": "Nummer å ringe", - "number_to_call_info": "Telefonnummeret til brukeren du ringer", "location_variable": "Sted", "location_info": "Hendelsessted", "additional_notes_variable": "Tilleggsnotater", @@ -1761,7 +1753,6 @@ "team_url": "Team URL", "team_members": "Team-medlemmer", "more": "Mer", - "cal_ai_workflows": "Cal.ai-arbeidsflyter", "and_count_more": "og {{count}} til", "more_page_footer": "Vi ser på mobil-applikasjonen som en utvidelse av nettapplikasjonen. Hvis du skal utføre kompliserte handlinger, vennligst gå tilbake til nettapplikasjonen.", "workflow_example_1": "Send SMS-påminnelse til deltakeren 24 timer før hendelsen starter", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Send deltakeren en e-post-påminnelse 1 time før hendelser starter", "workflow_example_5": "Send verten en tilpasset e-post når en hendelse får nytt tidspunkt", "workflow_example_6": "Send verten en tilpasset SMS når en ny hendelse er booket", - "send_sms_reminder": "Send SMS-påminnelse", - "send_sms_reminder_description": "24 timer før arrangementet starter", - "follow_up_with_no_shows": "Følg opp ved uteblivelse", - "follow_up_with_no_shows_description": "30 minutter etter at arrangementet er slutt", - "remind_attendees_to_bring_id": "Påminn deltakere om å ta med ID", - "remind_attendees_to_bring_id_description": "1 dag før arrangementet starter", - "email_to_remind_booking": "E-postpåminnelse", - "email_to_remind_booking_description": "1 time før arrangementet starter", - "custom_sms_reminder": "Tilpasset SMS-påminnelse", - "custom_sms_reminder_description": "Når arrangement blir planlagt", - "custom_email_reminder": "Tilpasset e-postpåminnelse", - "custom_email_reminder_description": "Arrangement er omplanlagt til vert", "count_managed_to_limit": "Inkluder antall bookinger fra administrerte hendelsestyper", "welcome_to_cal_header": "Velkommen til {{appName}}!", "edit_form_later_subtitle": "Du kan redigere dette senere.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Vis på bestillingssiden", "visit_cancelled_booking": "Du kan besøke siden for avbestilt booking", "get_started_zapier_templates": "Kom i gang med Zapier-maler", - "standard_templates": "Standardmaler", - "cal_ai_templates": "Cal.ai-maler", "team_is_unpublished": "{{team}} er upublisert", "org_is_unpublished_description": "Denne organisasjonslinken er for øyeblikket ikke tilgjengelig. Vennligst kontakt organisasjonseieren eller be dem publisere den.", "team_is_unpublished_description": "Denne teamlinken er for øyeblikket ikke tilgjengelig. Vennligst kontakt teameieren eller be dem publisere den.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Kunne ikke belaste kortet for betaling.", "insights": "Innsikt", "routing_forms": "Rute skjemaer", + "testing_workflow_info_message": "Når du tester denne arbeidsflyten, vær oppmerksom på at e-poster og SMS kun kan planlegges minst 1 time i forveien", "insights_no_data_found_for_filter": "Ingen data funnet for det valgte filteret eller de valgte datoene.", "acknowledge_booking_no_show_fee": "Jeg erkjenner at hvis jeg ikke deltar på dette arrangementet, vil et gebyr på {{amount, currency}} for manglende oppmøte bli belastet kortet mitt.", "days": "dager", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Lag: {{teamName}}", "insights_user_filter": "Bruker: {{userName}}", "insights_subtitle": "Se innsikt i bestillinger på tvers av dine arrangementer", - "call_history": "Samtalehistorikk", - "call_history_subtitle": "Se samtalehistorikk for alle dine Cal.ai-samtaler", "location_options": "{{locationCount}} stedsalternativer", - "channel_type": "Kanaltype", - "end_reason": "Årsak til avslutning", - "session_status": "Sesjonsstatus", - "user_sentiment": "Brukersentiment", - "time_header": "Tid", - "from_header": "Fra", "custom_plan": "Tilpasset plan", "email_embed": "E-post innebygging", "add_times_to_your_email": "Velg noen tilgjengelige tider og legg dem inn i e-posten din", @@ -2782,8 +2752,6 @@ "account_already_linked": "Kontoen er allerede koblet", "send_email": "Send e-post", "cal_ai_phone_call_action": "Ring deltaker ved hjelp av Cal.ai stemmeagent", - "call_to_confirm_booking": "Ring for å bekrefte booking", - "cal_ai_phone_call_action_description": "2 timer før avtalen starter", "cal_ai_agent_configuration": "Cal.ai-agent konfigurasjon", "choose_at_least_one_event_type_test_call": "Vennligst velg minst én hendelsestype for å foreta et testanrop.", "mark_as_no_show": "Merk som ikke møtt", @@ -3235,15 +3203,9 @@ "verify_email_change": "Bekreft endring av e-post", "buy_credits": "Kjøp kreditter", "credits": "Kreditter", - "credits_used": "Kreditter brukt", - "total_credits_remaining": "Totalt gjenværende", - "credits_per_tip_org": "Du mottar 1000 kreditter per måned, per teammedlem", - "credits_per_tip_teams": "Du mottar 750 kreditter per måned, per teammedlem", - "view_and_manage_credits": "Se og administrer kreditter for å sende SMS-meldinger", + "view_and_manage_credits": "Vis og administrer kreditter", "view_and_manage_credits_description": "Vis og administrer kreditter for å sende SMS-meldinger. Én kreditt er verdt 1¢ (USD). <0>Lær mer", - "credit_worth_description": "Én kreditt er verdt 1¢ (USD). <0>Lær mer", "buy_additional_credits": "Kjøp flere kreditter ($0.01 per kreditt)", - "view_additional_credits_expense_tip": "Du kan se ytterligere kredittforbruk i utgiftsloggen din", "overview": "Oversikt", "organization_slug_taken": "Organisasjonens kortnavn er allerede tatt", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Du kan ikke opprette en organisasjon fordi du allerede er en del av en organisasjon", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Cal.com-teamet ditt {{teamName}} har gått tom for kreditter. Som et resultat blir SMS-meldinger nå sendt via e-post i stedet. For å gjenoppta sending av SMS, vennligst kjøp flere kreditter.", "credit_limit_reached_message_user": "Cal.com-kontoen din har gått tom for kreditter. Som et resultat sendes SMS-meldinger nå via e-post i stedet. For å fortsette å sende SMS, vennligst kjøp flere kreditter.", "current_credit_balance": "Nåværende saldo: {{balance}} kreditter", - "current_balance": "Nåværende saldo:", "notification_about_your_booking": "Varsling om din bestilling", "monthly_credits": "Månedlige kreditter", "total_credits": "Totale kreditter: {{totalCredits}}", "remaining_credits": "Gjenværende kreditter: {{remainingCredits}}", - "remaining": "Gjenværende", - "total": "Totalt", "additional_credits": "Ekstra kreditter", "routing_form_next_in_queue": "{{count}} neste i køen", "routing_form_select_members_to_email": "Send e-postsvar til", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Se eksisterende arbeidsflyter og deres konfigurasjoner", "pbac_desc_update_workflows": "Redigere og endre arbeidsflytinnstillinger", "pbac_desc_delete_workflows": "Fjerne arbeidsflyter fra systemet", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Opprett webhook'er", - "pbac_desc_view_webhooks": "Se webhook'er", - "pbac_desc_update_webhooks": "Oppdater webhooks", - "pbac_desc_delete_webhooks": "Slett webhooks", "pbac_desc_manage_workflows": "Full administrasjonstilgang til alle arbeidsflyter", "pbac_desc_create_event_types": "Opprett hendelsestyper", "pbac_desc_view_event_types": "Vis hendelsestyper", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Navnet på utløserhendelsen (f.eks. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Tidspunktet for webhook", "webhook_type": "Hendelsestype-slug", - "set_up_agent": "Konfigurer agent", "webhook_title": "Navnet på hendelsestypen", "webhook_start_time": "Hendelsens starttid", "webhook_end_time": "Hendelsens sluttid", @@ -3672,9 +3625,6 @@ "visit": "Besøk", "location_custom_label_input_label": "Egendefinert etikett på bookingsiden", "meeting_link": "Møtelenke", - "session_outcome": "Øktresultat", - "call_created": "Samtale opprettet", - "voicemail": "Talepost", "my_bookings": "Mine bookinger", "phone": "Telefon", "free": "Gratis", @@ -3682,8 +3632,6 @@ "user_name": "Brukernavn", "expand_panel": "Utvid panel", "collapse_panel": "Skjul panel", - "email_verification_required": "E-postverifisering er påkrevd for denne hendelsestypen", - "invalid_verification_code": "Ugyldig verifiseringskode angitt", "you_have_one_team": "Du har ett team", "consider_consolidating_one_team_org": "Vurder å opprette en organisasjon for å samle fakturering, adminverktøy og analyser for teamet ditt.", "consider_consolidating_multi_team_org": "Vurder å opprette en organisasjon for å samle fakturering, adminverktøy og analyser for teamene dine.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Før planlagt starttid", "cancel_booking_acknowledge_no_show_fee": "Jeg erkjenner at ved å avbestille bookingen innen {{timeValue}} {{timeUnit}} før starttidspunktet vil jeg bli belastet et gebyr for uteblivelse på {{amount, currency}}", "contact_organizer": "Hvis du har spørsmål, vennligst kontakt arrangøren.", - "booking_time_option": "Bestillingstid", - "booking_time_option_description": "Når bestillingen er planlagt (start til slutt)", - "created_at_option": "Opprettet den", - "created_at_option_description": "Når bestillingen opprinnelig ble opprettet", - "call_details": "Samtaledetaljer", - "call_id": "Samtale-ID", - "call_information": "Samtaleinformasjon", - "sentiment": "Stemning", - "disconnect_reason": "Frakoblingsårsak", - "call_summary": "Sammendrag av samtale", - "transcription": "Transkripsjon", - "event_details": "Hendelsesdetaljer", - "agent": "Agent", - "no_transcript_available": "Ingen transkripsjon tilgjengelig", - "testing_sms_workflow_info_message": "Når du tester denne arbeidsflyten, vær oppmerksom på at SMS må planlegges minst 15 minutter i forveien", - "start_from_scratch_title": "Start fra bunnen av", - "start_from_scratch_description": "Lag din egen arbeidsflyt fra bunnen av.", - "cal_ai_template_title": "Cal.ai-mal", - "cal_ai_template_description": "AI-agenter som booker møter, sender påminnelser og følger opp!", - "voice": "Stemme", - "select_voice": "Velg stemme", - "select_voice_for_agent": "Velg en stemme for agenten din", - "choose_a_voice_for_your_agent": "Velg en stemme for agenten din", - "trait": "Egenskap", - "voice_id": "Stemme-ID", - "use_voice": "Bruk stemme", - "current_voice": "Nåværende stemme", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index aa7ce53ca2123f..44565d3da4ded8 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Potwierdź adres e-mail, aby upewnić się, że wiadomości e-mail i powiadomienia z kalendarza będą do Ciebie docierać.", "verify_email_email_header": "Zweryfikuj swój adres e-mail", "verify_email_button": "Zweryfikuj email", - "cal_ai_assistant": "Asystent", + "cal_ai_assistant": "Asystent AI Cal", "send_cal_video_transcription_emails": "Wysyłaj e-maile z transkrypcją Cal Video", "description_send_cal_video_transcription_emails": "Wysyłaj e-maile z transkrypcją Cal Video po zakończeniu spotkania. (Wymaga płatnego planu)", "verify_email_change_description": "Ostatnio poprosiłeś o zmianę adresu email, którego używasz do logowania się na swoje konto {{appName}}. Kliknij poniższy przycisk, aby potwierdzić nowy adres email.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Walidacja przepływu pracy nie powiodła się", "workflow_validation_empty_fields": "Jedno lub więcej kroków przepływu pracy ma puste treści wiadomości", "workflow_validation_unverified_contacts": "Jeden lub więcej numerów telefonów lub adresów e-mail nie zostało zweryfikowanych", - "supercharge_your_workflows_with_cal_ai": "Zwiększ efektywność swoich procesów z Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Realistyczni agenci AI, którzy rezerwują spotkania, wysyłają przypomnienia i kontaktują się z Twoimi klientami.", "phone_number_imported_successfully": "Numer telefonu został pomyślnie zaimportowany i powiązany z agentem", "phone_number_deleted_successfully": "Numer telefonu został pomyślnie usunięty", "delete_phone_number_confirmation": "Czy na pewno chcesz usunąć ten numer telefonu? Tej operacji nie można cofnąć.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Subskrypcja numeru telefonu została pomyślnie anulowana", "updating": "Aktualizowanie", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Cześć, jak się masz?", "round_robin_description": "Cykl spotkań między wieloma członkami zespołu.", "managed_event": "Zarządzane wydarzenie", "username_placeholder": "nazwa-użytkownika", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Czy na pewno chcesz usunąć ten krok przepływu pracy?", "do_you_still_want_to_unsubscribe": "Czy nadal chcesz wypisać numer telefonu z tego agenta?", "the_action_will_disconnect_phone_number": "Ta akcja odłączy numer telefonu od agenta. Agent nie będzie mógł wykonywać połączeń, dopóki nie zostanie podłączony nowy numer telefonu.", - "cal_ai_phone_numbers": "Numery telefonów", + "cal_ai_phone_numbers": "Numery telefonów Cal AI", "connect_phone_number": "Podłącz numer telefonu", - "cal_ai_phone_numbers_description": "Zarządzaj swoimi numerami telefonów", + "cal_ai_phone_numbers_description": "Zarządzaj swoimi numerami telefonów Cal AI", "import_number": "Importuj numer", "this_action_will_also": "Ta akcja również:", "import_phone_number": "Importuj numer telefonu", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Anuluj subskrypcję numeru telefonu", "delete_associated_phone_number": "Usuń powiązany numer telefonu", "unauthorized_create_workflow": "Nie masz uprawnień do utworzenia tego przepływu pracy", - "import_phone_number_description": "Zaimportuj swój numer Twilio, aby używać go z Phone", + "import_phone_number_description": "Zaimportuj swój numer telefonu Twilio, aby używać go z Phone", "phone_number_cost": "${{price}}/miesiąc", "buy_new_number": "Kup nowy numer", "buy_number_cost_x_per_month": "Zakup numeru telefonu kosztuje ${{priceInDollars}} miesięcznie. Opłata będzie naliczana co miesiąc za każdy aktywny numer telefonu.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Tak, anuluj subskrypcję", "cancel_phone_number_subscription_confirmation": "Czy na pewno chcesz anulować subskrypcję tego numeru telefonu? Tej akcji nie można cofnąć, a dostęp do tego numeru telefonu zostanie utracony.", "add_members": "Dodaj członków...", - "add_members_no_ellipsis": "Dodaj członków", "no_assigned_members": "Brak przypisanych członków", "assigned_to": "Przypisano do:", "you_must_be_logged_in_to": "Musisz być zalogowany do {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategorie", "pricing": "Cennik", "learn_more": "Więcej informacji", - "try_now": "Wypróbuj teraz", "privacy_policy": "Polityka prywatności", "terms_of_service": "Warunki korzystania z usługi", "remove": "Usuń", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "wyślij wiadomość w aplikacji Whatsapp do uczestnika", "workflows": "Przepływy pracy", "new_workflow_btn": "Nowy przepływ pracy", - "how_would_you_like_to_start": "Jak chciałbyś zacząć?", "add_new_workflow": "Dodaj nowy przepływ pracy", "reschedule_event_trigger": "kiedy wydarzenie zostanie przełożone", "trigger": "Wyzwalacz", @@ -1722,8 +1716,6 @@ "event_duration_info": "Czas trwania wydarzenia", "event_time_info": "Godzina rozpoczęcia wydarzenia", "event_type_not_found": "Typ wydarzenia nie znaleziony", - "number_to_call_variable": "Numer do zadzwonienia", - "number_to_call_info": "Numer telefonu użytkownika, do którego dzwonisz", "location_variable": "Lokalizacja", "location_info": "Lokalizacja wydarzenia", "additional_notes_variable": "Dodatkowe uwagi", @@ -1761,7 +1753,6 @@ "team_url": "Adres URL zespołu", "team_members": "Członkowie zespołu", "more": "Więcej", - "cal_ai_workflows": "Procesy Cal.ai", "and_count_more": "i {{count}} więcej", "more_page_footer": "Aplikacja mobilna jest jedynie rozszerzeniem aplikacji przeglądarkowej. Jeśli wykonujesz skomplikowane działania, użyj aplikacji przeglądarkowej.", "workflow_example_1": "Wyślij do uczestnika przypomnienie SMS na 24 godziny przed rozpoczęciem wydarzenia", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Wyślij do uczestnika wiadomość e-mail z przypomnieniem na 1 godzinę przed rozpoczęciem wydarzenia", "workflow_example_5": "Wyślij do gospodarza niestandardową wiadomość e-mail, gdy wydarzenie zostanie przełożone", "workflow_example_6": "Wyślij do gospodarza niestandardową wiadomość SMS, gdy zostanie zarezerwowane nowe wydarzenie", - "send_sms_reminder": "Wyślij przypomnienie SMS", - "send_sms_reminder_description": "24 godziny przed rozpoczęciem wydarzenia", - "follow_up_with_no_shows": "Skontaktuj się z nieobecnymi", - "follow_up_with_no_shows_description": "30 minut po zakończeniu wydarzenia", - "remind_attendees_to_bring_id": "Przypomnij uczestnikom o zabraniu dowodu tożsamości", - "remind_attendees_to_bring_id_description": "1 dzień przed rozpoczęciem wydarzenia", - "email_to_remind_booking": "Przypomnienie e-mail", - "email_to_remind_booking_description": "1 godzina przed rozpoczęciem wydarzenia", - "custom_sms_reminder": "Niestandardowe przypomnienie SMS", - "custom_sms_reminder_description": "Gdy wydarzenie zostanie zaplanowane", - "custom_email_reminder": "Niestandardowe przypomnienie e-mail", - "custom_email_reminder_description": "Wydarzenie zostało przełożone na gospodarza", "count_managed_to_limit": "Uwzględnij liczbę rezerwacji z zarządzanych typów wydarzeń", "welcome_to_cal_header": "Witaj w {{appName}}!", "edit_form_later_subtitle": "Będziesz mógł/mogła to edytować później.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Pokaż na stronie rezerwacji", "visit_cancelled_booking": "Możesz odwiedzić stronę anulowanej rezerwacji", "get_started_zapier_templates": "Zacznij korzystać z szablonów Zapier", - "standard_templates": "Standardowe szablony", - "cal_ai_templates": "Szablony Cal.ai", "team_is_unpublished": "Zespół {{team}} nie został opublikowany", "org_is_unpublished_description": "Link organizacji jest obecnie niedostępny. Skontaktuj się z właścicielem organizacji lub poproś o jego opublikowanie.", "team_is_unpublished_description": "Link jednostki {{entity}} jest obecnie niedostępny. Skontaktuj się z właścicielem jednostki {{entity}} lub poproś o jego opublikowanie.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Nie udało się obciążyć karty za płatność.", "insights": "Wglądy", "routing_forms": "Formularze routingu", + "testing_workflow_info_message": "Podczas testowania tego przepływu pracy pamiętaj, że wiadomości e-mail i SMS można zaplanować jedynie z co najmniej 1-godzinnym wyprzedzeniem", "insights_no_data_found_for_filter": "Nie znaleziono danych dotyczących wybranego filtru lub wskazanych dat.", "acknowledge_booking_no_show_fee": "Rozumiem, że jeśli nie wezmę udziału w wydarzeniu, z mojej karty pobrana zostanie opłata za niestawienie się w wysokości {{amount, currency}}.", "days": "dni", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Zespół: {{teamName}}", "insights_user_filter": "Użytkownik: {{userName}}", "insights_subtitle": "Wyświetl wskaźniki Insights rezerwacji z różnych wydarzeń", - "call_history": "Historia połączeń", - "call_history_subtitle": "Przeglądaj historię połączeń w ramach rozmów Cal.ai", "location_options": "Lokalizacje do wyboru: {{locationCount}}", - "channel_type": "Typ kanału", - "end_reason": "Powód zakończenia", - "session_status": "Status sesji", - "user_sentiment": "Nastawienie użytkownika", - "time_header": "Czas", - "from_header": "Od", "custom_plan": "Plan niestandardowy", "email_embed": "Osadź w wiadomości e-mail", "add_times_to_your_email": "Wybierz kilka dostępnych terminów i osadź je w wiadomości e-mail", @@ -2782,8 +2752,6 @@ "account_already_linked": "Konto jest już połączone", "send_email": "Wyślij wiadomość e-mail", "cal_ai_phone_call_action": "Zadzwoń do uczestnika za pomocą agenta głosowego Cal.ai", - "call_to_confirm_booking": "Połączenie w celu potwierdzenia rezerwacji", - "cal_ai_phone_call_action_description": "2 godziny przed rozpoczęciem wydarzenia", "cal_ai_agent_configuration": "Konfiguracja agenta Cal.ai", "choose_at_least_one_event_type_test_call": "Wybierz co najmniej jeden typ wydarzenia, aby wykonać testowe połączenie.", "mark_as_no_show": "Oznacz jako nieobecny", @@ -3235,15 +3203,9 @@ "verify_email_change": "Zweryfikuj zmianę adresu email", "buy_credits": "Kup kredyty", "credits": "Kredyty", - "credits_used": "Wykorzystane kredyty", - "total_credits_remaining": "Pozostało łącznie", - "credits_per_tip_org": "Otrzymujesz 1000 kredytów miesięcznie na członka zespołu", - "credits_per_tip_teams": "Otrzymujesz 750 kredytów miesięcznie na członka zespołu", - "view_and_manage_credits": "Zobacz i zarządzaj kredytami na wysyłanie wiadomości SMS", + "view_and_manage_credits": "Wyświetl i zarządzaj kredytami", "view_and_manage_credits_description": "Wyświetl i zarządzaj kredytami do wysyłania wiadomości SMS. Jeden kredyt jest wart 1¢ (USD). <0>Dowiedz się więcej", - "credit_worth_description": "Jeden kredyt jest wart 1¢ (USD). <0>Dowiedz się więcej", "buy_additional_credits": "Kup dodatkowe kredyty (0,01 USD za kredyt)", - "view_additional_credits_expense_tip": "Możesz zobaczyć dodatkowe wydatki na kredyty w swoim dzienniku wydatków", "overview": "Przegląd", "organization_slug_taken": "Slug organizacji jest już zajęty", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Nie możesz utworzyć organizacji, ponieważ jesteś już częścią innej organizacji", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Twój zespół Cal.com {{teamName}} nie ma już kredytów. W związku z tym wiadomości SMS są teraz wysyłane jako e-maile. Aby wznowić wysyłanie SMS-ów, kup dodatkowe kredyty.", "credit_limit_reached_message_user": "Twoje konto Cal.com nie ma już dostępnych kredytów. W związku z tym wiadomości SMS są teraz wysyłane za pośrednictwem e-maila. Aby wznowić wysyłanie SMS-ów, prosimy o zakup dodatkowych kredytów.", "current_credit_balance": "Aktualne saldo: {{balance}} kredytów", - "current_balance": "Bieżące saldo:", "notification_about_your_booking": "Powiadomienie o Twojej rezerwacji", "monthly_credits": "Miesięczne kredyty", "total_credits": "Łączna liczba kredytów: {{totalCredits}}", "remaining_credits": "Pozostałe kredyty: {{remainingCredits}}", - "remaining": "Pozostało", - "total": "Łącznie", "additional_credits": "Dodatkowe kredyty", "routing_form_next_in_queue": "{{count}} następny w kolejce", "routing_form_select_members_to_email": "Wyślij odpowiedzi e-mail do", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Wyświetl istniejące przepływy pracy i ich konfiguracje", "pbac_desc_update_workflows": "Edytuj i modyfikuj ustawienia przepływów pracy", "pbac_desc_delete_workflows": "Usuń przepływy pracy z systemu", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Twórz webhooki", - "pbac_desc_view_webhooks": "Przeglądaj webhooki", - "pbac_desc_update_webhooks": "Aktualizuj webhooks", - "pbac_desc_delete_webhooks": "Usuń webhooks", "pbac_desc_manage_workflows": "Pełny dostęp do zarządzania wszystkimi przepływami pracy", "pbac_desc_create_event_types": "Tworzenie typów wydarzeń", "pbac_desc_view_event_types": "Wyświetlanie typów wydarzeń", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Nazwa zdarzenia wyzwalającego (np. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Czas utworzenia webhooka", "webhook_type": "Slug typu zdarzenia", - "set_up_agent": "Skonfiguruj agenta", "webhook_title": "Nazwa typu zdarzenia", "webhook_start_time": "Czas rozpoczęcia wydarzenia", "webhook_end_time": "Czas zakończenia wydarzenia", @@ -3672,9 +3625,6 @@ "visit": "Odwiedź", "location_custom_label_input_label": "Niestandardowa etykieta na stronie rezerwacji", "meeting_link": "Link do spotkania", - "session_outcome": "Wynik sesji", - "call_created": "Połączenie utworzone", - "voicemail": "Poczta głosowa", "my_bookings": "Moje rezerwacje", "phone": "Telefon", "free": "Darmowe", @@ -3682,8 +3632,6 @@ "user_name": "Nazwa użytkownika", "expand_panel": "Rozwiń panel", "collapse_panel": "Zwiń panel", - "email_verification_required": "Weryfikacja e-mail jest wymagana dla tego typu wydarzenia", - "invalid_verification_code": "Podano nieprawidłowy kod weryfikacyjny", "you_have_one_team": "Masz jeden zespół", "consider_consolidating_one_team_org": "Rozważ utworzenie organizacji, aby zjednoczyć rozliczenia, narzędzia administracyjne i analitykę w swoim zespole.", "consider_consolidating_multi_team_org": "Rozważ utworzenie organizacji, aby zjednoczyć rozliczenia, narzędzia administracyjne i analitykę w swoich zespołach.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Przed zaplanowanym czasem rozpoczęcia", "cancel_booking_acknowledge_no_show_fee": "Potwierdzam, że odwołując rezerwację w ciągu {{timeValue}} {{timeUnit}} przed czasem rozpoczęcia, zostanie naliczona opłata za niepojawienie się w wysokości {{amount, currency}}", "contact_organizer": "Jeśli masz jakiekolwiek pytania, skontaktuj się z organizatorem.", - "booking_time_option": "Czas rezerwacji", - "booking_time_option_description": "Kiedy rezerwacja jest zaplanowana (od początku do końca)", - "created_at_option": "Utworzono", - "created_at_option_description": "Kiedy rezerwacja została pierwotnie utworzona", - "call_details": "Szczegóły połączenia", - "call_id": "ID połączenia", - "call_information": "Informacje o połączeniu", - "sentiment": "Nastrój", - "disconnect_reason": "Powód rozłączenia", - "call_summary": "Podsumowanie połączenia", - "transcription": "Transkrypcja", - "event_details": "Szczegóły wydarzenia", - "agent": "Agent", - "no_transcript_available": "Brak dostępnej transkrypcji", - "testing_sms_workflow_info_message": "Podczas testowania tego przepływu pracy pamiętaj, że SMS-y muszą być zaplanowane z co najmniej 15-minutowym wyprzedzeniem", - "start_from_scratch_title": "Zacznij od zera", - "start_from_scratch_description": "Utwórz własny przepływ pracy od podstaw.", - "cal_ai_template_title": "Szablon Cal.ai", - "cal_ai_template_description": "Agenci AI, którzy umawiają spotkania, wysyłają przypomnienia i śledzą działania!", - "voice": "Głos", - "select_voice": "Wybierz głos", - "select_voice_for_agent": "Wybierz głos dla swojego agenta", - "choose_a_voice_for_your_agent": "Wybierz głos dla swojego agenta", - "trait": "Cecha", - "voice_id": "ID głosu", - "use_voice": "Użyj głosu", - "current_voice": "Obecny głos", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index c229094d855855..40d438844cb49d 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Verifique seu endereço de e-mail para garantir o fornecimento do melhor e-mail e calendário", "verify_email_email_header": "Verifique seu endereço de e-mail", "verify_email_button": "Verificar e-mail", - "cal_ai_assistant": "Assistente", + "cal_ai_assistant": "Assistente Cal AI", "send_cal_video_transcription_emails": "Enviar e-mails com transcrição do Cal Video", "description_send_cal_video_transcription_emails": "Enviar e-mails com a transcrição do Cal Video após o término da reunião. (Requer um plano pago)", "verify_email_change_description": "Você solicitou recentemente a alteração do endereço de e-mail que você usa para entrar em sua conta {{appName}}. Por favor, clique no botão abaixo para confirmar o seu novo endereço de e-mail.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Falha na validação do fluxo de trabalho", "workflow_validation_empty_fields": "Uma ou mais etapas do fluxo de trabalho têm conteúdo de mensagem vazio", "workflow_validation_unverified_contacts": "Um ou mais números de telefone ou endereços de e-mail não estão verificados", - "supercharge_your_workflows_with_cal_ai": "Potencialize seus fluxos de trabalho com Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Agentes de IA realistas que marcam reuniões, enviam lembretes e fazem acompanhamento com seus clientes.", "phone_number_imported_successfully": "Número de telefone importado e vinculado ao agente com sucesso", "phone_number_deleted_successfully": "Número de telefone excluído com sucesso", "delete_phone_number_confirmation": "Tem certeza de que deseja excluir este número de telefone? Esta ação não pode ser desfeita.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Assinatura do número de telefone cancelada com sucesso", "updating": "Atualizando", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Olá, como você está?", "round_robin_description": "Reuniões recorrentes entre vários membros da equipe.", "managed_event": "Evento gerenciado", "username_placeholder": "nome de usuário", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Tem certeza que deseja excluir esta etapa do fluxo de trabalho?", "do_you_still_want_to_unsubscribe": "Você ainda deseja cancelar a assinatura do número de telefone deste agente?", "the_action_will_disconnect_phone_number": "Esta ação desconectará o número de telefone do agente. O agente não poderá fazer chamadas até que um novo número de telefone seja conectado.", - "cal_ai_phone_numbers": "Números de telefone", + "cal_ai_phone_numbers": "Números de telefone Cal AI", "connect_phone_number": "Conectar número de telefone", - "cal_ai_phone_numbers_description": "Gerencie seus números de telefone", + "cal_ai_phone_numbers_description": "Gerencie seus números de telefone Cal AI", "import_number": "Importar número", "this_action_will_also": "Esta ação também irá:", "import_phone_number": "Importar número de telefone", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Sim, cancelar assinatura", "cancel_phone_number_subscription_confirmation": "Tem certeza que deseja cancelar esta assinatura de número de telefone? Esta ação não pode ser desfeita e você perderá o acesso a este número de telefone.", "add_members": "Adicionar membros...", - "add_members_no_ellipsis": "Adicionar membros", "no_assigned_members": "Nenhum membro atribuído", "assigned_to": "Atribuído a", "you_must_be_logged_in_to": "Você deve estar logado em {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Categorias", "pricing": "Preço", "learn_more": "Saiba mais", - "try_now": "Experimente agora", "privacy_policy": "Política de privacidade", "terms_of_service": "Termos de serviço", "remove": "Remover", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "enviar mensagem por WhatsApp para participante", "workflows": "Fluxos de trabalho", "new_workflow_btn": "Novo fluxo de trabalho", - "how_would_you_like_to_start": "Como você gostaria de começar?", "add_new_workflow": "Adicionar um novo workflow", "reschedule_event_trigger": "quando o evento for reagendado", "trigger": "Gatilho", @@ -1722,8 +1716,6 @@ "event_duration_info": "A duração do evento", "event_time_info": "O horário inicial do evento", "event_type_not_found": "Tipo de evento não encontrado", - "number_to_call_variable": "Número para ligar", - "number_to_call_info": "O número de telefone do usuário que você está chamando", "location_variable": "Local", "location_info": "O local do evento", "additional_notes_variable": "Observações adicionais", @@ -1761,7 +1753,6 @@ "team_url": "URL da equipe", "team_members": "Membros da equipe", "more": "Mais", - "cal_ai_workflows": "Fluxos de trabalho Cal.ai", "and_count_more": "e mais {{count}}", "more_page_footer": "Nós vemos o aplicativo móvel como uma extensão do aplicativo da web. Se você estiver realizando alguma ação complicada, consulte o aplicativo da web.", "workflow_example_1": "Enviar lembrete por SMS 24 horas antes do evento começar para o participante", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Enviar um lembrete por e-mail 1 hora antes dos eventos começarem para o participante", "workflow_example_5": "Enviar e-mail personalizado quando o evento for reagendado para a(o) anfitriã(o)", "workflow_example_6": "Enviar SMS personalizado quando o novo evento for agendado para a(o) anfitriã(o)", - "send_sms_reminder": "Enviar lembrete por SMS", - "send_sms_reminder_description": "24 horas antes do início do evento", - "follow_up_with_no_shows": "Acompanhamento de ausentes", - "follow_up_with_no_shows_description": "30 minutos após o término do evento", - "remind_attendees_to_bring_id": "Lembrar participantes de trazer identificação", - "remind_attendees_to_bring_id_description": "1 dia antes do início do evento", - "email_to_remind_booking": "Lembrete por email", - "email_to_remind_booking_description": "1 hora antes do início do evento", - "custom_sms_reminder": "Lembrete SMS personalizado", - "custom_sms_reminder_description": "Quando o evento é agendado", - "custom_email_reminder": "Lembrete de email personalizado", - "custom_email_reminder_description": "Evento é reagendado para o anfitrião", "count_managed_to_limit": "Incluir contagens de reservas de tipos de eventos gerenciados", "welcome_to_cal_header": "Bem-vindo(a) à {{appName}}!", "edit_form_later_subtitle": "Você poderá editar isso mais tarde.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Mostrar na página de reservas", "visit_cancelled_booking": "Você pode visitar a página de reserva cancelada", "get_started_zapier_templates": "Comece agora com modelos do Zapier", - "standard_templates": "Modelos Padrão", - "cal_ai_templates": "Modelos Cal.ai", "team_is_unpublished": "Publicação de {{team}} cancelada", "org_is_unpublished_description": "Este link da organização não está disponível no momento. Entre em contato com o proprietário da organização ou peça que seja publicado.", "team_is_unpublished_description": "O link de {{entity}} não está disponível por enquanto. Fale com o proprietário de {{entity}} ou peça que seja publicado.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Não foi possível cobrar o cartão para pagamento.", "insights": "Insights", "routing_forms": "Formulários de Roteamento", + "testing_workflow_info_message": "Ao testar este fluxo de trabalho, não se esqueça de que e-mails e SMS só podem ser reprogramados com pelo menos uma hora de antecedência", "insights_no_data_found_for_filter": "Nenhum dado encontrado para o filtro ou dados selecionados.", "acknowledge_booking_no_show_fee": "Estou ciente de que, se eu não comparecer a este evento, será cobrada no meu cartão uma taxa de não comparecimento de {{amount, currency}}.", "days": "dias", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Equipe: {{teamName}}", "insights_user_filter": "Usuário: {{userName}}", "insights_subtitle": "Veja insights de reserva em seus eventos", - "call_history": "Histórico de Chamadas", - "call_history_subtitle": "Visualize o histórico de chamadas em todas as suas chamadas Cal.ai", "location_options": "{{locationCount}} opções de local", - "channel_type": "Tipo de Canal", - "end_reason": "Motivo de Encerramento", - "session_status": "Status da Sessão", - "user_sentiment": "Sentimento do Usuário", - "time_header": "Hora", - "from_header": "De", "custom_plan": "Plano personalizado", "email_embed": "Incorporação de e-mail", "add_times_to_your_email": "Selecione alguns horários disponíveis e incorpore-os no seu e-mail", @@ -2782,8 +2752,6 @@ "account_already_linked": "A conta já está vinculada", "send_email": "Enviar e-mail", "cal_ai_phone_call_action": "Ligar para o participante usando o Agente de Voz Cal.ai", - "call_to_confirm_booking": "Ligar para confirmar reserva", - "cal_ai_phone_call_action_description": "2 horas antes do início do evento", "cal_ai_agent_configuration": "Configuração do agente Cal.ai", "choose_at_least_one_event_type_test_call": "Por favor, escolha pelo menos um tipo de evento para fazer uma chamada de teste.", "mark_as_no_show": "Marcar como não compareceu", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verificar alteração de e-mail", "buy_credits": "Comprar créditos", "credits": "Créditos", - "credits_used": "Créditos utilizados", - "total_credits_remaining": "Total restante", - "credits_per_tip_org": "Você recebe 1000 créditos por mês, por membro da equipe", - "credits_per_tip_teams": "Você recebe 750 créditos por mês, por membro da equipe", - "view_and_manage_credits": "Visualizar e gerenciar créditos para envio de mensagens SMS", + "view_and_manage_credits": "Visualizar e gerenciar créditos", "view_and_manage_credits_description": "Visualize e gerencie créditos para envio de mensagens SMS. Um crédito vale 1¢ (USD). <0>Saiba mais", - "credit_worth_description": "Um crédito vale 1¢ (USD). <0>Saiba mais", "buy_additional_credits": "Comprar créditos adicionais ($0.01 por crédito)", - "view_additional_credits_expense_tip": "Você pode visualizar gastos de créditos adicionais no seu registro de despesas", "overview": "Visão geral", "organization_slug_taken": "O slug da organização já está em uso", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Você não pode criar uma organização, pois já faz parte de uma", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Sua equipe Cal.com {{teamName}} ficou sem créditos. Como resultado, as mensagens SMS agora estão sendo enviadas por e-mail. Para retomar o envio de SMS, compre créditos adicionais.", "credit_limit_reached_message_user": "Sua conta Cal.com ficou sem créditos. Como resultado, as mensagens SMS agora estão sendo enviadas por e-mail. Para retomar o envio de SMS, adquira créditos adicionais.", "current_credit_balance": "Saldo atual: {{balance}} créditos", - "current_balance": "Saldo atual:", "notification_about_your_booking": "Notificação sobre seu agendamento", "monthly_credits": "Créditos mensais", "total_credits": "Total de créditos: {{totalCredits}}", "remaining_credits": "Créditos restantes: {{remainingCredits}}", - "remaining": "Restante", - "total": "Total", "additional_credits": "Créditos adicionais", "routing_form_next_in_queue": "{{count}} próximo na fila", "routing_form_select_members_to_email": "Enviar respostas por e-mail para", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Visualizar fluxos de trabalho existentes e suas configurações", "pbac_desc_update_workflows": "Editar e modificar configurações de fluxo de trabalho", "pbac_desc_delete_workflows": "Remover fluxos de trabalho do sistema", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Criar webhooks", - "pbac_desc_view_webhooks": "Visualizar webhooks", - "pbac_desc_update_webhooks": "Atualizar webhooks", - "pbac_desc_delete_webhooks": "Excluir webhooks", "pbac_desc_manage_workflows": "Acesso completo de gerenciamento a todos os fluxos de trabalho", "pbac_desc_create_event_types": "Criar tipos de eventos", "pbac_desc_view_event_types": "Ver tipos de eventos", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "O nome do evento de gatilho (ex.: BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "O horário do webhook", "webhook_type": "O slug do tipo de evento", - "set_up_agent": "Configurar Agente", "webhook_title": "O nome do tipo de evento", "webhook_start_time": "O horário de início do evento", "webhook_end_time": "O horário de término do evento", @@ -3672,9 +3625,6 @@ "visit": "Visitar", "location_custom_label_input_label": "Rótulo personalizado na página de reserva", "meeting_link": "Link da reunião", - "session_outcome": "Resultado da Sessão", - "call_created": "Chamada Criada", - "voicemail": "Correio de Voz", "my_bookings": "Minhas reservas", "phone": "Telefone", "free": "Grátis", @@ -3682,8 +3632,6 @@ "user_name": "Nome do usuário", "expand_panel": "Expandir painel", "collapse_panel": "Recolher painel", - "email_verification_required": "A verificação de e-mail é necessária para este tipo de evento", - "invalid_verification_code": "Código de verificação fornecido é inválido", "you_have_one_team": "Você tem uma equipe", "consider_consolidating_one_team_org": "Considere configurar uma organização para unificar faturamento, ferramentas administrativas e análises em sua equipe.", "consider_consolidating_multi_team_org": "Considere configurar uma organização para unificar faturamento, ferramentas administrativas e análises em suas equipes.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Antes do horário de início agendado", "cancel_booking_acknowledge_no_show_fee": "Reconheço que, ao cancelar a reserva dentro de {{timeValue}} {{timeUnit}} antes do horário de início, serei cobrado pela taxa de não comparecimento de {{amount, currency}}", "contact_organizer": "Se você tiver alguma dúvida, entre em contato com o organizador.", - "booking_time_option": "Horário da reserva", - "booking_time_option_description": "Quando a reserva está agendada (início ao fim)", - "created_at_option": "Criado em", - "created_at_option_description": "Quando a reserva foi originalmente criada", - "call_details": "Detalhes da Chamada", - "call_id": "ID da Chamada", - "call_information": "Informações da Chamada", - "sentiment": "Sentimento", - "disconnect_reason": "Motivo da Desconexão", - "call_summary": "Resumo da Chamada", - "transcription": "Transcrição", - "event_details": "Detalhes do Evento", - "agent": "Agente", - "no_transcript_available": "Nenhuma transcrição disponível", - "testing_sms_workflow_info_message": "Ao testar este fluxo de trabalho, esteja ciente de que as mensagens SMS precisam ser agendadas com pelo menos 15 minutos de antecedência", - "start_from_scratch_title": "Começar do zero", - "start_from_scratch_description": "Crie seu próprio fluxo de trabalho do zero.", - "cal_ai_template_title": "Modelo Cal.ai", - "cal_ai_template_description": "Agentes de IA que agendam reuniões, enviam lembretes e fazem acompanhamento!", - "voice": "Voz", - "select_voice": "Selecionar voz", - "select_voice_for_agent": "Selecione uma voz para seu agente", - "choose_a_voice_for_your_agent": "Escolha uma voz para seu agente", - "trait": "Característica", - "voice_id": "ID da voz", - "use_voice": "Usar voz", - "current_voice": "Voz atual", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index 8c53fb5467a1d4..292a176db32781 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de e-mail e de agenda", "verify_email_email_header": "Confirme o seu endereço de e-mail", "verify_email_button": "Verificar e-mail", - "cal_ai_assistant": "Assistente", + "cal_ai_assistant": "Assistente de IA Cal", "send_cal_video_transcription_emails": "Enviar e-mails com transcrição do Cal Video", "description_send_cal_video_transcription_emails": "Enviar e-mails com a transcrição do Cal Video após o término da reunião. (Requer um plano pago)", "verify_email_change_description": "Solicitou recentemente a alteração do endereço de e-mail que utiliza para entrar na sua conta {{appName}}. Clique no botão abaixo para confirmar o seu novo endereço de e-mail.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Falha na validação do fluxo de trabalho", "workflow_validation_empty_fields": "Uma ou mais etapas do fluxo de trabalho têm conteúdo de mensagem vazio", "workflow_validation_unverified_contacts": "Um ou mais números de telefone ou endereços de e-mail não estão verificados", - "supercharge_your_workflows_with_cal_ai": "Potencialize seus fluxos de trabalho com Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Agentes de IA realistas que marcam reuniões, enviam lembretes e fazem acompanhamento com seus clientes.", "phone_number_imported_successfully": "Número de telefone importado e vinculado ao agente com sucesso", "phone_number_deleted_successfully": "Número de telefone excluído com sucesso", "delete_phone_number_confirmation": "Tem certeza de que deseja excluir este número de telefone? Esta ação não pode ser desfeita.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Assinatura do número de telefone cancelada com sucesso", "updating": "Atualizando", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Olá, como está?", "round_robin_description": "Reuniões de ciclo entre vários membros da equipa.", "managed_event": "Evento gerido", "username_placeholder": "nome-de-utilizador", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Tem certeza que deseja eliminar esta etapa do fluxo de trabalho?", "do_you_still_want_to_unsubscribe": "Ainda deseja cancelar a assinatura do número de telefone deste agente?", "the_action_will_disconnect_phone_number": "Esta ação desconectará o número de telefone do agente. O agente não poderá fazer chamadas até que um novo número de telefone seja conectado.", - "cal_ai_phone_numbers": "Números de telefone", + "cal_ai_phone_numbers": "Números de telefone Cal AI", "connect_phone_number": "Conectar número de telefone", - "cal_ai_phone_numbers_description": "Gerencie seus números de telefone", + "cal_ai_phone_numbers_description": "Gerencie seus números de telefone Cal AI", "import_number": "Importar número", "this_action_will_also": "Esta ação também irá:", "import_phone_number": "Importar número de telefone", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Sim, cancelar subscrição", "cancel_phone_number_subscription_confirmation": "Tem certeza que deseja cancelar esta subscrição de número de telefone? Esta ação não pode ser desfeita e você perderá o acesso a este número de telefone.", "add_members": "Adicionar membros...", - "add_members_no_ellipsis": "Adicionar membros", "no_assigned_members": "Sem membros atribuídos", "assigned_to": "Atribuído a", "you_must_be_logged_in_to": "Deve ter sessão iniciada para {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Categorias", "pricing": "Preços", "learn_more": "Saiba mais", - "try_now": "Experimente agora", "privacy_policy": "Política de privacidade", "terms_of_service": "Termos do serviço", "remove": "Remover", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "enviar Whatsapp ao participante", "workflows": "Fluxos de trabalho", "new_workflow_btn": "Novo fluxo de trabalho", - "how_would_you_like_to_start": "Como gostaria de começar?", "add_new_workflow": "Adicionar novo fluxo de trabalho", "reschedule_event_trigger": "quando o evento for reagendado", "trigger": "Causador", @@ -1722,8 +1716,6 @@ "event_duration_info": "A duração do evento", "event_time_info": "A hora de início do evento", "event_type_not_found": "Tipo de evento não encontrado", - "number_to_call_variable": "Número para ligar", - "number_to_call_info": "O número de telefone do usuário que você está chamando", "location_variable": "Localização", "location_info": "O local do evento", "additional_notes_variable": "Notas adicionais", @@ -1761,7 +1753,6 @@ "team_url": "Endereço da equipa", "team_members": "Membros da equipa", "more": "Mais", - "cal_ai_workflows": "Fluxos de trabalho Cal.ai", "and_count_more": "e mais {{count}}", "more_page_footer": "Vemos a aplicação móvel como uma extensão da aplicação web. Se estiver a realizar quaisquer ações complexas, consulte a aplicação web.", "workflow_example_1": "Enviar lembrete de participação via SMS, 24 horas antes do evento começar", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Enviar lembrete de participação por e-mail 1 hora antes do evento começar", "workflow_example_5": "Enviar um e-mail personalizado para o participante quando o evento for reagendado", "workflow_example_6": "Enviar um SMS personalizado para o anfitrião quando um novo evento é agendado", - "send_sms_reminder": "Enviar lembrete por SMS", - "send_sms_reminder_description": "24 horas antes do início do evento", - "follow_up_with_no_shows": "Acompanhamento de ausências", - "follow_up_with_no_shows_description": "30 minutos após o término do evento", - "remind_attendees_to_bring_id": "Lembrar participantes de trazer identificação", - "remind_attendees_to_bring_id_description": "1 dia antes do início do evento", - "email_to_remind_booking": "Lembrete por email", - "email_to_remind_booking_description": "1 hora antes do início do evento", - "custom_sms_reminder": "Lembrete SMS personalizado", - "custom_sms_reminder_description": "Quando o evento é agendado", - "custom_email_reminder": "Lembrete de email personalizado", - "custom_email_reminder_description": "Evento é reagendado para o anfitrião", "count_managed_to_limit": "Incluir contagens de reservas dos tipos de eventos geridos", "welcome_to_cal_header": "Bem-vindo(a) ao {{appName}}!", "edit_form_later_subtitle": "Você poderá editar isso mais tarde.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Mostrar na página de reservas", "visit_cancelled_booking": "Pode visitar a página da reserva cancelada", "get_started_zapier_templates": "Comece a utilizar os modelos Zapier", - "standard_templates": "Modelos padrão", - "cal_ai_templates": "Modelos Cal.ai", "team_is_unpublished": "A equipa {{team}} ainda não está publicada", "org_is_unpublished_description": "Esta ligação da organização não está atualmente disponível. Por favor, entre em contacto com os responsáveis pela organização ou solicite aos mesmos a respetiva publicação.", "team_is_unpublished_description": "Esta ligação de {{entity}} não está disponível neste momento. Por favor, entre em contacto com o responsável por {{entity}} ou solicite-lhe a respetiva publicação.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Não foi possível cobrar o cartão para pagamento.", "insights": "Insights", "routing_forms": "Formulários de Roteamento", + "testing_workflow_info_message": "Ao testar este fluxo de trabalho tenha em consideração que e-mails e SMS só podem ser agendados com, pelo menos, 1 hora de antecedência", "insights_no_data_found_for_filter": "Não foram encontrados dados para as datas ou filtros selecionados.", "acknowledge_booking_no_show_fee": "Eu aceito que caso eu não compareça a este evento, será cobrada uma taxa de não-comparência de {{amount, currency}} no meu cartão.", "days": "dias", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Equipa: {{teamName}}", "insights_user_filter": "Utilizador: {{userName}}", "insights_subtitle": "Ver informações sobre reservas nos seus eventos", - "call_history": "Histórico de chamadas", - "call_history_subtitle": "Visualize o histórico de chamadas em todas as suas chamadas Cal.ai", "location_options": "{{locationCount}} opções de localização", - "channel_type": "Tipo de canal", - "end_reason": "Motivo de encerramento", - "session_status": "Status da sessão", - "user_sentiment": "Sentimento do usuário", - "time_header": "Hora", - "from_header": "De", "custom_plan": "Plano personalizado", "email_embed": "Incorporação de e-mail", "add_times_to_your_email": "Selecione alguns horários disponíveis e incorpore-os no seu E-mail", @@ -2782,8 +2752,6 @@ "account_already_linked": "A conta já está vinculada", "send_email": "Enviar e-mail", "cal_ai_phone_call_action": "Ligar para o participante usando o Agente de Voz Cal.ai", - "call_to_confirm_booking": "Ligar para confirmar reserva", - "cal_ai_phone_call_action_description": "2 horas antes do início do evento", "cal_ai_agent_configuration": "Configuração do agente Cal.ai", "choose_at_least_one_event_type_test_call": "Por favor, escolha pelo menos um tipo de evento para fazer uma chamada de teste.", "mark_as_no_show": "Marcar como falta de comparência", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verificar alteração de e-mail", "buy_credits": "Comprar créditos", "credits": "Créditos", - "credits_used": "Créditos utilizados", - "total_credits_remaining": "Total restante", - "credits_per_tip_org": "Você recebe 1000 créditos por mês, por membro da equipe", - "credits_per_tip_teams": "Você recebe 750 créditos por mês, por membro da equipe", - "view_and_manage_credits": "Visualizar e gerenciar créditos para envio de mensagens SMS", + "view_and_manage_credits": "Visualizar e gerenciar créditos", "view_and_manage_credits_description": "Visualize e gerencie créditos para envio de mensagens SMS. Um crédito vale 1¢ (USD). <0>Saiba mais", - "credit_worth_description": "Um crédito vale 1¢ (USD). <0>Saiba mais", "buy_additional_credits": "Comprar créditos adicionais ($0.01 por crédito)", - "view_additional_credits_expense_tip": "Você pode visualizar gastos de créditos adicionais no seu registro de despesas", "overview": "Visão geral", "organization_slug_taken": "O slug da organização já está em uso", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Você não pode criar uma organização pois já faz parte de uma", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Sua equipe Cal.com {{teamName}} ficou sem créditos. Como resultado, as mensagens SMS agora estão sendo enviadas por email. Para retomar o envio de SMS, por favor compre créditos adicionais.", "credit_limit_reached_message_user": "Sua conta Cal.com ficou sem créditos. Como resultado, as mensagens SMS agora estão sendo enviadas por e-mail. Para retomar o envio de SMS, adquira créditos adicionais.", "current_credit_balance": "Saldo atual: {{balance}} créditos", - "current_balance": "Saldo atual:", "notification_about_your_booking": "Notificação sobre sua reserva", "monthly_credits": "Créditos mensais", "total_credits": "Total de créditos: {{totalCredits}}", "remaining_credits": "Créditos restantes: {{remainingCredits}}", - "remaining": "Restante", - "total": "Total", "additional_credits": "Créditos adicionais", "routing_form_next_in_queue": "{{count}} próximo na fila", "routing_form_select_members_to_email": "Enviar respostas por e-mail para", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Visualizar fluxos de trabalho existentes e suas configurações", "pbac_desc_update_workflows": "Editar e modificar configurações de fluxo de trabalho", "pbac_desc_delete_workflows": "Remover fluxos de trabalho do sistema", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Criar webhooks", - "pbac_desc_view_webhooks": "Visualizar webhooks", - "pbac_desc_update_webhooks": "Atualizar webhooks", - "pbac_desc_delete_webhooks": "Eliminar webhooks", "pbac_desc_manage_workflows": "Acesso completo de gerenciamento a todos os fluxos de trabalho", "pbac_desc_create_event_types": "Criar tipos de evento", "pbac_desc_view_event_types": "Ver tipos de evento", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "O nome do evento de gatilho (ex., BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "A hora do webhook", "webhook_type": "O slug do tipo de evento", - "set_up_agent": "Configurar Agente", "webhook_title": "O nome do tipo de evento", "webhook_start_time": "A hora de início do evento", "webhook_end_time": "A hora de fim do evento", @@ -3672,9 +3625,6 @@ "visit": "Visitar", "location_custom_label_input_label": "Etiqueta personalizada na página de reserva", "meeting_link": "Link da reunião", - "session_outcome": "Resultado da Sessão", - "call_created": "Chamada Criada", - "voicemail": "Correio de voz", "my_bookings": "As minhas reservas", "phone": "Telefone", "free": "Grátis", @@ -3682,8 +3632,6 @@ "user_name": "Nome do utilizador", "expand_panel": "Expandir painel", "collapse_panel": "Recolher painel", - "email_verification_required": "É necessária verificação de email para este tipo de evento", - "invalid_verification_code": "Código de verificação fornecido inválido", "you_have_one_team": "Tem uma equipa", "consider_consolidating_one_team_org": "Considere configurar uma organização para unificar faturação, ferramentas de administração e análises na sua equipa.", "consider_consolidating_multi_team_org": "Considere configurar uma organização para unificar faturação, ferramentas de administração e análises nas suas equipas.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Antes do horário de início agendado", "cancel_booking_acknowledge_no_show_fee": "Reconheço que ao cancelar a reserva dentro de {{timeValue}} {{timeUnit}} antes do horário de início, serei cobrado pela taxa de não comparecimento de {{amount, currency}}", "contact_organizer": "Se tiver alguma dúvida, entre em contato com o organizador.", - "booking_time_option": "Horário da reserva", - "booking_time_option_description": "Quando a reserva está agendada (início ao fim)", - "created_at_option": "Criado em", - "created_at_option_description": "Quando a reserva foi originalmente criada", - "call_details": "Detalhes da Chamada", - "call_id": "ID da Chamada", - "call_information": "Informações da Chamada", - "sentiment": "Sentimento", - "disconnect_reason": "Motivo de Desconexão", - "call_summary": "Resumo da Chamada", - "transcription": "Transcrição", - "event_details": "Detalhes do Evento", - "agent": "Agente", - "no_transcript_available": "Nenhuma transcrição disponível", - "testing_sms_workflow_info_message": "Ao testar este fluxo de trabalho, lembre-se que as mensagens SMS precisam ser agendadas com pelo menos 15 minutos de antecedência", - "start_from_scratch_title": "Começar do zero", - "start_from_scratch_description": "Crie seu próprio fluxo de trabalho do zero.", - "cal_ai_template_title": "Modelo Cal.ai", - "cal_ai_template_description": "Agentes de IA que marcam reuniões, enviam lembretes e fazem acompanhamento!", - "voice": "Voz", - "select_voice": "Selecionar Voz", - "select_voice_for_agent": "Selecione uma voz para o seu agente", - "choose_a_voice_for_your_agent": "Escolha uma voz para o seu agente", - "trait": "Característica", - "voice_id": "ID da Voz", - "use_voice": "Usar Voz", - "current_voice": "Voz Atual", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index 96a83527e8f1ae..946a892626ce16 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Verificați-vă adresa de e-mail pentru a garanta cea mai bună furnizare a e-mailurilor și datelor de calendar", "verify_email_email_header": "Verificați-vă adresa de e-mail", "verify_email_button": "Verifică e-mailul", - "cal_ai_assistant": "Asistent", + "cal_ai_assistant": "Asistentul AI Cal", "send_cal_video_transcription_emails": "Trimite e-mailuri cu transcrierea video Cal", "description_send_cal_video_transcription_emails": "Trimite e-mailuri cu transcrierea înregistrării video Cal după încheierea întâlnirii. (Necesită un abonament plătit)", "verify_email_change_description": "Ai solicitat recent schimbarea adresei de email pe care o folosești pentru a te conecta la contul tău {{appName}}. Te rugăm să faci clic pe butonul de mai jos pentru a confirma noua adresă de email.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Validarea fluxului de lucru a eșuat", "workflow_validation_empty_fields": "Unul sau mai mulți pași ai fluxului de lucru au conținut de mesaj gol", "workflow_validation_unverified_contacts": "Unul sau mai multe numere de telefon sau adrese de e-mail nu sunt verificate", - "supercharge_your_workflows_with_cal_ai": "Îmbunătățiți fluxurile de lucru cu Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Agenți AI realiști care programează întâlniri, trimit mementouri și urmăresc clienții dvs.", "phone_number_imported_successfully": "Numărul de telefon a fost importat și asociat cu agentul cu succes", "phone_number_deleted_successfully": "Numărul de telefon a fost șters cu succes", "delete_phone_number_confirmation": "Sigur doriți să ștergeți acest număr de telefon? Această acțiune nu poate fi anulată.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Abonamentul pentru numărul de telefon a fost anulat cu succes", "updating": "Se actualizează", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Bună, ce mai faci?", "round_robin_description": "Întâlniri ciclice între mai mulţi membri ai echipei.", "managed_event": "Eveniment configurat", "username_placeholder": "nume de utilizator", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Sigur doriți să ștergeți acest pas al fluxului de lucru?", "do_you_still_want_to_unsubscribe": "Doriți în continuare să dezabonați numărul de telefon de la acest agent?", "the_action_will_disconnect_phone_number": "Această acțiune va deconecta numărul de telefon de la agent. Agentul nu va putea efectua apeluri până când nu va fi conectat un nou număr de telefon.", - "cal_ai_phone_numbers": "Numere de telefon", + "cal_ai_phone_numbers": "Numere de telefon Cal AI", "connect_phone_number": "Conectați numărul de telefon", - "cal_ai_phone_numbers_description": "Gestionează numerele de telefon", + "cal_ai_phone_numbers_description": "Gestionați numerele de telefon Cal AI", "import_number": "Importați numărul", "this_action_will_also": "Această acțiune va include și:", "import_phone_number": "Importați numărul de telefon", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Da, anulează abonamentul", "cancel_phone_number_subscription_confirmation": "Sigur doriți să anulați acest abonament pentru numărul de telefon? Această acțiune nu poate fi anulată și veți pierde accesul la acest număr de telefon.", "add_members": "Adăugare membri...", - "add_members_no_ellipsis": "Adaugă membri", "no_assigned_members": "Niciun membru alocat", "assigned_to": "Alocat la", "you_must_be_logged_in_to": "Trebuie să fii autentificat pentru a accesa {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Categorii", "pricing": "Prețuri", "learn_more": "Aflați mai multe", - "try_now": "Încercați acum", "privacy_policy": "Politica de confidențialitate", "terms_of_service": "Condiții de utilizare", "remove": "Eliminați", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "trimiteți mesaj WhatsApp căte participant", "workflows": "Fluxuri de lucru", "new_workflow_btn": "Flux de lucru nou", - "how_would_you_like_to_start": "Cum ați dori să începeți?", "add_new_workflow": "Adăugați un flux de lucru nou", "reschedule_event_trigger": "atunci când evenimentul este reprogramat", "trigger": "Declanșator", @@ -1722,8 +1716,6 @@ "event_duration_info": "Durata evenimentului", "event_time_info": "Ora începerii evenimentului", "event_type_not_found": "Tipul de eveniment nu a fost găsit", - "number_to_call_variable": "Număr de apelat", - "number_to_call_info": "Numărul de telefon al utilizatorului pe care îl apelați", "location_variable": "Locație", "location_info": "Locul de desfășurare a evenimentului", "additional_notes_variable": "Note suplimentare", @@ -1761,7 +1753,6 @@ "team_url": "URL-ul echipei", "team_members": "Membrii echipei", "more": "Mai mult", - "cal_ai_workflows": "Fluxuri de lucru Cal.ai", "and_count_more": "și încă {{count}}", "more_page_footer": "Considerăm că aplicația mobilă este o extensie a aplicației web. Dacă efectuați orice acțiuni complicate, reveniți la aplicația web.", "workflow_example_1": "Trimiteți participantului un memento prin SMS cu 24 de ore înainte de începerea evenimentului", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Trimiteți participantului un memento prin e-mail cu 1 oră înainte de începerea evenimentului", "workflow_example_5": "Trimiteți gazdei un e-mail personalizat atunci când evenimentul nou este reprogramat", "workflow_example_6": "Trimiteți gazdei un SMS personalizat atunci când evenimentul nou este rezervat", - "send_sms_reminder": "Trimite memento SMS", - "send_sms_reminder_description": "Cu 24 de ore înainte de începerea evenimentului", - "follow_up_with_no_shows": "Urmărire pentru absenți", - "follow_up_with_no_shows_description": "30 de minute după terminarea evenimentului", - "remind_attendees_to_bring_id": "Reamintiți participanților să aducă un act de identitate", - "remind_attendees_to_bring_id_description": "Cu 1 zi înainte de începerea evenimentului", - "email_to_remind_booking": "Memento prin e-mail", - "email_to_remind_booking_description": "Cu 1 oră înainte de începerea evenimentului", - "custom_sms_reminder": "Memento SMS personalizat", - "custom_sms_reminder_description": "Când evenimentul este programat", - "custom_email_reminder": "Memento e-mail personalizat", - "custom_email_reminder_description": "Evenimentul este reprogramat pentru gazdă", "count_managed_to_limit": "Include numărul de rezervări din tipurile de evenimente gestionate", "welcome_to_cal_header": "Bun venit pe {{appName}}!", "edit_form_later_subtitle": "Vei putea edita acest lucru mai târziu.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Afișare pe pagina de rezervare", "visit_cancelled_booking": "Puteți vizita pagina rezervării anulate", "get_started_zapier_templates": "Faceți primii pași cu șabloanele Zapier", - "standard_templates": "Șabloane standard", - "cal_ai_templates": "Șabloane Cal.ai", "team_is_unpublished": "Echipa {{team}} nu este publicată", "org_is_unpublished_description": "Acest link de organizație nu este disponibil momentan. Contactați proprietarul organizației sau rugați-l să îl publice.", "team_is_unpublished_description": "Acest link de {{entity}} nu este disponibil momentan. Contactați proprietarul {{entity}} sau rugați-l să îl publice.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Nu s-a putut debita cardul pentru plată.", "insights": "Informații", "routing_forms": "Formulare de Rutare", + "testing_workflow_info_message": "La testarea acestui flux de lucru, țineți cont de faptul că e-mailurile și SMS-urile pot fi programate doar cu cel puțin o oră în prealabil", "insights_no_data_found_for_filter": "Nu s-au găsit date pentru filtrul selectat sau pentru datele selectate.", "acknowledge_booking_no_show_fee": "Dacă nu particip la acest eveniment, accept să mi se perceapă de pe card o taxă de neprezentare în valoare de {{amount, currency}}.", "days": "zile", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Echipă: {{teamName}}", "insights_user_filter": "Utilizator: {{userName}}", "insights_subtitle": "Vizualizați date Insights cu privire la rezervări pentru toate evenimentele dvs.", - "call_history": "Istoric apeluri", - "call_history_subtitle": "Vizualizează istoricul apelurilor din Cal.ai", "location_options": "{{locationCount}} (de) opțiuni privind locul", - "channel_type": "Tip canal", - "end_reason": "Motiv încheiere", - "session_status": "Stare sesiune", - "user_sentiment": "Sentiment utilizator", - "time_header": "Oră", - "from_header": "De la", "custom_plan": "Plan personalizat", "email_embed": "Încorporare în e-mail", "add_times_to_your_email": "Selectați câteva date disponibile și încorporați-le în e-mailul dvs.", @@ -2782,8 +2752,6 @@ "account_already_linked": "Contul este deja conectat", "send_email": "Trimiteți un e-mail", "cal_ai_phone_call_action": "Sună participantul folosind Cal.ai Voice Agent", - "call_to_confirm_booking": "Apel pentru confirmarea rezervării", - "cal_ai_phone_call_action_description": "Cu 2 ore înainte de începerea evenimentului", "cal_ai_agent_configuration": "Configurare agent Cal.ai", "choose_at_least_one_event_type_test_call": "Vă rugăm să alegeți cel puțin un tip de eveniment pentru a efectua un apel de test.", "mark_as_no_show": "Marchează ca neprezentat", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verifică schimbarea adresei de email", "buy_credits": "Cumpără credite", "credits": "Credite", - "credits_used": "Credite utilizate", - "total_credits_remaining": "Total rămas", - "credits_per_tip_org": "Primești 1000 de credite pe lună, per membru al echipei", - "credits_per_tip_teams": "Primești 750 de credite pe lună, per membru al echipei", - "view_and_manage_credits": "Vizualizează și gestionează creditele pentru trimiterea mesajelor SMS", + "view_and_manage_credits": "Vizualizează și gestionează creditele", "view_and_manage_credits_description": "Vizualizează și gestionează creditele pentru trimiterea mesajelor SMS. Un credit valorează 1¢ (USD). <0>Află mai multe", - "credit_worth_description": "Un credit valorează 1¢ (USD). <0>Află mai multe", "buy_additional_credits": "Cumpără credite suplimentare (0,01 USD per credit)", - "view_additional_credits_expense_tip": "Poți vizualiza cheltuielile suplimentare cu credite în jurnalul tău de cheltuieli", "overview": "Prezentare generală", "organization_slug_taken": "Slug-ul organizației este deja folosit", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Nu puteți crea o organizație deoarece faceți deja parte dintr-o organizație", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Echipa ta Cal.com {{teamName}} a rămas fără credite. Drept urmare, mesajele SMS sunt acum trimise prin e-mail. Pentru a relua trimiterea SMS-urilor, te rugăm să achiziționezi credite suplimentare.", "credit_limit_reached_message_user": "Contul tău Cal.com a rămas fără credite. Drept urmare, mesajele SMS sunt acum trimise prin e-mail. Pentru a relua trimiterea SMS-urilor, te rugăm să achiziționezi credite suplimentare.", "current_credit_balance": "Sold curent: {{balance}} credite", - "current_balance": "Sold curent:", "notification_about_your_booking": "Notificare despre rezervarea ta", "monthly_credits": "Credite lunare", "total_credits": "Total credite: {{totalCredits}}", "remaining_credits": "Credite rămase: {{remainingCredits}}", - "remaining": "Rămas", - "total": "Total", "additional_credits": "Credite suplimentare", "routing_form_next_in_queue": "{{count}} următor în coadă", "routing_form_select_members_to_email": "Trimiteți răspunsurile prin e-mail către", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Vizualizați fluxurile de lucru existente și configurațiile acestora", "pbac_desc_update_workflows": "Editați și modificați setările fluxurilor de lucru", "pbac_desc_delete_workflows": "Eliminați fluxurile de lucru din sistem", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Creează webhooks", - "pbac_desc_view_webhooks": "Vizualizează webhooks", - "pbac_desc_update_webhooks": "Actualizează webhooks", - "pbac_desc_delete_webhooks": "Șterge webhooks", "pbac_desc_manage_workflows": "Acces complet de gestionare pentru toate fluxurile de lucru", "pbac_desc_create_event_types": "Creați tipuri de evenimente", "pbac_desc_view_event_types": "Vizualizați tipuri de evenimente", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Numele evenimentului declanșator (de exemplu, BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Ora webhook-ului", "webhook_type": "Slug-ul tipului de eveniment", - "set_up_agent": "Configurează agentul", "webhook_title": "Numele tipului de eveniment", "webhook_start_time": "Ora de început a evenimentului", "webhook_end_time": "Ora de sfârșit a evenimentului", @@ -3672,9 +3625,6 @@ "visit": "Vizită", "location_custom_label_input_label": "Etichetă personalizată pe pagina de rezervare", "meeting_link": "Link întâlnire", - "session_outcome": "Rezultatul sesiuni", - "call_created": "Apel creat", - "voicemail": "Mesaj vocal", "my_bookings": "Rezervările mele", "phone": "Telefon", "free": "Gratuit", @@ -3682,8 +3632,6 @@ "user_name": "Nume utilizator", "expand_panel": "Extinde panoul", "collapse_panel": "Restrânge panoul", - "email_verification_required": "Verificarea emailului este necesară pentru acest tip de eveniment", - "invalid_verification_code": "Cod de verificare invalid furnizat", "you_have_one_team": "Aveți o echipă", "consider_consolidating_one_team_org": "Luați în considerare configurarea unei organizații pentru a unifica facturarea, instrumentele de administrare și analizele pentru echipa dvs.", "consider_consolidating_multi_team_org": "Luați în considerare configurarea unei organizații pentru a unifica facturarea, instrumentele de administrare și analizele pentru echipele dvs.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Înainte de ora programată de început", "cancel_booking_acknowledge_no_show_fee": "Recunosc că, anulând rezervarea cu {{timeValue}} {{timeUnit}} înainte de ora de început, voi fi taxat cu taxa de neprezentare de {{amount, currency}}", "contact_organizer": "Dacă aveți întrebări, vă rugăm să contactați organizatorul.", - "booking_time_option": "Ora rezervării", - "booking_time_option_description": "Când este programată rezervarea (de la început până la sfârșit)", - "created_at_option": "Creat la", - "created_at_option_description": "Când a fost creată inițial rezervarea", - "call_details": "Detalii apel", - "call_id": "ID apel", - "call_information": "Informații despre apel", - "sentiment": "Sentiment", - "disconnect_reason": "Motivul deconectării", - "call_summary": "Rezumatul apelului", - "transcription": "Transcriere", - "event_details": "Detalii eveniment", - "agent": "Agent", - "no_transcript_available": "Nu există nicio transcriere disponibilă", - "testing_sms_workflow_info_message": "Când testați acest flux de lucru, rețineți că SMS-urile trebuie programate cu cel puțin 15 minute în avans", - "start_from_scratch_title": "Începe de la zero", - "start_from_scratch_description": "Creează propriul flux de lucru de la zero.", - "cal_ai_template_title": "Șablon Cal.ai", - "cal_ai_template_description": "Agenți AI care programează întâlniri, trimit mementouri și urmăresc progresul!", - "voice": "Voce", - "select_voice": "Selectează vocea", - "select_voice_for_agent": "Selectează o voce pentru agentul tău", - "choose_a_voice_for_your_agent": "Alege o voce pentru agentul tău", - "trait": "Caracteristică", - "voice_id": "ID voce", - "use_voice": "Folosește vocea", - "current_voice": "Vocea curentă", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index 42accea08c8348..ff7b455d2ccfb8 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -819,8 +819,6 @@ "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": "Вы уверены, что хотите удалить этот номер телефона? Это действие нельзя будет отменить.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Подписка на номер телефона успешно отменена", "updating": "Обновление", "round_robin": "По кругу", - "hi_how_are_you_doing": "Привет, как дела?", "round_robin_description": "Цикл встреч между несколькими членами команды.", "managed_event": "Управляемое событие", "username_placeholder": "имя пользователя", @@ -879,9 +876,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_phone_numbers": "Номера телефонов Cal AI", "connect_phone_number": "Подключить номер телефона", - "cal_ai_phone_numbers_description": "Управляйте своими номерами телефонов", + "cal_ai_phone_numbers_description": "Управляйте своими номерами телефонов Cal AI", "import_number": "Импортировать номер", "this_action_will_also": "Это действие также:", "import_phone_number": "Импортировать номер телефона", @@ -889,7 +886,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", "phone_number_cost": "${{price}}/месяц", "buy_new_number": "Купить новый номер", "buy_number_cost_x_per_month": "Покупка номера телефона стоит ${{priceInDollars}} в месяц. С вас будет ежемесячно взиматься плата за каждый активный номер телефона.", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "Категории", "pricing": "Цены", "learn_more": "Подробнее", - "try_now": "Попробовать", "privacy_policy": "Политика конфиденциальности", "terms_of_service": "Условия использования", "remove": "Удалить", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "отправить участнику сообщение в Whatsapp", "workflows": "Рабочие процессы", "new_workflow_btn": "Новый рабочий процесс", - "how_would_you_like_to_start": "С чего вы хотите начать?", "add_new_workflow": "Добавить новый рабочий процесс", "reschedule_event_trigger": "при переносе события", "trigger": "Триггер", @@ -1722,8 +1716,6 @@ "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": "Дополнительная информация", @@ -1761,7 +1753,6 @@ "team_url": "URL-адрес команды", "team_members": "Участники команды", "more": "Еще", - "cal_ai_workflows": "Рабочие процессы Cal.ai", "and_count_more": "и еще {{count}}", "more_page_footer": "Наше мобильное приложение — это скорее дополнение к веб-приложению. Сложные действия лучше делать в веб-приложении.", "workflow_example_1": "Отправить участникам напоминание по SMS за 24 часа до начала события", @@ -1770,18 +1761,6 @@ "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": "Вы сможете отредактировать это позже.", @@ -2205,8 +2184,6 @@ "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}} и попросите его опубликовать ее.", @@ -2364,6 +2341,7 @@ "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": "дни", @@ -2486,15 +2464,7 @@ "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": "Выберите доступные промежутки времени и вставьте их в письмо", @@ -2782,8 +2752,6 @@ "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": "Отметить как неявку", @@ -3235,15 +3203,9 @@ "verify_email_change": "Подтвердить изменение email", "buy_credits": "Купить кредиты", "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": "Просмотр и управление кредитами", "view_and_manage_credits_description": "Просматривайте и управляйте кредитами для отправки SMS-сообщений. Один кредит стоит 1¢ (USD). <0>Узнать больше", - "credit_worth_description": "Один кредит стоит 1¢ (USD). <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": "Вы не можете создать организацию, так как уже являетесь частью организации", @@ -3424,13 +3386,10 @@ "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": "Отправить ответы по электронной почте", @@ -3508,11 +3467,6 @@ "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": "Просмотр типов событий", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Название события-триггера (например, BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Время создания вебхука", "webhook_type": "Слаг типа события", - "set_up_agent": "Настроить агента", "webhook_title": "Название типа события", "webhook_start_time": "Время начала события", "webhook_end_time": "Время окончания события", @@ -3672,9 +3625,6 @@ "visit": "Посетить", "location_custom_label_input_label": "Пользовательская метка на странице бронирования", "meeting_link": "Ссылка на встречу", - "session_outcome": "Результат сессии", - "call_created": "Звонок создан", - "voicemail": "Голосовая почта", "my_bookings": "Мои бронирования", "phone": "Телефон", "free": "Бесплатно", @@ -3682,8 +3632,6 @@ "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": "Рассмотрите возможность создания организации для объединения выставления счетов, административных инструментов и аналитики в ваших командах.", @@ -3704,32 +3652,5 @@ "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": "ИИ-агенты, которые бронируют встречи, отправляют напоминания и делают последующие рассылки!", - "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/sk-SK/common.json b/apps/web/public/static/locales/sk-SK/common.json index 36c3fe7aba4d17..f95d1fc0059814 100644 --- a/apps/web/public/static/locales/sk-SK/common.json +++ b/apps/web/public/static/locales/sk-SK/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Overte svoju emailovú adresu, aby ste zaručili najlepšiu doručiteľnosť emailov a kalendára", "verify_email_email_header": "Overte svoju emailovú adresu", "verify_email_button": "Overiť email", - "cal_ai_assistant": "Asistent", + "cal_ai_assistant": "asistent", "send_cal_video_transcription_emails": "Posielať prepisy Cal Video e-mailom", "description_send_cal_video_transcription_emails": "Posielať e-maily s prepisom Cal Video po skončení stretnutia. (Vyžaduje platený plán)", "verify_email_change_description": "Nedávno ste požiadali o zmenu emailovej adresy, ktorú používate na prihlásenie do svojho účtu {{appName}}. Kliknite na tlačidlo nižšie, aby ste potvrdili svoju novú emailovú adresu.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Overenie pracovného postupu zlyhalo", "workflow_validation_empty_fields": "Jeden alebo viac krokov pracovného postupu má prázdny obsah správy", "workflow_validation_unverified_contacts": "Jedno alebo viac telefónnych čísel alebo e-mailových adries nie je overených", - "supercharge_your_workflows_with_cal_ai": "Posilnite svoje pracovné postupy s Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Realistickí AI agenti, ktorí rezervujú stretnutia, posielajú pripomienky a komunikujú s vašimi zákazníkmi.", "phone_number_imported_successfully": "Telefónne číslo bolo úspešne importované a prepojené s agentom", "phone_number_deleted_successfully": "Telefónne číslo bolo úspešne odstránené", "delete_phone_number_confirmation": "Naozaj chcete odstrániť toto telefónne číslo? Túto akciu nemožno vrátiť späť.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Predplatné telefónneho čísla bolo úspešne zrušené", "updating": "Aktualizuje sa", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Ahoj, ako sa máš?", "round_robin_description": "Cyklické striedanie stretnutí medzi viacerými členmi tímu.", "managed_event": "Spravovaná udalosť", "username_placeholder": "používateľské meno", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Ste si istí, že chcete odstrániť tento krok pracovného postupu?", "do_you_still_want_to_unsubscribe": "Stále chcete odhlásiť telefónne číslo od tohto agenta?", "the_action_will_disconnect_phone_number": "Táto akcia odpojí telefónne číslo od agenta. Agent nebude môcť uskutočňovať hovory, kým nebude pripojené nové telefónne číslo.", - "cal_ai_phone_numbers": "Telefónne čísla", + "cal_ai_phone_numbers": "Telefónne čísla Cal AI", "connect_phone_number": "Pripojiť telefónne číslo", - "cal_ai_phone_numbers_description": "Spravujte svoje telefónne čísla", + "cal_ai_phone_numbers_description": "Spravujte svoje telefónne čísla Cal AI", "import_number": "Importovať číslo", "this_action_will_also": "Táto akcia tiež:", "import_phone_number": "Importovať telefónne číslo", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Zrušiť predplatné vášho telefónneho čísla", "delete_associated_phone_number": "Odstrániť priradené telefónne číslo", "unauthorized_create_workflow": "Nemáte oprávnenie vytvoriť tento pracovný postup", - "import_phone_number_description": "Importujte svoje telefónne číslo z Twilio na použitie s telefónom", + "import_phone_number_description": "Importujte svoje telefónne číslo Twilio na použitie s Phone", "phone_number_cost": "${{price}}/mesiac", "buy_new_number": "Kúpiť nové číslo", "buy_number_cost_x_per_month": "Kúpa telefónneho čísla stojí ${{priceInDollars}} mesačne. Za každé aktívne telefónne číslo vám bude účtovaný mesačný poplatok.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Áno, zrušiť predplatné", "cancel_phone_number_subscription_confirmation": "Ste si istí, že chcete zrušiť toto predplatné telefónneho čísla? Túto akciu nie je možné vrátiť späť a stratíte prístup k tomuto telefónnemu číslu.", "add_members": "Pridať členov...", - "add_members_no_ellipsis": "Pridať členov", "no_assigned_members": "Žiadni pridelení členovia", "assigned_to": "Pridelené", "you_must_be_logged_in_to": "Musíte byť prihlásený do {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategórie", "pricing": "Ceny", "learn_more": "Zistiť viac", - "try_now": "Vyskúšajte teraz", "privacy_policy": "Zásady ochrany osobných údajov", "terms_of_service": "Podmienky služby", "remove": "Odstrániť", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "poslať správu cez WhatsApp účastníkovi", "workflows": "Pracovné postupy", "new_workflow_btn": "Nový pracovný postup", - "how_would_you_like_to_start": "Ako by ste chceli začať?", "add_new_workflow": "Pridať nový pracovný postup", "reschedule_event_trigger": "keď je udalosť preplánovaná", "trigger": "Spúšťač", @@ -1722,8 +1716,6 @@ "event_duration_info": "Trvanie udalosti", "event_time_info": "Čas začiatku udalosti", "event_type_not_found": "Typ udalosti sa nenašiel", - "number_to_call_variable": "Číslo na volanie", - "number_to_call_info": "Telefónne číslo používateľa, ktorému voláte", "location_variable": "Miesto", "location_info": "Miesto konania udalosti", "additional_notes_variable": "Ďalšie poznámky", @@ -1761,7 +1753,6 @@ "team_url": "URL tímu", "team_members": "Členovia tímu", "more": "Viac", - "cal_ai_workflows": "Cal.ai pracovné postupy", "and_count_more": "a ďalších {{count}}", "more_page_footer": "Mobilnú aplikáciu vnímame ako rozšírenie webovej aplikácie. Ak vykonávate zložitejšie úkony, vráťte sa prosím k webovej aplikácii.", "workflow_example_1": "Poslať SMS pripomienku účastníkovi 24 hodín pred začiatkom udalosti", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Poslať e-mailovú pripomienku účastníkovi 1 hodinu pred začiatkom udalosti", "workflow_example_5": "Poslať vlastný e-mail hostiteľovi pri preložení udalosti", "workflow_example_6": "Poslať vlastnú SMS hostiteľovi pri rezervácii novej udalosti", - "send_sms_reminder": "Poslať SMS pripomienku", - "send_sms_reminder_description": "24 hodín pred začiatkom udalosti", - "follow_up_with_no_shows": "Sledovať neúčasť", - "follow_up_with_no_shows_description": "30 minút po skončení udalosti", - "remind_attendees_to_bring_id": "Pripomenúť účastníkom, aby si priniesli doklad totožnosti", - "remind_attendees_to_bring_id_description": "1 deň pred začiatkom udalosti", - "email_to_remind_booking": "E-mailová pripomienka", - "email_to_remind_booking_description": "1 hodinu pred začiatkom udalosti", - "custom_sms_reminder": "Vlastná SMS pripomienka", - "custom_sms_reminder_description": "Keď je udalosť naplánovaná", - "custom_email_reminder": "Vlastná e-mailová pripomienka", - "custom_email_reminder_description": "Udalosť je preplánovaná pre hostiteľa", "count_managed_to_limit": "Zahrnúť počty rezervácií z riadených typov udalostí", "welcome_to_cal_header": "Vitajte v {{appName}}!", "edit_form_later_subtitle": "Toto budete môcť upraviť neskôr.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Zobraziť na stránke rezervácie", "visit_cancelled_booking": "Môžete navštíviť stránku zrušenej rezervácie", "get_started_zapier_templates": "Začnite so šablónami Zapier", - "standard_templates": "Štandardné šablóny", - "cal_ai_templates": "Cal.ai šablóny", "team_is_unpublished": "{{team}} nie je publikovaný", "org_is_unpublished_description": "Tento odkaz na organizáciu momentálne nie je dostupný. Kontaktujte prosím vlastníka organizácie alebo ho požiadajte o publikovanie.", "team_is_unpublished_description": "Tento odkaz tímu momentálne nie je dostupný. Kontaktujte prosím vlastníka tímu alebo ho požiadajte o publikovanie.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Nepodarilo sa zaúčtovať platbu na kartu.", "insights": "Prehľady", "routing_forms": "Smerovacie formuláre", + "testing_workflow_info_message": "Pri testovaní tohto pracovného postupu majte na pamäti, že e-maily a SMS môžu byť naplánované iba minimálne 1 hodinu vopred.", "insights_no_data_found_for_filter": "Pre vybraný filter alebo vybrané dátumy neboli nájdené žiadne údaje.", "acknowledge_booking_no_show_fee": "Potvrdzujem, že ak sa nezúčastním tejto udalosti, bude na moju kartu uplatnený poplatok za neúčasť vo výške {{amount, currency}}.", "days": "dni", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Tím: {{teamName}}", "insights_user_filter": "Používateľ: {{userName}}", "insights_subtitle": "Zobraziť prehľad rezervácií naprieč vašimi udalosťami", - "call_history": "História hovorov", - "call_history_subtitle": "Zobraziť históriu hovorov naprieč vašimi Cal.ai hovormi", "location_options": "{{locationCount}} možnosti umiestnenia", - "channel_type": "Typ kanála", - "end_reason": "Dôvod ukončenia", - "session_status": "Stav relácie", - "user_sentiment": "Sentiment používateľa", - "time_header": "Čas", - "from_header": "Od", "custom_plan": "Vlastný plán", "email_embed": "Vloženie do e-mailu", "add_times_to_your_email": "Vyberte niekoľko dostupných časov a vložte ich do svojho e-mailu", @@ -2782,8 +2752,6 @@ "account_already_linked": "Účet je už prepojený", "send_email": "Poslať email", "cal_ai_phone_call_action": "Zavolať účastníkovi pomocou hlasového agenta Cal.ai", - "call_to_confirm_booking": "Hovor na potvrdenie rezervácie", - "cal_ai_phone_call_action_description": "2 hodiny pred začiatkom udalosti", "cal_ai_agent_configuration": "Konfigurácia agenta Cal.ai", "choose_at_least_one_event_type_test_call": "Vyberte prosím aspoň jeden typ udalosti na vykonanie testovacieho hovoru.", "mark_as_no_show": "Označiť ako nedostavil/a sa", @@ -3235,15 +3203,9 @@ "verify_email_change": "Overiť zmenu e-mailu", "buy_credits": "Kúpiť kredity", "credits": "Kredity", - "credits_used": "Použité kredity", - "total_credits_remaining": "Zostávajúce celkom", - "credits_per_tip_org": "Dostávate 1000 kreditov mesačne na každého člena tímu", - "credits_per_tip_teams": "Dostávate 750 kreditov mesačne na každého člena tímu", - "view_and_manage_credits": "Zobraziť a spravovať kredity na odosielanie SMS správ", + "view_and_manage_credits": "Zobraziť a spravovať kredity", "view_and_manage_credits_description": "Zobrazte a spravujte kredity na odosielanie SMS správ. Jeden kredit má hodnotu 1¢ (USD). <0>Dozvedieť sa viac", - "credit_worth_description": "Jeden kredit má hodnotu 1¢ (USD). <0>Dozvedieť sa viac", "buy_additional_credits": "Kúpiť ďalšie kredity (0,01 $ za kredit)", - "view_additional_credits_expense_tip": "Dodatočné výdavky na kredity si môžete pozrieť v zázname výdavkov", "overview": "Prehľad", "organization_slug_taken": "Slug organizácie je už obsadený", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Nemôžete vytvoriť organizáciu, pretože už ste súčasťou organizácie", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Vášmu Cal.com tímu {{teamName}} došli kredity. V dôsledku toho sa SMS správy teraz odosielajú prostredníctvom e-mailu. Pre obnovenie odosielania SMS si zakúpte ďalšie kredity.", "credit_limit_reached_message_user": "Vášmu účtu Cal.com došli kredity. V dôsledku toho sa SMS správy teraz posielajú prostredníctvom e-mailu. Pre obnovenie posielania SMS si prosím zakúpte ďalšie kredity.", "current_credit_balance": "Aktuálny zostatok: {{balance}} kreditov", - "current_balance": "Aktuálny zostatok:", "notification_about_your_booking": "Upozornenie o vašej rezervácii", "monthly_credits": "Mesačné kredity", "total_credits": "Celkové kredity: {{totalCredits}}", "remaining_credits": "Zostávajúce kredity: {{remainingCredits}}", - "remaining": "Zostávajúce", - "total": "Celkom", "additional_credits": "Dodatočné kredity", "routing_form_next_in_queue": "{{count}} ďalších v poradí", "routing_form_select_members_to_email": "Poslať e-mailové odpovede na", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Zobraziť existujúce pracovné postupy a ich konfigurácie", "pbac_desc_update_workflows": "Upravovať a modifikovať nastavenia pracovných postupov", "pbac_desc_delete_workflows": "Odstraňovať pracovné postupy zo systému", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Vytvárať webhooky", - "pbac_desc_view_webhooks": "Zobraziť webhooky", - "pbac_desc_update_webhooks": "Aktualizovať webhoky", - "pbac_desc_delete_webhooks": "Odstrániť webhoky", "pbac_desc_manage_workflows": "Úplný prístup k správe všetkých pracovných postupov", "pbac_desc_create_event_types": "Vytvárať typy udalostí", "pbac_desc_view_event_types": "Zobraziť typy udalostí", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Názov spúšťacej udalosti (napr. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Čas vytvorenia webhooku", "webhook_type": "Slug typu udalosti", - "set_up_agent": "Nastaviť agenta", "webhook_title": "Názov typu udalosti", "webhook_start_time": "Čas začiatku udalosti", "webhook_end_time": "Čas ukončenia udalosti", @@ -3672,9 +3625,6 @@ "visit": "Navštíviť", "location_custom_label_input_label": "Vlastný popis na stránke rezervácie", "meeting_link": "Odkaz na stretnutie", - "session_outcome": "Výsledok relácie", - "call_created": "Hovor vytvorený", - "voicemail": "Hlasová schránka", "my_bookings": "Moje rezervácie", "phone": "Telefón", "free": "Bezplatné", @@ -3682,8 +3632,6 @@ "user_name": "Meno používateľa", "expand_panel": "Rozbaliť panel", "collapse_panel": "Zbaliť panel", - "email_verification_required": "Pre tento typ udalosti je potrebné overenie e-mailu", - "invalid_verification_code": "Bol poskytnutý neplatný overovací kód", "you_have_one_team": "Máte jeden tím", "consider_consolidating_one_team_org": "Zvážte vytvorenie organizácie na zjednotenie fakturácie, administratívnych nástrojov a analytiky v rámci vášho tímu.", "consider_consolidating_multi_team_org": "Zvážte vytvorenie organizácie na zjednotenie fakturácie, administratívnych nástrojov a analytiky v rámci vašich tímov.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Pred plánovaným časom začiatku", "cancel_booking_acknowledge_no_show_fee": "Beriem na vedomie, že zrušením rezervácie v rámci {{timeValue}} {{timeUnit}} pred začiatkom mi bude účtovaný poplatok za neúčasť vo výške {{amount, currency}}", "contact_organizer": "Ak máte akékoľvek otázky, kontaktujte organizátora.", - "booking_time_option": "Čas rezervácie", - "booking_time_option_description": "Kedy je rezervácia naplánovaná (od začiatku do konca)", - "created_at_option": "Vytvorené", - "created_at_option_description": "Kedy bola rezervácia pôvodne vytvorená", - "call_details": "Detaily hovoru", - "call_id": "ID hovoru", - "call_information": "Informácie o hovore", - "sentiment": "Sentiment", - "disconnect_reason": "Dôvod odpojenia", - "call_summary": "Zhrnutie hovoru", - "transcription": "Prepis", - "event_details": "Detaily udalosti", - "agent": "Agent", - "no_transcript_available": "Žiadny prepis nie je k dispozícii", - "testing_sms_workflow_info_message": "Pri testovaní tohto pracovného postupu majte na pamäti, že SMS musia byť naplánované najmenej 15 minút vopred", - "start_from_scratch_title": "Začať od nuly", - "start_from_scratch_description": "Vytvorte si vlastný pracovný postup od základov.", - "cal_ai_template_title": "Cal.ai šablóna", - "cal_ai_template_description": "AI agenti, ktorí rezervujú stretnutia, posielajú pripomienky a následne komunikujú!", - "voice": "Hlas", - "select_voice": "Vybrať hlas", - "select_voice_for_agent": "Vyberte hlas pre vášho agenta", - "choose_a_voice_for_your_agent": "Vyberte hlas pre vášho agenta", - "trait": "Vlastnosť", - "voice_id": "ID hlasu", - "use_voice": "Použiť hlas", - "current_voice": "Aktuálny hlas", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Pridajte svoje nové reťazce nad túto líniu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 7f5aa21b7a65bd..1c40c84ddc688a 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -819,8 +819,6 @@ "workflow_validation_failed": "Validacija toka rada nije uspela", "workflow_validation_empty_fields": "Jedan ili više koraka toka rada imaju prazan sadržaj poruke", "workflow_validation_unverified_contacts": "Jedan ili više brojeva telefona ili email adresa nisu verifikovani", - "supercharge_your_workflows_with_cal_ai": "Unapredite svoje radne tokove sa Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Realistični AI agenti koji zakazuju sastanke, šalju podsetnike i prate vaše klijente.", "phone_number_imported_successfully": "Broj telefona je uspešno uvezen i povezan sa agentom", "phone_number_deleted_successfully": "Broj telefona je uspešno obrisan", "delete_phone_number_confirmation": "Da li ste sigurni da želite da obrišete ovaj broj telefona? Ova akcija se ne može poništiti.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Pretplata za broj telefona je uspešno otkazana", "updating": "Ažuriranje", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Zdravo, kako ste?", "round_robin_description": "Ciklusirajte sastanke između više članova tima.", "managed_event": "Upravljani događaj", "username_placeholder": "username", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Da li ste sigurni da želite da obrišete ovaj korak radnog toka?", "do_you_still_want_to_unsubscribe": "Da li i dalje želite da odjavite telefonski broj sa ovog agenta?", "the_action_will_disconnect_phone_number": "Ova akcija će prekinuti vezu telefonskog broja sa agentom. Agent neće moći da obavlja pozive dok se ne poveže novi telefonski broj.", - "cal_ai_phone_numbers": "Brojevi telefona", + "cal_ai_phone_numbers": "telefonski brojevi", "connect_phone_number": "Povežite telefonski broj", - "cal_ai_phone_numbers_description": "Upravljajte svojim brojevima telefona", + "cal_ai_phone_numbers_description": "Upravljajte svojim telefonskim brojevima", "import_number": "Uvezite broj", "this_action_will_also": "Ova akcija će takođe:", "import_phone_number": "Uvezite telefonski broj", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Otkažite pretplatu za telefonski broj", "delete_associated_phone_number": "Obrišite povezani telefonski broj", "unauthorized_create_workflow": "Niste ovlašćeni da kreirate ovaj radni tok", - "import_phone_number_description": "Uvezite svoj Twilio broj telefona za korišćenje sa telefonom", + "import_phone_number_description": "Uvezite svoj Twilio telefonski broj za korišćenje sa telefonom", "phone_number_cost": "${{price}}/mesečno", "buy_new_number": "Kupite novi broj", "buy_number_cost_x_per_month": "Kupovina telefonskog broja košta ${{priceInDollars}} mesečno. Naplaćivaće vam se mesečno za svaki aktivni telefonski broj.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Da, otkaži pretplatu", "cancel_phone_number_subscription_confirmation": "Da li ste sigurni da želite da otkažete pretplatu za ovaj telefonski broj? Ova radnja se ne može poništiti i izgubićete pristup ovom telefonskom broju.", "add_members": "Dodajte članove...", - "add_members_no_ellipsis": "Dodaj članove", "no_assigned_members": "Nema dodeljenih članova", "assigned_to": "Dodeljeno", "you_must_be_logged_in_to": "Morate biti prijavljeni na {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategorije", "pricing": "Cene", "learn_more": "Saznajte više", - "try_now": "Isprobajte sada", "privacy_policy": "Pravila o privatnosti", "terms_of_service": "Uslovi korišćenja", "remove": "Ukloni", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "pošaljite Whatsapp poruku učesniku", "workflows": "Radni tokovi", "new_workflow_btn": "Novi radni tok", - "how_would_you_like_to_start": "Kako želite da počnete?", "add_new_workflow": "Dodaj novi radni tok", "reschedule_event_trigger": "kada se događaju promeni vreme", "trigger": "Okidač", @@ -1722,8 +1716,6 @@ "event_duration_info": "Trajanje događaja", "event_time_info": "Vreme početka događaja", "event_type_not_found": "Tip događaja nije pronađen", - "number_to_call_variable": "Broj za poziv", - "number_to_call_info": "Broj telefona korisnika kojeg pozivate", "location_variable": "Lokacija", "location_info": "Lokacija događaja", "additional_notes_variable": "Dodatne beleške", @@ -1761,7 +1753,6 @@ "team_url": "URL tima", "team_members": "Članovi tima", "more": "Više", - "cal_ai_workflows": "Cal.ai radni tokovi", "and_count_more": "i još {{count}}", "more_page_footer": "Mi mobilnu aplikaciju smatramo produžetkom veb aplikacije. Ako vršite bilo kakve komplikovane radnje, molimo da se vratite na veb aplikaciju.", "workflow_example_1": "Pošalji SMS podsetnik učesniku 24 sata pre početka događaja", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Pošalji imejl podsetnik učesniku jedan sat pre početka događaja", "workflow_example_5": "Pošalji prilagođeni imejl domaćinu kada je događaju promenjeno vreme", "workflow_example_6": "Pošalji prilagođeni SMS domaćinu kada je novi događaj rezervisan", - "send_sms_reminder": "Pošalji SMS podsetnik", - "send_sms_reminder_description": "24 sata pre početka događaja", - "follow_up_with_no_shows": "Pratite one koji se nisu pojavili", - "follow_up_with_no_shows_description": "30 minuta nakon završetka događaja", - "remind_attendees_to_bring_id": "Podsetite učesnike da ponesu ličnu kartu", - "remind_attendees_to_bring_id_description": "1 dan pre početka događaja", - "email_to_remind_booking": "Email podsetnik", - "email_to_remind_booking_description": "1 sat pre početka događaja", - "custom_sms_reminder": "Prilagođeni SMS podsetnik", - "custom_sms_reminder_description": "Kada je događaj zakazan", - "custom_email_reminder": "Prilagođeni email podsetnik", - "custom_email_reminder_description": "Događaj je pomeren za domaćina", "count_managed_to_limit": "Uključi broj rezervacija iz upravljanih tipova događaja", "welcome_to_cal_header": "Dobrodošli na {{appName}}!", "edit_form_later_subtitle": "Moći ćete da uredite ovo kasnije.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Prikaži na stranici zakazivanja", "visit_cancelled_booking": "Možete posetiti stranicu otkazane rezervacije", "get_started_zapier_templates": "Započnite sa Zapier predlošcima", - "standard_templates": "Standardni šabloni", - "cal_ai_templates": "Cal.ai šabloni", "team_is_unpublished": "Opozvano je objavljivanje tima {{team}}", "org_is_unpublished_description": "Ovaj link Organizacije trenutno nije dostupan. Obratite se vlasniku Organizacije i zamolite ga da ga objavi.", "team_is_unpublished_description": "Ovaj link {{entity}} više nije dostupan. Obratite se vlasniku {{entity}} i zamolite ga da objavi.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Nije moguće naplatiti karticu za plaćanje.", "insights": "Uvidi", "routing_forms": "Obrasci za usmeravanje", + "testing_workflow_info_message": "Kada testirate ovaj radni tok, imajte na umu da imejlovi i SMS-ovi mogu da budu zakazani najmanje 1 sat unapred", "insights_no_data_found_for_filter": "Nisu pronađeni podaci za izabrani filter ili izabrane datume.", "acknowledge_booking_no_show_fee": "Jasno mi je da će mi biti naplaćena naknada za izostanak putem kartice u iznosu od {{amount, currency}} ukoliko ne prisustvujem ovom događaju.", "days": "dana", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Tim: {{teamName}}", "insights_user_filter": "Korisnik: {{userName}}", "insights_subtitle": "Pogledajte insights rezervacije za vaše događaje", - "call_history": "Istorija poziva", - "call_history_subtitle": "Pregledajte istoriju poziva za sve vaše Cal.ai pozive", "location_options": "{{locationCount}} opcije lokacije", - "channel_type": "Tip kanala", - "end_reason": "Razlog završetka", - "session_status": "Status sesije", - "user_sentiment": "Raspoloženje korisnika", - "time_header": "Vreme", - "from_header": "Od", "custom_plan": "Prilagođeni plan", "email_embed": "Ugradi u imejl", "add_times_to_your_email": "Izaberite nekoliko dostupnih termina i ugradite ih u svoj imejl", @@ -2782,8 +2752,6 @@ "account_already_linked": "Nalog je već povezan", "send_email": "Pošaljite imejl", "cal_ai_phone_call_action": "Pozovi učesnika koristeći Cal.ai glasovnog agenta", - "call_to_confirm_booking": "Poziv za potvrdu rezervacije", - "cal_ai_phone_call_action_description": "2 sata pre početka događaja", "cal_ai_agent_configuration": "Cal.ai konfiguracija agenta", "choose_at_least_one_event_type_test_call": "Molimo izaberite najmanje jedan tip događaja za probni poziv.", "mark_as_no_show": "Označi kao nepojavljivanje", @@ -3235,15 +3203,9 @@ "verify_email_change": "Potvrdi promenu email adrese", "buy_credits": "Kupi kredite", "credits": "Krediti", - "credits_used": "Iskorišćeni krediti", - "total_credits_remaining": "Ukupno preostalo", - "credits_per_tip_org": "Dobijate 1000 kredita mesečno po članu tima", - "credits_per_tip_teams": "Dobijate 750 kredita mesečno po članu tima", - "view_and_manage_credits": "Pregledajte i upravljajte kreditima za slanje SMS poruka", + "view_and_manage_credits": "Pregledaj i upravljaj kreditima", "view_and_manage_credits_description": "Pregledajte i upravljajte kreditima za slanje SMS poruka. Jedan kredit vredi 1¢ (USD). <0>Saznaj više", - "credit_worth_description": "Jedan kredit vredi 1¢ (USD). <0>Saznajte više", "buy_additional_credits": "Kupi dodatne kredite ($0.01 po kreditu)", - "view_additional_credits_expense_tip": "Dodatnu potrošnju kredita možete videti u evidenciji troškova", "overview": "Pregled", "organization_slug_taken": "Slug organizacije je već zauzet", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Ne možete kreirati organizaciju jer ste već deo neke organizacije", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Vaš Cal.com tim {{teamName}} je potrošio sve kredite. Kao rezultat, SMS poruke se sada šalju putem e-pošte. Da biste nastavili sa slanjem SMS-ova, kupite dodatne kredite.", "credit_limit_reached_message_user": "Vaš Cal.com nalog je ostao bez kredita. Zbog toga se SMS poruke sada šalju putem e-pošte. Da biste nastavili sa slanjem SMS poruka, molimo vas da kupite dodatne kredite.", "current_credit_balance": "Trenutno stanje: {{balance}} kredita", - "current_balance": "Trenutno stanje:", "notification_about_your_booking": "Obaveštenje o vašoj rezervaciji", "monthly_credits": "Mesečni krediti", "total_credits": "Ukupno kredita: {{totalCredits}}", "remaining_credits": "Preostalo kredita: {{remainingCredits}}", - "remaining": "Preostalo", - "total": "Ukupno", "additional_credits": "Dodatni krediti", "routing_form_next_in_queue": "{{count}} sledeći u redu", "routing_form_select_members_to_email": "Pošalji odgovore putem e-pošte", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Pregled postojećih radnih tokova i njihovih konfiguracija", "pbac_desc_update_workflows": "Uređivanje i modifikovanje podešavanja radnih tokova", "pbac_desc_delete_workflows": "Uklanjanje radnih tokova iz sistema", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Kreirajte webhooks", - "pbac_desc_view_webhooks": "Pregledajte webhooks", - "pbac_desc_update_webhooks": "Ažuriraj webhook-ove", - "pbac_desc_delete_webhooks": "Obriši webhook-ove", "pbac_desc_manage_workflows": "Puni pristup upravljanju svim radnim tokovima", "pbac_desc_create_event_types": "Kreiranje tipova događaja", "pbac_desc_view_event_types": "Pregled tipova događaja", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Naziv događaja okidača (npr. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Vreme webhook-a", "webhook_type": "Slug tipa događaja", - "set_up_agent": "Podesi agenta", "webhook_title": "Ime tipa događaja", "webhook_start_time": "Vreme početka događaja", "webhook_end_time": "Vreme završetka događaja", @@ -3672,9 +3625,6 @@ "visit": "Poseti", "location_custom_label_input_label": "Prilagođena oznaka na stranici za rezervacije", "meeting_link": "Link za sastanak", - "session_outcome": "Ishod sesije", - "call_created": "Poziv kreiran", - "voicemail": "Govorna pošta", "my_bookings": "Moje rezervacije", "phone": "Telefon", "free": "Besplatno", @@ -3682,8 +3632,6 @@ "user_name": "Ime korisnika", "expand_panel": "Proširi panel", "collapse_panel": "Skupi panel", - "email_verification_required": "Za ovaj tip događaja potrebna je verifikacija e-pošte", - "invalid_verification_code": "Unet je nevažeći verifikacioni kod", "you_have_one_team": "Imate jedan tim", "consider_consolidating_one_team_org": "Razmotrite postavljanje organizacije za objedinjavanje naplate, administratorskih alata i analitike za vaš tim.", "consider_consolidating_multi_team_org": "Razmotrite postavljanje organizacije za objedinjavanje naplate, administratorskih alata i analitike za vaše timove.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Pre zakazanog vremena početka", "cancel_booking_acknowledge_no_show_fee": "Potvrđujem da ću otkazivanjem rezervacije unutar {{timeValue}} {{timeUnit}} od vremena početka biti zadužen/a naknadom za nedolazak u iznosu od {{amount, currency}}", "contact_organizer": "Ako imate bilo kakvih pitanja, molimo kontaktirajte organizatora.", - "booking_time_option": "Vreme rezervacije", - "booking_time_option_description": "Kada je rezervacija zakazana (od početka do kraja)", - "created_at_option": "Kreirano", - "created_at_option_description": "Kada je rezervacija prvobitno kreirana", - "call_details": "Detalji poziva", - "call_id": "ID poziva", - "call_information": "Informacije o pozivu", - "sentiment": "Raspoloženje", - "disconnect_reason": "Razlog prekida veze", - "call_summary": "Rezime poziva", - "transcription": "Transkripcija", - "event_details": "Detalji događaja", - "agent": "Agent", - "no_transcript_available": "Transkript nije dostupan", - "testing_sms_workflow_info_message": "Kada testirate ovaj tok rada, imajte u vidu da SMS poruke moraju biti zakazane najmanje 15 minuta unapred", - "start_from_scratch_title": "Počni od nule", - "start_from_scratch_description": "Kreirajte sopstveni tok rada od nule.", - "cal_ai_template_title": "Cal.ai šablon", - "cal_ai_template_description": "AI agenti koji zakazuju sastanke, šalju podsetnike i prate napredak!", - "voice": "Glas", - "select_voice": "Izaberite glas", - "select_voice_for_agent": "Izaberite glas za vašeg agenta", - "choose_a_voice_for_your_agent": "Odaberite glas za vašeg agenta", - "trait": "Karakteristika", - "voice_id": "ID glasa", - "use_voice": "Koristi glas", - "current_voice": "Trenutni glas", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index 53e2151f9caed0..c1086034cdd102 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Verifiera din e-postadress för att garantera bästa e-post- och kalenderleverans", "verify_email_email_header": "Verifiera din e-postadress", "verify_email_button": "Verifiera e-post", - "cal_ai_assistant": "Assistent", + "cal_ai_assistant": "Cal AI-assistent", "send_cal_video_transcription_emails": "Skicka Cal Video-transkriptionsmejl", "description_send_cal_video_transcription_emails": "Skicka mejl med transkriptionen av Cal Video efter att mötet avslutas. (Kräver ett betalt abonnemang)", "verify_email_change_description": "Du har nyligen begärt att ändra e-postadressen du använder för att logga in på ditt {{appName}}-konto. Klicka på knappen nedan för att bekräfta din nya e-postadress.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Arbetsflödesvalidering misslyckades", "workflow_validation_empty_fields": "Ett eller flera steg i arbetsflödet har tomt meddelandeinnehåll", "workflow_validation_unverified_contacts": "Ett eller flera telefonnummer eller e-postadresser är inte verifierade", - "supercharge_your_workflows_with_cal_ai": "Maximera dina arbetsflöden med Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Naturtrogna AI-agenter som bokar möten, skickar påminnelser och följer upp med dina kunder.", "phone_number_imported_successfully": "Telefonnummer importerades och kopplades till agenten framgångsrikt", "phone_number_deleted_successfully": "Telefonnummer togs bort framgångsrikt", "delete_phone_number_confirmation": "Är du säker på att du vill ta bort detta telefonnummer? Denna åtgärd kan inte ångras.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Telefonnummerprenumeration avbröts framgångsrikt", "updating": "Uppdaterar", "round_robin": "Tur och ordning", - "hi_how_are_you_doing": "Hej, hur mår du?", "round_robin_description": "Variera möten mellan olika teammedlemmar.", "managed_event": "Hanterad händelse", "username_placeholder": "användarnamn", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Är du säker på att du vill ta bort detta arbetsflödessteg?", "do_you_still_want_to_unsubscribe": "Vill du fortfarande avregistrera telefonnumret från denna agent?", "the_action_will_disconnect_phone_number": "Denna åtgärd kommer att koppla bort telefonnumret från agenten. Agenten kommer inte att kunna ringa samtal förrän ett nytt telefonnummer är anslutet.", - "cal_ai_phone_numbers": "Telefonnummer", + "cal_ai_phone_numbers": "Cal AI-telefonnummer", "connect_phone_number": "Anslut telefonnummer", - "cal_ai_phone_numbers_description": "Hantera dina telefonnummer", + "cal_ai_phone_numbers_description": "Hantera dina Cal AI-telefonnummer", "import_number": "Importera nummer", "this_action_will_also": "Denna åtgärd kommer också att:", "import_phone_number": "Importera telefonnummer", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Avsluta din telefonnummerprenumeration", "delete_associated_phone_number": "Ta bort det associerade telefonnumret", "unauthorized_create_workflow": "Du är inte behörig att skapa detta arbetsflöde", - "import_phone_number_description": "Importera ditt Twilio-telefonnummer för att använda med Phone", + "import_phone_number_description": "Importera ditt Twilio-telefonnummer för att använda med Cal AI-telefon", "phone_number_cost": "${{price}}/månad", "buy_new_number": "Köp nytt nummer", "buy_number_cost_x_per_month": "Att köpa ett telefonnummer kostar ${{priceInDollars}} per månad. Du debiteras månadsvis för varje aktivt telefonnummer.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Ja, avbryt prenumerationen", "cancel_phone_number_subscription_confirmation": "Är du säker på att du vill avbryta denna telefonnummerprenumeration? Denna åtgärd kan inte ångras och du kommer att förlora åtkomsten till detta telefonnummer.", "add_members": "Lägg till medlemmar ...", - "add_members_no_ellipsis": "Lägg till medlemmar", "no_assigned_members": "Inga tilldelade medlemmar", "assigned_to": "Tilldelad till", "you_must_be_logged_in_to": "Du måste vara inloggad på {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Kategorier", "pricing": "Priser", "learn_more": "Läs mer", - "try_now": "Prova nu", "privacy_policy": "Sekretesspolicy", "terms_of_service": "Användningsvillkor", "remove": "Radera", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "skicka Whatsapp till deltagare", "workflows": "Arbetsflöden", "new_workflow_btn": "Nytt arbetsflöde", - "how_would_you_like_to_start": "Hur vill du börja?", "add_new_workflow": "Lägg till ett nytt arbetsflöde", "reschedule_event_trigger": "när händelsen bokas om", "trigger": "Utlösare", @@ -1722,8 +1716,6 @@ "event_duration_info": "Eventets varaktighet", "event_time_info": "Händelsens starttid", "event_type_not_found": "Eventtyp hittades inte", - "number_to_call_variable": "Nummer att ringa", - "number_to_call_info": "Telefonnumret till användaren du ringer", "location_variable": "Plats", "location_info": "Händelsens plats", "additional_notes_variable": "Ytterligare noteringar", @@ -1761,7 +1753,6 @@ "team_url": "Team-URL", "team_members": "Teammedlemmar", "more": "Mer", - "cal_ai_workflows": "Cal.ai-arbetsflöden", "and_count_more": "och {{count}} till", "more_page_footer": "Vi ser mobilprogrammet som en förlängning av webbprogrammet. Om du utför komplicerade åtgärder kan du hänvisa tillbaka till webbprogrammet.", "workflow_example_1": "Skicka SMS-påminnelse till deltagare 24 timmar innan händelsen börjar", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Skicka en e-postpåminnelse till deltagare 1 timme innan händelsen startar", "workflow_example_5": "Skicka anpassat e-postmeddelande till värd när händelsen bokas om", "workflow_example_6": "Skicka anpassat SMS till värd när ny händelse bokas", - "send_sms_reminder": "Skicka SMS-påminnelse", - "send_sms_reminder_description": "24 timmar innan händelsen börjar", - "follow_up_with_no_shows": "Följ upp med uteblivna", - "follow_up_with_no_shows_description": "30 minuter efter att händelsen slutar", - "remind_attendees_to_bring_id": "Påminn deltagare att ta med ID", - "remind_attendees_to_bring_id_description": "1 dag innan händelsen börjar", - "email_to_remind_booking": "E-postpåminnelse", - "email_to_remind_booking_description": "1 timme innan händelsen börjar", - "custom_sms_reminder": "Anpassad SMS-påminnelse", - "custom_sms_reminder_description": "När händelsen är schemalagd", - "custom_email_reminder": "Anpassad e-postpåminnelse", - "custom_email_reminder_description": "Händelsen är ombokad till värden", "count_managed_to_limit": "Inkludera bokningsantal från hanterade händelsetyper", "welcome_to_cal_header": "Välkommen till {{appName}}!", "edit_form_later_subtitle": "Du kommer att kunna redigera detta senare.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Visa på bokningssidan", "visit_cancelled_booking": "Du kan besöka sidan för avbokade bokningar", "get_started_zapier_templates": "Kom igång med Zapier-mallar", - "standard_templates": "Standardmallar", - "cal_ai_templates": "Cal.ai-mallar", "team_is_unpublished": "{{team}} har avpublicerats", "org_is_unpublished_description": "Den här organisationslänken är för närvarande inte tillgänglig. Kontakta organisationens ägare eller be denne att publicera den.", "team_is_unpublished_description": "Den här {{entity}}-länken är för närvarande inte tillgänglig. Kontakta ägaren till {{entity}} eller be vederbörande att publicera den.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Kunde inte debitera kortet för betalning.", "insights": "Insikter", "routing_forms": "Routningsformulär", + "testing_workflow_info_message": "När det här arbetsflödet testas ska du vara medveten om att e-postmeddelanden och SMS endast kan schemaläggas minst en timme i förväg", "insights_no_data_found_for_filter": "Inga data hittades för valt filter eller valda datum.", "acknowledge_booking_no_show_fee": "Jag bekräftar att en avgift på {{amount, currency}} för utebliven närvaro debiteras mitt kort om jag inte deltar i händelsen.", "days": "dagar", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Team: {{teamName}}", "insights_user_filter": "Användare: {{userName}}", "insights_subtitle": "Visa Insights-bokningar för dina händelser", - "call_history": "Samtalshistorik", - "call_history_subtitle": "Visa samtalshistorik för dina Cal.ai-samtal", "location_options": "{{locationCount}} platsalternativ", - "channel_type": "Kanaltyp", - "end_reason": "Avslutsorsak", - "session_status": "Sessionsstatus", - "user_sentiment": "Användarens känsla", - "time_header": "Tid", - "from_header": "Från", "custom_plan": "Anpassad plan", "email_embed": "E-post inbäddad", "add_times_to_your_email": "Välj några tillgängliga tider och bädda in dem i din e-post", @@ -2782,8 +2752,6 @@ "account_already_linked": "Kontot är redan länkat", "send_email": "Skicka e-post", "cal_ai_phone_call_action": "Ring deltagare med Cal.ai Voice Agent", - "call_to_confirm_booking": "Ring för att bekräfta bokning", - "cal_ai_phone_call_action_description": "2 timmar innan evenemanget börjar", "cal_ai_agent_configuration": "Cal.ai-agentkonfiguration", "choose_at_least_one_event_type_test_call": "Välj minst en händelsetyp för att göra ett testanrop.", "mark_as_no_show": "Markera som utebliven", @@ -3235,15 +3203,9 @@ "verify_email_change": "Verifiera ändring av e-postadress", "buy_credits": "Köp krediter", "credits": "Krediter", - "credits_used": "Använda krediter", - "total_credits_remaining": "Totalt kvarvarande", - "credits_per_tip_org": "Du får 1000 krediter per månad, per teammedlem", - "credits_per_tip_teams": "Du får 750 krediter per månad, per teammedlem", - "view_and_manage_credits": "Visa och hantera krediter för att skicka SMS-meddelanden", + "view_and_manage_credits": "Visa och hantera krediter", "view_and_manage_credits_description": "Visa och hantera krediter för att skicka SMS-meddelanden. En kredit är värd 1¢ (USD). <0>Läs mer", - "credit_worth_description": "En kredit är värd 1¢ (USD). <0>Läs mer", "buy_additional_credits": "Köp fler krediter (0,01 USD per kredit)", - "view_additional_credits_expense_tip": "Du kan se ytterligare kreditutgifter i din utgiftslogg", "overview": "Översikt", "organization_slug_taken": "Organisationens slug är redan tagen", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Du kan inte skapa en organisation eftersom du redan är en del av en organisation", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Ditt Cal.com-team {{teamName}} har slut på krediter. Som ett resultat skickas SMS-meddelanden nu via e-post istället. För att återuppta SMS-sändningar, vänligen köp fler krediter.", "credit_limit_reached_message_user": "Ditt Cal.com-konto har slut på krediter. Som ett resultat skickas SMS nu via e-post istället. För att återuppta SMS-utskick, vänligen köp fler krediter.", "current_credit_balance": "Nuvarande saldo: {{balance}} krediter", - "current_balance": "Nuvarande saldo:", "notification_about_your_booking": "Avisering om din bokning", "monthly_credits": "Månatliga krediter", "total_credits": "Totala krediter: {{totalCredits}}", "remaining_credits": "Återstående krediter: {{remainingCredits}}", - "remaining": "Kvarvarande", - "total": "Totalt", "additional_credits": "Ytterligare krediter", "routing_form_next_in_queue": "{{count}} nästa i kön", "routing_form_select_members_to_email": "Skicka e-postsvar till", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Visa befintliga arbetsflöden och deras konfigurationer", "pbac_desc_update_workflows": "Redigera och ändra inställningar för arbetsflöden", "pbac_desc_delete_workflows": "Ta bort arbetsflöden från systemet", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Skapa webhooks", - "pbac_desc_view_webhooks": "Visa webhooks", - "pbac_desc_update_webhooks": "Uppdatera webhooks", - "pbac_desc_delete_webhooks": "Ta bort webhooks", "pbac_desc_manage_workflows": "Fullständig hantering av alla arbetsflöden", "pbac_desc_create_event_types": "Skapa händelsetyper", "pbac_desc_view_event_types": "Visa händelsetyper", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Namnet på utlösarhändelsen (t.ex. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Tidpunkten för webhooken", "webhook_type": "Händelsetypens slug", - "set_up_agent": "Ställ in agent", "webhook_title": "Händelsetypens namn", "webhook_start_time": "Händelsens starttid", "webhook_end_time": "Händelsens sluttid", @@ -3672,9 +3625,6 @@ "visit": "Besök", "location_custom_label_input_label": "Anpassad etikett på bokningssidan", "meeting_link": "Möteslänk", - "session_outcome": "Sessionsresultat", - "call_created": "Samtal skapat", - "voicemail": "Röstmeddelande", "my_bookings": "Mina bokningar", "phone": "Telefon", "free": "Gratis", @@ -3682,8 +3632,6 @@ "user_name": "Användarnamn", "expand_panel": "Expandera panel", "collapse_panel": "Minimera panel", - "email_verification_required": "E-postverifiering krävs för denna typ av händelse", - "invalid_verification_code": "Ogiltig verifieringskod angiven", "you_have_one_team": "Du har ett team", "consider_consolidating_one_team_org": "Överväg att skapa en organisation för att förena fakturering, administratörsverktyg och analys för ditt team.", "consider_consolidating_multi_team_org": "Överväg att skapa en organisation för att förena fakturering, administratörsverktyg och analys för dina team.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Före schemalagd starttid", "cancel_booking_acknowledge_no_show_fee": "Jag bekräftar att genom att avboka inom {{timeValue}} {{timeUnit}} före starttiden kommer jag att debiteras uteblivandeavgiften på {{amount, currency}}", "contact_organizer": "Om du har några frågor, vänligen kontakta arrangören.", - "booking_time_option": "Bokningstid", - "booking_time_option_description": "När bokningen är schemalagd (start till slut)", - "created_at_option": "Skapad", - "created_at_option_description": "När bokningen ursprungligen skapades", - "call_details": "Samtalsdetaljer", - "call_id": "Samtals-ID", - "call_information": "Samtalsinformation", - "sentiment": "Känsla", - "disconnect_reason": "Anledning till frånkoppling", - "call_summary": "Samtalssammanfattning", - "transcription": "Transkription", - "event_details": "Händelsedetaljer", - "agent": "Agent", - "no_transcript_available": "Ingen transkription tillgänglig", - "testing_sms_workflow_info_message": "När du testar detta arbetsflöde, var medveten om att SMS måste schemaläggas minst 15 minuter i förväg", - "start_from_scratch_title": "Börja från början", - "start_from_scratch_description": "Skapa ditt eget arbetsflöde från grunden.", - "cal_ai_template_title": "Cal.ai-mall", - "cal_ai_template_description": "AI-agenter som bokar möten, skickar påminnelser och följer upp!", - "voice": "Röst", - "select_voice": "Välj röst", - "select_voice_for_agent": "Välj en röst för din agent", - "choose_a_voice_for_your_agent": "Välj en röst för din agent", - "trait": "Egenskap", - "voice_id": "Röst-ID", - "use_voice": "Använd röst", - "current_voice": "Nuvarande röst", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 1716add76060e6..f1d7b50d10118b 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "E-postaların ve takvim bilgilerinin güvenilir bir şekilde teslim edilmesini sağlamak için e-posta adresinizi doğrulayın", "verify_email_email_header": "E-posta adresinizi doğrulayın", "verify_email_button": "E-postayı doğrula", - "cal_ai_assistant": "Asistan", + "cal_ai_assistant": "Asistanı", "send_cal_video_transcription_emails": "Cal Video Transkript E-postaları Gönder", "description_send_cal_video_transcription_emails": "Toplantı sona erdikten sonra Cal Video transkriptini içeren e-postalar gönder. (Ücretli plan gerektirir)", "verify_email_change_description": "{{appName}} hesabınıza giriş yapmak için kullandığınız e-posta adresini değiştirme talebinde bulundunuz. Yeni e-posta adresinizi onaylamak için lütfen aşağıdaki butona tıklayın.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "İş akışı doğrulaması başarısız oldu", "workflow_validation_empty_fields": "Bir veya daha fazla iş akışı adımında boş mesaj içeriği var", "workflow_validation_unverified_contacts": "Bir veya daha fazla telefon numarası veya e-posta adresi doğrulanmadı", - "supercharge_your_workflows_with_cal_ai": "Cal.ai ile İş Akışlarınızı Güçlendirin", - "supercharge_your_workflows_with_cal_ai_description": "Toplantıları planlayan, hatırlatmalar gönderen ve müşterilerinizle takip yapan gerçekçi yapay zeka ajanları.", "phone_number_imported_successfully": "Telefon numarası başarıyla içe aktarıldı ve temsilciye bağlandı", "phone_number_deleted_successfully": "Telefon numarası başarıyla silindi", "delete_phone_number_confirmation": "Bu telefon numarasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Telefon numarası aboneliği başarıyla iptal edildi", "updating": "Güncelleniyor", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Merhaba, nasılsınız?", "round_robin_description": "Toplantıları birden fazla ekip üyesi arasında döndürün.", "managed_event": "Yönetilen Etkinlik", "username_placeholder": "kullanıcı adı", @@ -881,7 +878,7 @@ "the_action_will_disconnect_phone_number": "Bu işlem, telefon numarasının ajandan bağlantısını kesecektir. Yeni bir telefon numarası bağlanana kadar ajan arama yapamayacaktır.", "cal_ai_phone_numbers": "Telefon Numaraları", "connect_phone_number": "Telefon Numarası Bağla", - "cal_ai_phone_numbers_description": "Telefon Numaralarınızı Yönetin", + "cal_ai_phone_numbers_description": "Telefon Numaralarınızı yönetin", "import_number": "Numara İçe Aktar", "this_action_will_also": "Bu işlem ayrıca:", "import_phone_number": "Telefon Numarası İçe Aktar", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Telefon numarası aboneliğinizi iptal edin", "delete_associated_phone_number": "İlişkili telefon numarasını sil", "unauthorized_create_workflow": "Bu iş akışını oluşturma yetkiniz yok", - "import_phone_number_description": "Telefon ile kullanmak için Twilio telefon numaranızı içe aktarın", + "import_phone_number_description": "Phone ile kullanmak için Twilio telefon numaranızı içe aktarın", "phone_number_cost": "${{price}}/ay", "buy_new_number": "Yeni Numara Satın Al", "buy_number_cost_x_per_month": "Telefon numarası satın almak aylık ${{priceInDollars}} tutarındadır. Her aktif telefon numarası için aylık olarak ücretlendirileceksiniz.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Evet, Aboneliği İptal Et", "cancel_phone_number_subscription_confirmation": "Bu telefon numarası aboneliğini iptal etmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve bu telefon numarasına erişiminizi kaybedeceksiniz.", "add_members": "Üye ekle...", - "add_members_no_ellipsis": "Üyeleri ekle", "no_assigned_members": "Atanan üye yok", "assigned_to": "Şuraya atandı:", "you_must_be_logged_in_to": "{{url}} adresine giriş yapmış olmalısınız", @@ -1211,7 +1207,6 @@ "categories": "Kategoriler", "pricing": "Fiyatlandırma", "learn_more": "Daha fazla bilgi edinin", - "try_now": "Hemen deneyin", "privacy_policy": "Gizlilik Politikası", "terms_of_service": "Kullanım Koşulları", "remove": "Kaldır", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "katılımcıya Whatsapp mesajı gönder", "workflows": "İş akışları", "new_workflow_btn": "Yeni İş Akışı", - "how_would_you_like_to_start": "Nasıl başlamak istersiniz?", "add_new_workflow": "Yeni iş akışı ekle", "reschedule_event_trigger": "etkinlik yeniden planlandığında", "trigger": "Tetikleyici", @@ -1722,8 +1716,6 @@ "event_duration_info": "Etkinlik süresi", "event_time_info": "Etkinlik başlama saati", "event_type_not_found": "Etkinlik Türü Bulunamadı", - "number_to_call_variable": "Aranacak numara", - "number_to_call_info": "Aradığınız kullanıcının telefon numarası", "location_variable": "Konum", "location_info": "Etkinlik yeri", "additional_notes_variable": "Ek notlar", @@ -1761,7 +1753,6 @@ "team_url": "Ekip URL'si", "team_members": "Ekip üyeleri", "more": "Daha fazla", - "cal_ai_workflows": "Cal.ai İş Akışları", "and_count_more": "ve {{count}} daha fazla", "more_page_footer": "Mobil uygulamayı web uygulamasının bir uzantısı olarak görüyoruz. Karmaşık bir işlem yapıyorsanız lütfen web uygulamasına geri dönün.", "workflow_example_1": "Etkinlik başlamadan 24 saat önce katılımcıya SMS hatırlatıcısı gönder", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Etkinlikler başlamadan 1 saat önce katılımcıya e-posta hatırlatıcısı gönder", "workflow_example_5": "Etkinlik yeniden planlandığında etkinlik sahibine özel e-posta gönder", "workflow_example_6": "Yeni bir etkinlik rezervasyonu yapıldığında organizatöre özel SMS gönder", - "send_sms_reminder": "SMS hatırlatması gönder", - "send_sms_reminder_description": "Etkinlik başlamadan 24 saat önce", - "follow_up_with_no_shows": "Katılmayanlarla takip yap", - "follow_up_with_no_shows_description": "Etkinlik bittikten 30 dakika sonra", - "remind_attendees_to_bring_id": "Katılımcılara kimlik getirmelerini hatırlat", - "remind_attendees_to_bring_id_description": "Etkinlik başlamadan 1 gün önce", - "email_to_remind_booking": "E-posta Hatırlatması", - "email_to_remind_booking_description": "Etkinlik başlamadan 1 saat önce", - "custom_sms_reminder": "Özel SMS Hatırlatması", - "custom_sms_reminder_description": "Etkinlik planlandığında", - "custom_email_reminder": "Özel E-posta Hatırlatması", - "custom_email_reminder_description": "Etkinlik ev sahibi için yeniden planlandığında", "count_managed_to_limit": "Yönetilen etkinlik türlerinden rezervasyon sayılarını dahil et", "welcome_to_cal_header": "{{appName}}'a hoş geldiniz!", "edit_form_later_subtitle": "Bunu daha sonra düzenleyebileceksiniz.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Rezervasyon sayfasında göster", "visit_cancelled_booking": "İptal edilen rezervasyon sayfasını ziyaret edebilirsiniz", "get_started_zapier_templates": "Zapier şablonlarını kullanmaya başlayın", - "standard_templates": "Standart Şablonlar", - "cal_ai_templates": "Cal.ai Şablonları", "team_is_unpublished": "{{team}} paylaşılmadı", "org_is_unpublished_description": "Bu kuruluş bağlantısı şu anda kullanılamıyor. Lütfen kuruluş sahibiyle iletişime geçin veya ondan bir bağlantı paylaşmasını isteyin.", "team_is_unpublished_description": "Bu {{entity}} bağlantısı şu anda kullanılamıyor. Lütfen {{entity}} sahibiyle iletişime geçin veya ondan bir bağlantı paylaşmasını isteyin.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Ödeme için karttan çekim yapılamadı.", "insights": "Öngörüler", "routing_forms": "Yönlendirme Formları", + "testing_workflow_info_message": "Bu iş akışını test ederken, E-postaların ve SMS'lerin yalnızca en az 1 saat önceden planlanabileceğini unutmayın", "insights_no_data_found_for_filter": "Seçili filtre veya tarihler için veri bulunamadı.", "acknowledge_booking_no_show_fee": "Bu etkinliğe katılmadığım takdirde kartımdan {{amount, currency}} tutarında katılmama ücreti alınmasını kabul ediyorum.", "days": "gün", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Ekip: {{teamName}}", "insights_user_filter": "Kullanıcı: {{userName}}", "insights_subtitle": "Etkinliklerinizdeki rezervasyon insights'ı görüntüleyin", - "call_history": "Arama Geçmişi", - "call_history_subtitle": "Cal.ai aramalarınız genelinde arama geçmişini görüntüleyin", "location_options": "{{locationCount}} konum seçenekleri", - "channel_type": "Kanal Türü", - "end_reason": "Sonlandırma Nedeni", - "session_status": "Oturum Durumu", - "user_sentiment": "Kullanıcı Duygusu", - "time_header": "Zaman", - "from_header": "Kimden", "custom_plan": "Özel Plan", "email_embed": "E-posta Yerleştirme", "add_times_to_your_email": "Uygun zamanlardan bazılarını seçin ve bunları E-postanıza ekleyin", @@ -2782,8 +2752,6 @@ "account_already_linked": "Hesap zaten bağlı", "send_email": "E-posta gönder", "cal_ai_phone_call_action": "Cal.ai Ses Ajanı kullanarak katılımcıyı ara", - "call_to_confirm_booking": "Rezervasyonu onaylamak için arama", - "cal_ai_phone_call_action_description": "Etkinlik başlamadan 2 saat önce", "cal_ai_agent_configuration": "Cal.ai Ajan Yapılandırması", "choose_at_least_one_event_type_test_call": "Test araması yapmak için lütfen en az bir etkinlik türü seçin.", "mark_as_no_show": "Katılmadı olarak işaretle", @@ -3235,15 +3203,9 @@ "verify_email_change": "E-posta değişikliğini doğrula", "buy_credits": "Kredi Satın Al", "credits": "Krediler", - "credits_used": "Kullanılan krediler", - "total_credits_remaining": "Toplam kalan", - "credits_per_tip_org": "Ayda, ekip üyesi başına 1000 kredi alırsınız", - "credits_per_tip_teams": "Ayda, ekip üyesi başına 750 kredi alırsınız", - "view_and_manage_credits": "SMS mesajları göndermek için kredileri görüntüleyin ve yönetin", + "view_and_manage_credits": "Kredileri görüntüle ve yönet", "view_and_manage_credits_description": "SMS mesajları göndermek için kredileri görüntüleyin ve yönetin. Bir kredi 1¢ (USD) değerindedir. <0>Daha fazla bilgi", - "credit_worth_description": "Bir kredi 1¢ (USD) değerindedir. <0>Daha fazla bilgi", "buy_additional_credits": "Ek kredi satın al (kredi başına $0.01)", - "view_additional_credits_expense_tip": "Ek kredi harcamalarını gider kaydınızda görüntüleyebilirsiniz", "overview": "Genel bakış", "organization_slug_taken": "Organizasyon kısa adı zaten alınmış", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Zaten bir organizasyonun parçası olduğunuz için yeni bir organizasyon oluşturamazsınız", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Cal.com ekibiniz {{teamName}} kredileri tükendi. Sonuç olarak, SMS mesajları artık e-posta yoluyla gönderiliyor. SMS göndermeye devam etmek için lütfen ek krediler satın alın.", "credit_limit_reached_message_user": "Cal.com hesabınızın kredileri tükendi. Bu nedenle, SMS mesajları artık e-posta yoluyla gönderiliyor. SMS gönderimini sürdürmek için lütfen ek krediler satın alın.", "current_credit_balance": "Mevcut bakiye: {{balance}} kredi", - "current_balance": "Mevcut bakiye:", "notification_about_your_booking": "Rezervasyonunuz hakkında bildirim", "monthly_credits": "Aylık krediler", "total_credits": "Toplam krediler: {{totalCredits}}", "remaining_credits": "Kalan krediler: {{remainingCredits}}", - "remaining": "Kalan", - "total": "Toplam", "additional_credits": "Ek krediler", "routing_form_next_in_queue": "Sırada {{count}} sonraki", "routing_form_select_members_to_email": "E-posta yanıtlarını gönderilecek kişiler", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Mevcut iş akışlarını ve yapılandırmalarını görüntüle", "pbac_desc_update_workflows": "İş akışı ayarlarını düzenle ve değiştir", "pbac_desc_delete_workflows": "İş akışlarını sistemden kaldır", - "pbac_resource_webhook": "Web kancası", - "pbac_desc_create_webhooks": "Web kancaları oluştur", - "pbac_desc_view_webhooks": "Web kancalarını görüntüle", - "pbac_desc_update_webhooks": "Web kancalarını güncelle", - "pbac_desc_delete_webhooks": "Web kancalarını sil", "pbac_desc_manage_workflows": "Tüm iş akışlarına tam yönetim erişimi", "pbac_desc_create_event_types": "Etkinlik türleri oluştur", "pbac_desc_view_event_types": "Etkinlik türlerini görüntüle", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Tetikleyici olayın adı (örn. BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Webhook zamanı", "webhook_type": "Etkinlik türü kısa adı", - "set_up_agent": "Temsilci Kur", "webhook_title": "Etkinlik türü adı", "webhook_start_time": "Etkinliğin başlama zamanı", "webhook_end_time": "Etkinliğin bitiş zamanı", @@ -3672,9 +3625,6 @@ "visit": "Ziyaret et", "location_custom_label_input_label": "Rezervasyon sayfasında özel etiket", "meeting_link": "Toplantı bağlantısı", - "session_outcome": "Oturum Sonucu", - "call_created": "Arama Oluşturuldu", - "voicemail": "Sesli Mesaj", "my_bookings": "Rezervasyonlarım", "phone": "Telefon", "free": "Ücretsiz", @@ -3682,8 +3632,6 @@ "user_name": "Kullanıcı Adı", "expand_panel": "Paneli Genişlet", "collapse_panel": "Paneli Daralt", - "email_verification_required": "Bu etkinlik türü için e-posta doğrulaması gereklidir", - "invalid_verification_code": "Geçersiz doğrulama kodu girildi", "you_have_one_team": "Bir ekibiniz var", "consider_consolidating_one_team_org": "Faturalama, yönetim araçları ve analitiği ekibiniz genelinde birleştirmek için bir organizasyon kurmayı düşünün.", "consider_consolidating_multi_team_org": "Faturalama, yönetim araçları ve analitiği ekipleriniz genelinde birleştirmek için bir organizasyon kurmayı düşünün.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Planlanan başlangıç zamanından önce", "cancel_booking_acknowledge_no_show_fee": "Başlangıç saatine {{timeValue}} {{timeUnit}} kala rezervasyonu iptal etmem durumunda {{amount, currency}} tutarında no-show ücreti ödeyeceğimi kabul ediyorum", "contact_organizer": "Herhangi bir sorunuz varsa, lütfen organizatörle iletişime geçin.", - "booking_time_option": "Rezervasyon zamanı", - "booking_time_option_description": "Rezervasyonun planlandığı zaman (başlangıçtan bitişe)", - "created_at_option": "Oluşturulma tarihi", - "created_at_option_description": "Rezervasyonun ilk oluşturulduğu zaman", - "call_details": "Arama Detayları", - "call_id": "Arama Kimliği", - "call_information": "Arama Bilgileri", - "sentiment": "Duygu Analizi", - "disconnect_reason": "Bağlantı Kesme Nedeni", - "call_summary": "Arama Özeti", - "transcription": "Transkripsiyon", - "event_details": "Etkinlik Detayları", - "agent": "Temsilci", - "no_transcript_available": "Transkript mevcut değil", - "testing_sms_workflow_info_message": "Bu iş akışını test ederken, SMS'lerin en az 15 dakika önceden planlanması gerektiğini unutmayın", - "start_from_scratch_title": "Sıfırdan başla", - "start_from_scratch_description": "Kendi iş akışınızı sıfırdan oluşturun.", - "cal_ai_template_title": "Cal.ai şablonu", - "cal_ai_template_description": "Toplantıları planlayan, hatırlatmalar gönderen ve takip eden yapay zeka ajanları!", - "voice": "Ses", - "select_voice": "Ses Seç", - "select_voice_for_agent": "Ajanınız için bir ses seçin", - "choose_a_voice_for_your_agent": "Ajanınız için bir ses seçin", - "trait": "Özellik", - "voice_id": "Ses Kimliği", - "use_voice": "Sesi Kullan", - "current_voice": "Mevcut Ses", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index ec4342391a6554..d5060efedaf8e8 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -819,8 +819,6 @@ "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": "Ви впевнені, що хочете видалити цей номер телефону? Цю дію неможливо скасувати.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Підписку на номер телефону успішно скасовано", "updating": "Оновлення", "round_robin": "Ротація", - "hi_how_are_you_doing": "Привіт, як ваші справи?", "round_robin_description": "Кілька учасників команди призначаються для нарад циклічно й по черзі.", "managed_event": "Керований захід", "username_placeholder": "ім’я користувача", @@ -879,9 +876,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_phone_numbers": "Телефонні номери Cal AI", "connect_phone_number": "Підключити номер телефону", - "cal_ai_phone_numbers_description": "Керуйте своїми номерами телефонів", + "cal_ai_phone_numbers_description": "Керуйте вашими телефонними номерами Cal AI", "import_number": "Імпортувати номер", "this_action_will_also": "Ця дія також:", "import_phone_number": "Імпортувати номер телефону", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Скасувати підписку на номер телефону", "delete_associated_phone_number": "Видалити пов'язаний номер телефону", "unauthorized_create_workflow": "Ви не маєте прав для створення цього робочого процесу", - "import_phone_number_description": "Імпортуйте свій номер телефону Twilio для використання з телефоном", + "import_phone_number_description": "Імпортуйте свій номер телефону Twilio для використання з Phone", "phone_number_cost": "${{price}}/місяць", "buy_new_number": "Придбати новий номер", "buy_number_cost_x_per_month": "Придбання номера телефону коштує ${{priceInDollars}} на місяць. З вас щомісяця стягуватиметься плата за кожен активний номер телефону.", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "Категорії", "pricing": "Ціни", "learn_more": "Докладніше", - "try_now": "Спробувати зараз", "privacy_policy": "Політика конфіденційності", "terms_of_service": "Умови користування", "remove": "Вилучити", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "надсилати повідомлення у Whatsapp учаснику", "workflows": "Робочі процеси", "new_workflow_btn": "Новий робочий процес", - "how_would_you_like_to_start": "Як би ви хотіли почати?", "add_new_workflow": "Додати новий робочий процес", "reschedule_event_trigger": "у разі перенесення заходу", "trigger": "Тригер", @@ -1722,8 +1716,6 @@ "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": "Додаткові примітки", @@ -1761,7 +1753,6 @@ "team_url": "URL команди", "team_members": "Учасники команди", "more": "Більше", - "cal_ai_workflows": "Робочі процеси Cal.ai", "and_count_more": "та ще {{count}}", "more_page_footer": "Ми вважаємо мобільний додаток розширенням вебдодатка. Якщо вам потрібно виконати якісь складні дії, скористайтесь останнім.", "workflow_example_1": "Надсилати учаснику нагадування про захід через SMS за 24 години до початку", @@ -1770,18 +1761,6 @@ "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": "Ви зможете відредагувати це пізніше.", @@ -2205,8 +2184,6 @@ "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}} або попросіть його опублікувати посилання.", @@ -2364,6 +2341,7 @@ "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": "днів", @@ -2486,15 +2464,7 @@ "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": "Виберіть кілька доступних часових проміжків і вбудуйте їх в електронний лист", @@ -2782,8 +2752,6 @@ "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": "Позначити як неявку", @@ -3235,15 +3203,9 @@ "verify_email_change": "Підтвердити зміну електронної пошти", "buy_credits": "Купити кредити", "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": "Переглянути та керувати кредитами", "view_and_manage_credits_description": "Переглядайте та керуйте кредитами для надсилання SMS-повідомлень. Один кредит коштує 1¢ (USD). <0>Дізнатися більше", - "credit_worth_description": "Один кредит коштує 1¢ (USD). <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": "Ви не можете створити організацію, оскільки вже є частиною іншої організації", @@ -3424,13 +3386,10 @@ "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": "Надіслати відповіді електронною поштою", @@ -3508,11 +3467,6 @@ "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": "Перегляд типів заходів", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Назва події-тригера (наприклад, BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Час створення вебхука", "webhook_type": "Слаг типу події", - "set_up_agent": "Налаштувати агента", "webhook_title": "Назва типу події", "webhook_start_time": "Час початку події", "webhook_end_time": "Час завершення події", @@ -3672,9 +3625,6 @@ "visit": "Відвідати", "location_custom_label_input_label": "Власна мітка на сторінці бронювання", "meeting_link": "Посилання на зустріч", - "session_outcome": "Результат сесії", - "call_created": "Дзвінок створено", - "voicemail": "Голосова пошта", "my_bookings": "Мої бронювання", "phone": "Телефон", "free": "Безкоштовно", @@ -3682,8 +3632,6 @@ "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": "Розгляньте можливість створення організації для об'єднання оплати, інструментів адміністрування та аналітики у ваших командах.", @@ -3704,32 +3652,5 @@ "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": "ШІ-агенти, які бронюють зустрічі, надсилають нагадування та здійснюють подальший зв'язок!", - "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index b5d140d794ad25..3fe1e67a4e7e9c 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -23,7 +23,7 @@ "verify_email_banner_body": "Xác minh địa chỉ email của bạn nhằm bảo đảm khả năng gửi email và lịch hẹn tốt nhất", "verify_email_email_header": "Xác minh địa chỉ email của bạn", "verify_email_button": "Xác minh email", - "cal_ai_assistant": "Trợ lý", + "cal_ai_assistant": "Trợ lý AI của Cal", "send_cal_video_transcription_emails": "Gửi email bản ghi Cal Video", "description_send_cal_video_transcription_emails": "Gửi email kèm bản ghi của Cal Video sau khi cuộc họp kết thúc. (Yêu cầu gói trả phí)", "verify_email_change_description": "Bạn đã yêu cầu thay đổi địa chỉ email mà bạn sử dụng để đăng nhập vào tài khoản {{appName}} của mình. Vui lòng nhấp vào nút bên dưới để xác nhận địa chỉ email mới của bạn.", @@ -819,8 +819,6 @@ "workflow_validation_failed": "Xác thực quy trình làm việc thất bại", "workflow_validation_empty_fields": "Một hoặc nhiều bước quy trình làm việc có nội dung tin nhắn trống", "workflow_validation_unverified_contacts": "Một hoặc nhiều số điện thoại hoặc địa chỉ email chưa được xác minh", - "supercharge_your_workflows_with_cal_ai": "Tăng cường Quy trình làm việc của bạn với Cal.ai", - "supercharge_your_workflows_with_cal_ai_description": "Các đại diện AI chân thực sẽ đặt lịch họp, gửi lời nhắc và theo dõi với khách hàng của bạn.", "phone_number_imported_successfully": "Số điện thoại đã được nhập và liên kết với đại lý thành công", "phone_number_deleted_successfully": "Số điện thoại đã được xóa thành công", "delete_phone_number_confirmation": "Bạn có chắc chắn muốn xóa số điện thoại này không? Hành động này không thể hoàn tác.", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "Hủy đăng ký thuê bao số điện thoại thành công", "updating": "Đang cập nhật", "round_robin": "Round Robin", - "hi_how_are_you_doing": "Xin chào, bạn khỏe không?", "round_robin_description": "Luân phiên những cuộc họp giữa các thành viên trong nhóm.", "managed_event": "Sự kiện được quản lý", "username_placeholder": "tên người dùng", @@ -879,9 +876,9 @@ "are_you_sure_you_want_to_delete_workflow_step": "Bạn có chắc muốn xóa bước quy trình làm việc này không?", "do_you_still_want_to_unsubscribe": "Bạn vẫn muốn hủy đăng ký số điện thoại khỏi trợ lý này?", "the_action_will_disconnect_phone_number": "Hành động này sẽ ngắt kết nối số điện thoại khỏi trợ lý. Trợ lý sẽ không thể thực hiện cuộc gọi cho đến khi kết nối số điện thoại mới.", - "cal_ai_phone_numbers": "Số điện thoại", + "cal_ai_phone_numbers": "Số điện thoại Cal AI", "connect_phone_number": "Kết nối số điện thoại", - "cal_ai_phone_numbers_description": "Quản lý Số điện thoại của bạn", + "cal_ai_phone_numbers_description": "Quản lý số điện thoại của bạn", "import_number": "Nhập số điện thoại", "this_action_will_also": "Hành động này cũng sẽ:", "import_phone_number": "Nhập số điện thoại", @@ -889,7 +886,7 @@ "cancel_your_phone_number_subscription": "Hủy đăng ký số điện thoại của bạn", "delete_associated_phone_number": "Xóa số điện thoại đã liên kết", "unauthorized_create_workflow": "Bạn không được phép tạo quy trình làm việc này", - "import_phone_number_description": "Nhập số điện thoại Twilio của bạn để sử dụng với Điện thoại", + "import_phone_number_description": "Nhập số điện thoại Twilio của bạn để sử dụng với Phone", "phone_number_cost": "${{price}}/tháng", "buy_new_number": "Mua số mới", "buy_number_cost_x_per_month": "Mua số điện thoại có giá ${{priceInDollars}} mỗi tháng. Bạn sẽ bị tính phí hàng tháng cho mỗi số điện thoại đang hoạt động.", @@ -900,7 +897,6 @@ "yes_cancel_subscription": "Có, hủy đăng ký", "cancel_phone_number_subscription_confirmation": "Bạn có chắc chắn muốn hủy đăng ký số điện thoại này không? Hành động này không thể hoàn tác và bạn sẽ mất quyền truy cập vào số điện thoại này.", "add_members": "Thêm thành viên...", - "add_members_no_ellipsis": "Thêm thành viên", "no_assigned_members": "Không thành viên nào được phân công", "assigned_to": "Được phân công cho", "you_must_be_logged_in_to": "Bạn phải đăng nhập vào {{url}}", @@ -1211,7 +1207,6 @@ "categories": "Thể loại", "pricing": "Giá", "learn_more": "Tìm hiểu thêm", - "try_now": "Thử ngay", "privacy_policy": "Chính sách bảo mật", "terms_of_service": "Điều khoản dịch vụ", "remove": "Xoá", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "gửi WhatsApp đến người tham gia", "workflows": "Tiến độ công việc", "new_workflow_btn": "Dòng công việc mới", - "how_would_you_like_to_start": "Bạn muốn bắt đầu như thế nào?", "add_new_workflow": "Thêm một tiến độ công việc mới", "reschedule_event_trigger": "thời điểm sự kiện được sắp lịch lại", "trigger": "Kích hoạt", @@ -1722,8 +1716,6 @@ "event_duration_info": "Thời lượng sự kiện", "event_time_info": "Thời gian bắt đầu sự kiện", "event_type_not_found": "Không tìm thấy loại sự kiện", - "number_to_call_variable": "Số để gọi", - "number_to_call_info": "Số điện thoại của người dùng bạn đang gọi", "location_variable": "Vị trí", "location_info": "Địa điểm sự kiện", "additional_notes_variable": "Ghi chú bổ sung", @@ -1761,7 +1753,6 @@ "team_url": "URL nhóm", "team_members": "Thành viên nhóm", "more": "Khác", - "cal_ai_workflows": "Quy trình Cal.ai", "and_count_more": "và {{count}} nữa", "more_page_footer": "Chúng tôi xem ứng dụng di động là mở rộng của ứng dụng web. Nếu bạn sắp thực hiện bất kì thao tác phức tạp nào, vui lòng trở lại ứng dụng web.", "workflow_example_1": "Gửi lời nhắc SMS cho người tham gia 24 giờ trước lúc bắt đầu sự kiện", @@ -1770,18 +1761,6 @@ "workflow_example_4": "Gửi lời nhắc email cho người tham gia 1 giờ trước khi bắt đầu sự kiện", "workflow_example_5": "Gửi email tuỳ chỉnh cho chủ sự kiện khi sự kiện được sắp lịch lại", "workflow_example_6": "Gửi SMS tuỳ chỉnh cho chủ sự kiện khi sự kiện mới được đặt lịch hẹn", - "send_sms_reminder": "Gửi nhắc nhở qua SMS", - "send_sms_reminder_description": "24 giờ trước khi sự kiện bắt đầu", - "follow_up_with_no_shows": "Theo dõi với người không tham dự", - "follow_up_with_no_shows_description": "30 phút sau khi sự kiện kết thúc", - "remind_attendees_to_bring_id": "Nhắc người tham dự mang theo ID", - "remind_attendees_to_bring_id_description": "1 ngày trước khi sự kiện bắt đầu", - "email_to_remind_booking": "Email nhắc nhở", - "email_to_remind_booking_description": "1 giờ trước khi sự kiện bắt đầu", - "custom_sms_reminder": "Nhắc nhở SMS tùy chỉnh", - "custom_sms_reminder_description": "Khi sự kiện được lên lịch", - "custom_email_reminder": "Nhắc nhở Email tùy chỉnh", - "custom_email_reminder_description": "Sự kiện được lên lịch lại cho người tổ chức", "count_managed_to_limit": "Bao gồm số lượng đặt chỗ từ các loại sự kiện được quản lý", "welcome_to_cal_header": "Chào mừng đến với {{appName}}!", "edit_form_later_subtitle": "Bạn sẽ có thể chỉnh sửa điều này sau.", @@ -2205,8 +2184,6 @@ "show_on_booking_page": "Hiện trên trang lịch hẹn", "visit_cancelled_booking": "Bạn có thể truy cập trang đặt chỗ đã hủy", "get_started_zapier_templates": "Bắt đầu với các mẫu Zapier", - "standard_templates": "Mẫu Tiêu chuẩn", - "cal_ai_templates": "Mẫu Cal.ai", "team_is_unpublished": "{{team}} chưa được công bố", "org_is_unpublished_description": "Liên kết tổ chức này hiện không khả dụng. Hãy liên lạc với chủ tổ chức hoặc yêu cầu họ công bố liên kết đó.", "team_is_unpublished_description": "Liên kết {{entity}} này hiện không khả dụng. Hãy liên lạc với chủ {{entity}} hoặc yêu cầu họ công bố liên kết đó.", @@ -2364,6 +2341,7 @@ "could_not_charge_card": "Không thể tính phí thẻ cho thanh toán.", "insights": "Thông tin chi tiết", "routing_forms": "Biểu mẫu Định tuyến", + "testing_workflow_info_message": "Khi kiểm tra tiến độ công việc này, hãy để ý rằng Email và SMS chỉ có thể được đặt lịch trước ít nhất 1 giờ", "insights_no_data_found_for_filter": "Không tìm thấy dữ liệu cho bộ lọc hay ngày đã chọn.", "acknowledge_booking_no_show_fee": "Tôi công nhận là nếu tôi không tham dự sự kiện này thì một khoản phí vắng mặt là {{amount, currency}} sẽ được áp dụng cho thẻ của tôi.", "days": "ngày", @@ -2486,15 +2464,7 @@ "insights_team_filter": "Nhóm: {{teamName}}", "insights_user_filter": "Người dùng: {{userName}}", "insights_subtitle": "Xem insights của đặt lịch ở mọi sự kiện của bạn", - "call_history": "Lịch sử cuộc gọi", - "call_history_subtitle": "Xem lịch sử cuộc gọi trên tất cả các cuộc gọi Cal.ai của bạn", "location_options": "{{locationCount}} lựa chọn vị trí", - "channel_type": "Loại kênh", - "end_reason": "Lý do kết thúc", - "session_status": "Trạng thái phiên", - "user_sentiment": "Cảm xúc người dùng", - "time_header": "Thời gian", - "from_header": "Từ", "custom_plan": "Gói Tuỳ chỉnh", "email_embed": "Email đã nhúng", "add_times_to_your_email": "Chọn một số thời gian trống và nhúng chúng vào trong Email của bạn", @@ -2782,8 +2752,6 @@ "account_already_linked": "Tài khoản đã được liên kết", "send_email": "Gửi email", "cal_ai_phone_call_action": "Gọi cho người tham dự bằng Trợ lý Giọng nói Cal.ai", - "call_to_confirm_booking": "Gọi để xác nhận đặt lịch", - "cal_ai_phone_call_action_description": "2 giờ trước khi sự kiện bắt đầu", "cal_ai_agent_configuration": "Cấu hình Agent Cal.ai", "choose_at_least_one_event_type_test_call": "Vui lòng chọn ít nhất một loại sự kiện để thực hiện cuộc gọi thử nghiệm.", "mark_as_no_show": "Đánh dấu là không xuất hiện", @@ -3235,15 +3203,9 @@ "verify_email_change": "Xác minh thay đổi email", "buy_credits": "Mua tín dụng", "credits": "Tín dụng", - "credits_used": "Tín dụng đã sử dụng", - "total_credits_remaining": "Tổng số còn lại", - "credits_per_tip_org": "Bạn nhận được 1000 tín dụng mỗi tháng, cho mỗi thành viên nhóm", - "credits_per_tip_teams": "Bạn nhận được 750 tín dụng mỗi tháng, cho mỗi thành viên nhóm", - "view_and_manage_credits": "Xem và quản lý tín dụng để gửi tin nhắn SMS", + "view_and_manage_credits": "Xem và quản lý tín dụng", "view_and_manage_credits_description": "Xem và quản lý tín dụng để gửi tin nhắn SMS. Một tín dụng có giá trị 1¢ (USD). <0>Tìm hiểu thêm", - "credit_worth_description": "Một tín dụng có giá trị 1¢ (USD). <0>Tìm hiểu thêm", "buy_additional_credits": "Mua thêm tín dụng ($0.01 mỗi tín dụng)", - "view_additional_credits_expense_tip": "Bạn có thể xem chi tiêu tín dụng bổ sung trong nhật ký chi phí của mình", "overview": "Tổng quan", "organization_slug_taken": "Slug của tổ chức đã được sử dụng", "you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Bạn không thể tạo một tổ chức vì bạn đã là thành viên của một tổ chức khác", @@ -3424,13 +3386,10 @@ "credit_limit_reached_message": "Đội Cal.com {{teamName}} của bạn đã hết tín dụng. Do đó, tin nhắn SMS hiện đang được gửi qua email. Để tiếp tục gửi SMS, vui lòng mua thêm tín dụng.", "credit_limit_reached_message_user": "Tài khoản Cal.com của bạn đã hết tín dụng. Do đó, tin nhắn SMS hiện đang được gửi qua email thay thế. Để tiếp tục gửi SMS, vui lòng mua thêm tín dụng.", "current_credit_balance": "Số dư hiện tại: {{balance}} tín dụng", - "current_balance": "Số dư hiện tại:", "notification_about_your_booking": "Thông báo về lịch hẹn của bạn", "monthly_credits": "Tín dụng hàng tháng", "total_credits": "Tổng tín dụng: {{totalCredits}}", "remaining_credits": "Tín dụng còn lại: {{remainingCredits}}", - "remaining": "Còn lại", - "total": "Tổng cộng", "additional_credits": "Tín dụng bổ sung", "routing_form_next_in_queue": "{{count}} tiếp theo trong hàng đợi", "routing_form_select_members_to_email": "Gửi phản hồi qua email đến", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "Xem tiến độ công việc hiện có và cấu hình của chúng", "pbac_desc_update_workflows": "Chỉnh sửa và điều chỉnh cài đặt tiến độ công việc", "pbac_desc_delete_workflows": "Xóa tiến độ công việc khỏi hệ thống", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "Tạo webhooks", - "pbac_desc_view_webhooks": "Xem webhooks", - "pbac_desc_update_webhooks": "Cập nhật webhooks", - "pbac_desc_delete_webhooks": "Xóa webhooks", "pbac_desc_manage_workflows": "Quyền truy cập quản lý đầy đủ đối với tất cả tiến độ công việc", "pbac_desc_create_event_types": "Tạo loại sự kiện", "pbac_desc_view_event_types": "Xem loại sự kiện", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "Tên của sự kiện kích hoạt (ví dụ: BOOKING_CREATED, BOOKING_CANCELLED)", "webhook_created_at": "Thời gian của webhook", "webhook_type": "Slug loại sự kiện", - "set_up_agent": "Thiết lập Tổng đài viên", "webhook_title": "Tên loại sự kiện", "webhook_start_time": "Thời gian bắt đầu sự kiện", "webhook_end_time": "Thời gian kết thúc sự kiện", @@ -3672,9 +3625,6 @@ "visit": "Truy cập", "location_custom_label_input_label": "Nhãn tùy chỉnh trên trang đặt lịch", "meeting_link": "Liên kết cuộc họp", - "session_outcome": "Kết quả Phiên", - "call_created": "Cuộc gọi đã được Tạo", - "voicemail": "Thư thoại", "my_bookings": "Lịch hẹn của tôi", "phone": "Điện thoại", "free": "Miễn phí", @@ -3682,8 +3632,6 @@ "user_name": "Tên người dùng", "expand_panel": "Mở rộng bảng điều khiển", "collapse_panel": "Thu gọn bảng điều khiển", - "email_verification_required": "Yêu cầu xác minh email cho loại sự kiện này", - "invalid_verification_code": "Mã xác minh không hợp lệ", "you_have_one_team": "Bạn có một nhóm", "consider_consolidating_one_team_org": "Hãy cân nhắc thiết lập một tổ chức để hợp nhất thanh toán, công cụ quản trị và phân tích cho nhóm của bạn.", "consider_consolidating_multi_team_org": "Hãy cân nhắc thiết lập một tổ chức để hợp nhất thanh toán, công cụ quản trị và phân tích cho các nhóm của bạn.", @@ -3704,32 +3652,5 @@ "before_scheduled_start_time": "Trước thời gian bắt đầu đã lên lịch", "cancel_booking_acknowledge_no_show_fee": "Tôi xác nhận rằng khi hủy lịch hẹn trong vòng {{timeValue}} {{timeUnit}} trước thời gian bắt đầu, tôi sẽ bị tính phí vắng mặt là {{amount, currency}}", "contact_organizer": "Nếu bạn có bất kỳ câu hỏi nào, vui lòng liên hệ với người tổ chức.", - "booking_time_option": "Thời gian đặt lịch", - "booking_time_option_description": "Khi cuộc hẹn được lên lịch (từ đầu đến cuối)", - "created_at_option": "Được tạo vào", - "created_at_option_description": "Khi cuộc hẹn được tạo ban đầu", - "call_details": "Chi tiết Cuộc gọi", - "call_id": "ID Cuộc gọi", - "call_information": "Thông tin Cuộc gọi", - "sentiment": "Cảm xúc", - "disconnect_reason": "Lý do Ngắt kết nối", - "call_summary": "Tóm tắt Cuộc gọi", - "transcription": "Bản ghi", - "event_details": "Chi tiết Sự kiện", - "agent": "Tổng đài viên", - "no_transcript_available": "Không có bản ghi", - "testing_sms_workflow_info_message": "Khi kiểm tra quy trình này, lưu ý rằng SMS cần được lên lịch trước ít nhất 15 phút", - "start_from_scratch_title": "Bắt đầu từ đầu", - "start_from_scratch_description": "Tạo quy trình của riêng bạn từ đầu.", - "cal_ai_template_title": "Mẫu Cal.ai", - "cal_ai_template_description": "Trợ lý AI đặt lịch họp, gửi lời nhắc và theo dõi!", - "voice": "Giọng nói", - "select_voice": "Chọn giọng nói", - "select_voice_for_agent": "Chọn giọng nói cho trợ lý của bạn", - "choose_a_voice_for_your_agent": "Chọn giọng nói cho trợ lý của bạn", - "trait": "Đặc điểm", - "voice_id": "ID giọng nói", - "use_voice": "Sử dụng giọng nói", - "current_voice": "Giọng nói hiện tại", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index 29c93e6e4c783c..125b12d0382c40 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -819,8 +819,6 @@ "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": "您确定要删除此电话号码吗?此操作无法撤销。", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "电话号码订阅已成功取消", "updating": "正在更新", "round_robin": "轮流模式", - "hi_how_are_you_doing": "您好,您最近怎么样?", "round_robin_description": "和多个团队成员之间轮流举行会议。", "managed_event": "托管活动", "username_placeholder": "用户名", @@ -881,7 +878,7 @@ "the_action_will_disconnect_phone_number": "此操作将断开电话号码与代理的连接。代理在未连接新电话号码之前将无法拨打电话。", "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": "导入电话号码", @@ -889,7 +886,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}}。每个活跃电话号码将按月收费。", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "类别", "pricing": "定价", "learn_more": "了解更多", - "try_now": "立即试用", "privacy_policy": "隐私政策", "terms_of_service": "服务条款", "remove": "移除", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "向参与者发送 Whatsapp", "workflows": "工作流程", "new_workflow_btn": "新建工作流程", - "how_would_you_like_to_start": "您想如何开始?", "add_new_workflow": "添加新的工作流程", "reschedule_event_trigger": "重新安排活动时", "trigger": "触发", @@ -1722,8 +1716,6 @@ "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": "附加备注", @@ -1761,7 +1753,6 @@ "team_url": "团队链接", "team_members": "团队成员", "more": "更多", - "cal_ai_workflows": "Cal.ai 工作流程", "and_count_more": "以及另外 {{count}} 个", "more_page_footer": "我们将移动应用程序视为 Web 应用程序的扩展。如果您要执行任何复杂操作,请回到 Web 应用程序。", "workflow_example_1": "在活动开始前 24 小时向参与者发送短信提醒", @@ -1770,18 +1761,6 @@ "workflow_example_4": "在活动开始前 1 小时向参与者发送电子邮件提醒", "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": "活动开始前 1 天", - "email_to_remind_booking": "邮件提醒", - "email_to_remind_booking_description": "活动开始前 1 小时", - "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": "您稍后可以编辑此内容。", @@ -2205,8 +2184,6 @@ "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}}所有者或请他们发布。", @@ -2364,6 +2341,7 @@ "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": "天", @@ -2486,15 +2464,7 @@ "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": "选择几个可预约时间并将它们嵌入到您的电子邮件", @@ -2782,8 +2752,6 @@ "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": "标记为未出席", @@ -3235,15 +3203,9 @@ "verify_email_change": "验证邮箱变更", "buy_credits": "购买积分", "credits": "积分", - "credits_used": "已用积分", - "total_credits_remaining": "剩余总积分", - "credits_per_tip_org": "每位团队成员每月可获得 1000 积分", - "credits_per_tip_teams": "每位团队成员每月可获得 750 积分", - "view_and_manage_credits": "查看和管理发送短信的积分", + "view_and_manage_credits": "查看和管理积分", "view_and_manage_credits_description": "查看和管理发送短信的积分。一个积分价值 1 美分(USD)。<0>了解更多", - "credit_worth_description": "每个积分价值 1 美分 (USD)。<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": "您无法创建组织,因为您已经是某个组织的成员", @@ -3424,13 +3386,10 @@ "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": "发送电子邮件回复给", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "查看现有工作流程及其配置", "pbac_desc_update_workflows": "编辑和修改工作流程设置", "pbac_desc_delete_workflows": "从系统中移除工作流程", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "创建 Webhook", - "pbac_desc_view_webhooks": "查看 Webhook", - "pbac_desc_update_webhooks": "更新 Webhooks", - "pbac_desc_delete_webhooks": "删除 Webhooks", "pbac_desc_manage_workflows": "全面管理所有工作流程的权限", "pbac_desc_create_event_types": "创建活动类型", "pbac_desc_view_event_types": "查看活动类型", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "触发事件的名称(例如:BOOKING_CREATED,BOOKING_CANCELLED)", "webhook_created_at": "Webhook 创建时间", "webhook_type": "事件类型标识", - "set_up_agent": "设置代理", "webhook_title": "事件类型名称", "webhook_start_time": "事件的开始时间", "webhook_end_time": "事件的结束时间", @@ -3672,9 +3625,6 @@ "visit": "访问", "location_custom_label_input_label": "预约页面上的自定义标签", "meeting_link": "会议链接", - "session_outcome": "会话结果", - "call_created": "通话已创建", - "voicemail": "语音留言", "my_bookings": "我的预约", "phone": "电话", "free": "免费", @@ -3682,8 +3632,6 @@ "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": "建议设置一个组织,以统一多个团队的账单、管理工具和分析功能。", @@ -3704,32 +3652,5 @@ "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": "测试此工作流时,请注意短信需要至少提前 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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 4a5ce61422fd4d..46cf8a2839ef95 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -819,8 +819,6 @@ "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": "您確定要刪除此電話號碼嗎?此操作無法復原。", @@ -848,7 +846,6 @@ "phone_number_subscription_cancelled_successfully": "電話號碼訂閱已成功取消", "updating": "正在更新", "round_robin": "循環制", - "hi_how_are_you_doing": "嗨,您最近怎麼樣?", "round_robin_description": "與多位團隊成員進行週期會議。", "managed_event": "受管活動", "username_placeholder": "使用者名稱", @@ -881,7 +878,7 @@ "the_action_will_disconnect_phone_number": "此操作將使電話號碼與代理斷開連接。代理將無法撥打電話,直到連接新的電話號碼為止。", "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": "匯入電話號碼", @@ -889,7 +886,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}}。每個有效的電話號碼將按月收費。", @@ -900,7 +897,6 @@ "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}}", @@ -1211,7 +1207,6 @@ "categories": "類別", "pricing": "定價", "learn_more": "了解詳情", - "try_now": "立即試用", "privacy_policy": "隱私權政策", "terms_of_service": "服務條款", "remove": "移除", @@ -1445,7 +1440,6 @@ "whatsapp_attendee_action": "傳送 Whatsapp 給與會者", "workflows": "工作流程", "new_workflow_btn": "新工作流程", - "how_would_you_like_to_start": "您想如何開始?", "add_new_workflow": "新增工作流程", "reschedule_event_trigger": "當重新預定活動時", "trigger": "觸發條件", @@ -1722,8 +1716,6 @@ "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": "額外提醒", @@ -1761,7 +1753,6 @@ "team_url": "團隊網址", "team_members": "團隊成員", "more": "更多", - "cal_ai_workflows": "Cal.ai 工作流程", "and_count_more": "以及另外 {{count}} 項", "more_page_footer": "我們將行動應用程式視為 Web 應用程式的擴充項目。若要執行任何複雜的動作,請回到 Web 應用程式。", "workflow_example_1": "活動開始前 24 小時傳送簡訊提醒給與會者", @@ -1770,18 +1761,6 @@ "workflow_example_4": "在活動開始前 1 小時傳送電子郵件提醒給與會者", "workflow_example_5": "重新預定活動時傳送自訂電子郵件給主辦者", "workflow_example_6": "預約新活動時傳送自訂簡訊給主辦者", - "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": "您之後可以編輯此內容。", @@ -2205,8 +2184,6 @@ "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}} 擁有者,或請對方發佈。", @@ -2364,6 +2341,7 @@ "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": "天", @@ -2486,15 +2464,7 @@ "insights_team_filter": "團隊:{{teamName}}", "insights_user_filter": "使用者:{{userName}}", "insights_subtitle": "查看所有活動的預約 Insight", - "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": "選取幾個開放預約的時段,然後嵌入您的電子郵件", @@ -2782,8 +2752,6 @@ "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": "標記為未出席", @@ -3235,15 +3203,9 @@ "verify_email_change": "驗證電子郵件變更", "buy_credits": "購買點數", "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": "檢視與管理點數", "view_and_manage_credits_description": "檢視與管理用於發送 SMS 簡訊的點數。一點數等於 1 美分 (USD)。<0>了解更多", - "credit_worth_description": "一點數等於 1 美分 (USD)。<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": "您無法創建組織,因為您已經是某個組織的成員", @@ -3424,13 +3386,10 @@ "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": "將電子郵件回覆發送給", @@ -3508,11 +3467,6 @@ "pbac_desc_view_workflows": "檢視現有的工作流程及其設定", "pbac_desc_update_workflows": "編輯和修改工作流程設定", "pbac_desc_delete_workflows": "從系統中移除工作流程", - "pbac_resource_webhook": "Webhook", - "pbac_desc_create_webhooks": "建立 Webhook", - "pbac_desc_view_webhooks": "查看 Webhook", - "pbac_desc_update_webhooks": "更新 Webhooks", - "pbac_desc_delete_webhooks": "刪除 Webhooks", "pbac_desc_manage_workflows": "完全管理所有工作流程的存取權限", "pbac_desc_create_event_types": "建立活動類型", "pbac_desc_view_event_types": "檢視活動類型", @@ -3640,7 +3594,6 @@ "webhook_trigger_event": "觸發事件的名稱(例如:BOOKING_CREATED、BOOKING_CANCELLED)", "webhook_created_at": "Webhook 的建立時間", "webhook_type": "事件類型的代稱", - "set_up_agent": "設定代理", "webhook_title": "事件類型名稱", "webhook_start_time": "事件的開始時間", "webhook_end_time": "事件的結束時間", @@ -3672,9 +3625,6 @@ "visit": "造訪", "location_custom_label_input_label": "預約頁面的自訂標籤", "meeting_link": "會議連結", - "session_outcome": "會議結果", - "call_created": "通話已建立", - "voicemail": "語音信箱", "my_bookings": "我的預約", "phone": "電話", "free": "免費", @@ -3682,8 +3632,6 @@ "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": "考慮建立一個組織,以統一多個團隊的帳單、管理工具和分析。", @@ -3704,32 +3652,5 @@ "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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } \ No newline at end of file diff --git a/apps/web/scripts/copy-app-store-static.js b/apps/web/scripts/copy-app-store-static.js index 68d74babaa5297..440638fd54e0fe 100644 --- a/apps/web/scripts/copy-app-store-static.js +++ b/apps/web/scripts/copy-app-store-static.js @@ -20,7 +20,7 @@ const copyAppStoreStatic = () => { fs.mkdirSync(destDir, { recursive: true }); } - // Copy file to destination (Turborepo caching handles change detection) + // Copy file to destination const destPath = path.join(destDir, fileName); fs.copyFileSync(file, destPath); console.log(`Copied ${file} to ${destPath}`); diff --git a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts index a48bd543afec3b..37f7e6c7fade36 100644 --- a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts @@ -7,7 +7,7 @@ import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; import { shouldHideBrandingForUserEvent } from "@calcom/lib/hideBranding"; import { EventRepository } from "@calcom/lib/server/repository/event"; import { UserRepository } from "@calcom/lib/server/repository/user"; diff --git a/apps/web/server/lib/[user]/getServerSideProps.ts b/apps/web/server/lib/[user]/getServerSideProps.ts index 5e8cecaaad06e1..438527b083e808 100644 --- a/apps/web/server/lib/[user]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/getServerSideProps.ts @@ -4,9 +4,9 @@ import { encode } from "querystring"; import type { z } from "zod"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { getEventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic"; import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; +import { getEventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import logger from "@calcom/lib/logger"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index 0e1cb7e51c8db8..5939d70e782fb9 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -647,8 +647,3 @@ select:focus { [data-radix-popper-content-wrapper] { border: none; } - -html[dir="rtl"] .react-tel-input .flag-dropdown { - left: auto !important; - right: 0 !important; -} diff --git a/packages/features/eventtypes/lib/checkForEmptyAssignment.test.ts b/apps/web/test/lib/CheckForEmptyAssignment.test.ts similarity index 97% rename from packages/features/eventtypes/lib/checkForEmptyAssignment.test.ts rename to apps/web/test/lib/CheckForEmptyAssignment.test.ts index 3d1d30ba0631b9..3dbfce4c15380a 100644 --- a/packages/features/eventtypes/lib/checkForEmptyAssignment.test.ts +++ b/apps/web/test/lib/CheckForEmptyAssignment.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { checkForEmptyAssignment } from "./checkForEmptyAssignment"; +import { checkForEmptyAssignment } from "@calcom/lib/event-types/utils/checkForEmptyAssignment"; describe("Tests to Check if Event Types have empty Assignment", () => { it("should return true if managed event type has no assigned users", () => { diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index f53459e734feba..eec979b593224d 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -143,10 +143,9 @@ describe("handleChildrenEventTypes", () => { profileId: null, updatedValues: {}, }); - const { createdAt, updatedAt, ...expectedEvType } = evType; expect(prismaMock.eventType.create).toHaveBeenCalledWith({ data: { - ...expectedEvType, + ...evType, parentId: 1, users: { connect: [{ id: 4 }] }, lockTimeZoneToggleOnBookingPage: false, @@ -207,7 +206,7 @@ describe("handleChildrenEventTypes", () => { bookingLimits: undefined, }, }); - const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType; + const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { ...rest, @@ -318,10 +317,9 @@ describe("handleChildrenEventTypes", () => { profileId: null, updatedValues: {}, }); - const { createdAt, updatedAt, ...expectedEvType } = evType; expect(prismaMock.eventType.create).toHaveBeenCalledWith({ data: { - ...expectedEvType, + ...evType, parentId: 1, users: { connect: [{ id: 4 }] }, bookingLimits: undefined, @@ -385,7 +383,7 @@ describe("handleChildrenEventTypes", () => { length: 30, }, }); - const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType; + const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { ...rest, @@ -466,11 +464,7 @@ describe("handleChildrenEventTypes", () => { ...evType, }; - prismaMock.eventType.update.mockResolvedValue({ - ...mockUpdatedEventType, - createdAt: new Date(), - updatedAt: new Date(), - }); + prismaMock.eventType.update.mockResolvedValue(mockUpdatedEventType); await updateChildrenEventTypes({ eventTypeId: 1, @@ -486,10 +480,9 @@ describe("handleChildrenEventTypes", () => { updatedValues: {}, }); - const { createdAt, updatedAt, ...expectedEvType } = evType; expect(prismaMock.eventType.create).toHaveBeenCalledWith({ data: { - ...expectedEvType, + ...evType, bookingLimits: undefined, durationLimits: undefined, recurringEvent: undefined, @@ -514,7 +507,7 @@ describe("handleChildrenEventTypes", () => { allowReschedulingCancelledBookings: false, }, }); - const { profileId, rrSegmentQueryValue, createdAt: _, updatedAt: __, ...rest } = evType; + const { profileId, rrSegmentQueryValue, ...rest } = evType; if ("workflows" in rest) delete rest.workflows; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 7577bddfd8a1cc..1650471e6a6076 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -1534,7 +1534,6 @@ export function getOrganizer({ completedOnboarding, username, locked, - emailVerified, }: { name: string; email: string; @@ -1552,7 +1551,6 @@ export function getOrganizer({ completedOnboarding?: boolean; username?: string; locked?: boolean; - emailVerified?: Date | null; }) { username = username ?? TestData.users.example.username; return { @@ -1574,8 +1572,7 @@ export function getOrganizer({ smsLockState, completedOnboarding, locked, - emailVerified, - }; + }; } export function getScenarioData( diff --git a/docs/self-hosting/docker.mdx b/docs/self-hosting/docker.mdx index 84baa8a2fb4c4f..b1744034b5df11 100644 --- a/docs/self-hosting/docker.mdx +++ b/docs/self-hosting/docker.mdx @@ -3,15 +3,7 @@ title: "Docker" icon: "docker" --- -### Introduction - -This image can be found on DockerHub at https://hub.docker.com/r/calcom/cal.com. - -Note for ARM Users: Use the {version}-arm suffix for pulling images. Example: `docker pull calcom/cal.com:v5.6.19-arm`. - -### Contributing - -The Docker configuration for Cal.com is an effort powered by people within the community. Cal.com, Inc. does not yet provide official support for Docker, but we will accept fixes and documentation at this time. Use at your own risk. +The Docker configuration for Cal is an effort powered by people within the community. Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk. If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32). @@ -34,63 +26,24 @@ Note: `docker compose` without the hyphen is now the primary method of using doc 2. Change into the directory ``` - cd docker + cd calcom-docker ``` -3. Prepare your configuration: Rename .env.example to .env and then update .env - ``` - cp .env.example .env - ``` - Most configurations can be left as-is, but for configuration options see Important Run-time variables below. +4. Rename `.env.example` to `.env` and then update `.env` - Update the appropriate values in your .env file, then proceed. - -4. (optional) Pre-Pull the images by running the following command +5. Build and start Cal.com via docker compose ``` - docker compose pull + docker compose up --build ``` -5. Start Cal.com via docker compose - - (Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio: - ```bash - docker compose up -d - ``` - - To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run: - - ```bash - docker compose up -d calcom studio - ``` - - To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run: - - ```bash - docker compose up -d calcom - ``` - -6. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go! - -### Update Calcom Instance - -1. Stop the Cal.com stack - - ```bash - docker compose down - ``` - -2. Pull the latest changes - - ```bash - docker compose pull - ``` -3. Update environment variables as necessary. -4. Re-start the Cal.com stack +6. (First Run) Open a browser to [http://localhost:5555](http://localhost:5555/) to look at or modify the database content. + + a. Click on the `User` model to add a new user record. + + b. Fill out the fields (remembering to encrypt your password with [BCrypt](https://bcrypt-generator.com/)) and click `Save 1 Record` to create your first user. - ```bash - docker compose up -d - ``` +7. Open a browser to [http://localhost:3000](http://localhost:3000/) and login with your just created, first user. ### Configuration @@ -108,37 +61,8 @@ These variables must be provided at the time of the docker build, and can be pro * NEXTAUTH_SECRET -## Advanced Users - Building and Configuring - -Check out the [Cal.com Docker](https://github.com/calcom/docker) repository for more detailed instructions on how to build and configure your own Docker image. - -## Troubleshooting +### Troubleshooting * SSL edge termination: If running behind a load balancer which handles SSL certificates, you will need to add the environmental variable `NODE_TLS_REJECT_UNAUTHORIZED=0` to prevent requests from being rejected. Only do this if you know what you are doing and trust the services/load-balancers directing traffic to your service. * Failed to commit changes: Invalid 'prisma.user.create()': Certain versions may have trouble creating a user if the field `metadata` is empty. Using an empty json object `{}` as the field value should resolve this issue. Also, the `id` field will autoincrement, so you may also try leaving the value of `id` as empty. - -### CLIENT_FETCH_ERROR - -If you experience this error, it may be the way the default Auth callback in the server is using the WEBAPP_URL as a base url. The container does not necessarily have access to the same DNS as your local machine, and therefor needs to be configured to resolve to itself. You may be able to correct this by configuring `NEXTAUTH_URL=http://localhost:3000/api/auth`, to help the backend loop back to itself. -``` -docker-calcom-1 | @calcom/web:start: [next-auth][error][CLIENT_FETCH_ERROR] -docker-calcom-1 | @calcom/web:start: https://next-auth.js.org/errors#client_fetch_error request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost { -docker-calcom-1 | @calcom/web:start: error: { -docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost', -docker-calcom-1 | @calcom/web:start: stack: 'FetchError: request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost\n' + -docker-calcom-1 | @calcom/web:start: ' at ClientRequest. (/calcom/node_modules/next/dist/compiled/node-fetch/index.js:1:65756)\n' + -docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:events:513:28)\n' + -docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:domain:489:12)\n' + -docker-calcom-1 | @calcom/web:start: ' at Socket.socketErrorListener (node:_http_client:494:9)\n' + -docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:events:513:28)\n' + -docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:domain:489:12)\n' + -docker-calcom-1 | @calcom/web:start: ' at emitErrorNT (node:internal/streams/destroy:157:8)\n' + -docker-calcom-1 | @calcom/web:start: ' at emitErrorCloseNT (node:internal/streams/destroy:122:3)\n' + -docker-calcom-1 | @calcom/web:start: ' at processTicksAndRejections (node:internal/process/task_queues:83:21)', -docker-calcom-1 | @calcom/web:start: name: 'FetchError' -docker-calcom-1 | @calcom/web:start: }, -docker-calcom-1 | @calcom/web:start: url: 'http://testing.localhost:3000/api/auth/session', -docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost' -docker-calcom-1 | @calcom/web:start: } -``` diff --git a/i18n.lock b/i18n.lock index 8ea52f6e7652a5..eebe537a36312a 100644 --- a/i18n.lock +++ b/i18n.lock @@ -25,7 +25,7 @@ checksums: verify_email_banner_body: f259317dafe60ec0085915633f1c0258 verify_email_email_header: 5c96f738bd6153ee07b72094cdfd2b98 verify_email_button: 42dcab68d931f9145d9b6d76740a5c66 - cal_ai_assistant: 30a8b9d635a88b0226419e0670d4ca88 + cal_ai_assistant: b015d2022d06f05133b6fd6de0e04de7 send_cal_video_transcription_emails: 72bb1e50955446a1a3e69be24193578d description_send_cal_video_transcription_emails: c91f20c86fbaa270441badcb2b04edd9 verify_email_change_description: 21e50aab7ae115057a90847a36cf79ac @@ -821,8 +821,6 @@ checksums: workflow_validation_failed: 74ba4abc955bb78214c1e033cbd1a891 workflow_validation_empty_fields: f82bdee29ab695c6b8d4e6c2ba50144b workflow_validation_unverified_contacts: 128de2dffd10817a5523126fc91433d2 - supercharge_your_workflows_with_cal_ai: 5ed82ace50deacd1105e6e4c505b4225 - supercharge_your_workflows_with_cal_ai_description: 8a5199cd2900d54bf7d9336d196284fa phone_number_imported_successfully: d51423bb1e86b31624e23c81bfa551da phone_number_deleted_successfully: 2e5d42ae18b8fcd3712c9ed69e9126a0 delete_phone_number_confirmation: 51c31ee7b06bfa3e698a6eafeb439e25 @@ -850,7 +848,6 @@ checksums: phone_number_subscription_cancelled_successfully: 6b5eba60b290f87fd4ebb0cc99293ab4 updating: 68d418a669d7a521939549c5b980dc03 round_robin: 998652ffc86b77950604ad1e90e36ed8 - hi_how_are_you_doing: 1ee4bddac0904264d1651ff54b99629e round_robin_description: 9a76c2ae7f3c045538b682c198541e95 managed_event: 2625aa362fc478198a25fb1897ec8e1c username_placeholder: eb69c662ee8b49b0cd5828d6f5fe4229 @@ -881,9 +878,9 @@ checksums: are_you_sure_you_want_to_delete_workflow_step: 7137eb0693599e7d7b0d524f792b8d3c do_you_still_want_to_unsubscribe: 12443d9ae5e8a54543c441a3c59fe726 the_action_will_disconnect_phone_number: 8f626508df2b758d396fb0d8ef2fd242 - cal_ai_phone_numbers: e4c9c7f7f46b5ea5c995a67a20dd6fc8 + cal_ai_phone_numbers: 824e5942a561c9c5445a0af1bae8d466 connect_phone_number: 1627ad800f4c610cd1654aa7725a3e3d - cal_ai_phone_numbers_description: 59f87e6d53a6e964baa629d8b2cc5d28 + cal_ai_phone_numbers_description: 41a94164f44906a809f63859448ba0ff import_number: 3063d2a6c54ffbf708141c72ff630bde this_action_will_also: 077ee21f3fc73c649bac8517d96cfb2b import_phone_number: 8aa01175a06e6c257d7d0e19d6c4f9fa @@ -891,7 +888,7 @@ checksums: cancel_your_phone_number_subscription: a692df90016a5aefd6c482d20251a9bc delete_associated_phone_number: a69ba72aafedb813dfe09b66f2fe28c7 unauthorized_create_workflow: 3492d132fb4cc4c14137c11800a14b9c - import_phone_number_description: 451271aab09e14d90a38de7e56b2c6e1 + import_phone_number_description: 92f421b7539e3890902ac43b6effcd62 phone_number_cost: 767cbc244d1d59df8194f4c8f3ddc01d buy_new_number: d45bdeabb452da6a278059f5cfc8f23f buy_number_cost_x_per_month: f5f89e89314ab24af69eb48d5b42d5c0 @@ -902,7 +899,6 @@ checksums: yes_cancel_subscription: b0b585aad2d0904c62135a4114cfae40 cancel_phone_number_subscription_confirmation: c523ba7295a60c46d00c729b27c6fab5 add_members: 496492b2ecacc2650c29b325ef84f490 - add_members_no_ellipsis: 5222e45d220123cf8b27e4e608776606 no_assigned_members: 5e0514e0e48908c2f444e533fddc249e assigned_to: 34d9e500c2308e0e23d003919f5ca592 you_must_be_logged_in_to: 9b5bf857507887ee5b2ed05dbac7bf41 @@ -1213,7 +1209,6 @@ checksums: categories: fd4e44f3b1b2bba9ca45f3aef963d042 pricing: ce27f1aeacccc542a174c4b2bce022b0 learn_more: e598091d132f890c37a6d4ed94f6d794 - try_now: 18bede34413f128409b9c469890a6bb0 privacy_policy: 7459744a63ef8af4e517a09024bd7c08 terms_of_service: 5add91f519e39025708e54a7eb7a9fc5 remove: dba2fe5fe9f83f8078c687f28cba4b52 @@ -1446,7 +1441,6 @@ checksums: whatsapp_attendee_action: e2228eb197bdff521e7f5580de775629 workflows: b0c9c8615a9ba7d9cb73e767290a7f72 new_workflow_btn: 39cf03f117133354542d7937f1d61861 - how_would_you_like_to_start: 118de435d9585cb475d85dbae1f331f8 add_new_workflow: fc34bdfa29921346715ef27d4e520dbd reschedule_event_trigger: 5a8f239d2d347402e65f0ccf41a4893a trigger: 25f7594d1ac2f32a3d2774dcd11dddfe @@ -1723,8 +1717,6 @@ checksums: event_duration_info: 2ed8fb3b9b152602e140a9c4e34ac9bd event_time_info: a000c4195df3733507313d8312f7f1d5 event_type_not_found: 51890a3a3fd93d173795a3f3196da032 - number_to_call_variable: 84189264e7c5495f1745bcafbcba265a - number_to_call_info: 1df6ab71b347fc8960281566348aa4f3 location_variable: 036cbdfe32bbe9ad67b5d06ef6a02a16 location_info: 38a03a8e5d83713221944a25250d1e51 additional_notes_variable: 416243d69874a5fd915db4c16a5311cc @@ -1762,7 +1754,6 @@ checksums: team_url: 29e1dfe4aef6e2ad84f0828ad60b1d3b team_members: d148d3e9f3d013feddc4f41bb620ec28 more: ee5e035ee328a8947be2ea44e338a57d - cal_ai_workflows: fcb65f5d7a888d40b26600af2c45b8b6 and_count_more: 307b4b3b3d8b441b35142907df4197fa more_page_footer: e2deddfbcc810846fd0cded621687b73 workflow_example_1: 5f72e83ced3d7155fbf3d5bbc86ac868 @@ -1771,18 +1762,6 @@ checksums: workflow_example_4: 05ec3b7900f5e4c19546914407ae1d8c workflow_example_5: d2ee89e4551e043a4eeefc44b71d66f9 workflow_example_6: 59fa576aa1cd974f666f4227c12b839b - send_sms_reminder: d66cfcfe2a54385153414ca2459a5d78 - send_sms_reminder_description: be749047a34cdd1e4968c0e8261b920a - follow_up_with_no_shows: 2dfd34cd3a38688667fa1ffdd28b6c56 - follow_up_with_no_shows_description: fa0fe338c08c0486818b3b348c92575e - remind_attendees_to_bring_id: adadc5588109a7d0d497a5f45d7216bd - remind_attendees_to_bring_id_description: 8e5ef7fc27a2d5cd28203606631fbf82 - email_to_remind_booking: 046c13edbe1fc8272651e185956be615 - email_to_remind_booking_description: 43669d1c43190b59cff4ae442a492657 - custom_sms_reminder: 852687f645f3dac9181ee93d4c3b648a - custom_sms_reminder_description: 6cb0a7f5f616012fb8ff6602cd383740 - custom_email_reminder: b819be10935f4839e1ecfb71fc861eb5 - custom_email_reminder_description: 6b6b94dad20aa0ad0265f02770ca7f33 count_managed_to_limit: b0ea1798bda7d078e5e8afa7a45ed386 welcome_to_cal_header: 7a43302ac23af654e035160bdbd47115 edit_form_later_subtitle: 1365b273b1e345edfdce9c903fd0aa64 @@ -2202,8 +2181,6 @@ checksums: show_on_booking_page: 515d42a965a615916eb9299f9a8ba698 visit_cancelled_booking: 42c1a6ee7e33ca49661c4e4763feb9e3 get_started_zapier_templates: 0adfac5a026b181a55ff131b9572b5ab - standard_templates: af5d213fca8bf19225c3ecf6bc1df04a - cal_ai_templates: 3154b221e2083fe0df91c8d98bf8b25c team_is_unpublished: 57d5a8b888c52b11062c208a1c4f5146 org_is_unpublished_description: 61c9364cb8fba87d1e442db181155c1e team_is_unpublished_description: dc44c314e2e3ad208b06f6710e67809d @@ -2361,6 +2338,7 @@ checksums: could_not_charge_card: eca4c10d6b6819b2cebb9dc7ecf75f17 insights: 1e88b2537ba6966f2a90b0def1ce1ddc routing_forms: d247797af01c33bcf3545d9708f39286 + testing_workflow_info_message: 63eb6b5f857e4fab7eae515a85e6bed5 insights_no_data_found_for_filter: af12cfd502181543842597325894bb30 acknowledge_booking_no_show_fee: 392224bac006a342a7fb63dad685689f days: c95fe8aedde21a0b5653dbd0b3c58b48 @@ -2483,15 +2461,7 @@ checksums: insights_team_filter: 15f4ac2dce74a6a237c563dd623cbc31 insights_user_filter: 36bccd6efac875b31245326c012987c1 insights_subtitle: 38559f7536fd820961908b14efe9c6f9 - call_history: 7c1db69ded3d8bba17241bc14cf25cfa - call_history_subtitle: 7ab0c247977ddc49e55e2dd81cf4991f location_options: 5b0a1f717ef185b10b44002569a1aba0 - channel_type: a1b582bec957f395fbe0ad4ad6472414 - end_reason: 94e9929d39276939f9935e5d36c2a1c6 - session_status: b547de4658ef48374dc0d9d3931b52f9 - user_sentiment: 6d5266e52cd715f6fd3880f8d0b23663 - time_header: b504a03d52e8001bfdc5cb6205364f42 - from_header: 3d84daca8c92c8609deeab4b294b4afb custom_plan: 4d406f4ff899f0da4e3711763599a9aa email_embed: 01145dc429b3f8050733f1d4b6d71d9d add_times_to_your_email: 572a04f56df1ec2623ea7ee8c9c9c5bb @@ -2779,8 +2749,6 @@ checksums: account_already_linked: 26bea87b637a83247e0975aefc091bda send_email: 0ef83c0bb40de25921a9ee7fa05babec cal_ai_phone_call_action: c7cb094eec2df48e1741afde2283da13 - call_to_confirm_booking: 1c1929934d2f3b120b354c463920452d - cal_ai_phone_call_action_description: 38ab6d1042ea764dd8c09fa76d0b80f8 cal_ai_agent_configuration: e18e1bc9e4b55b651e2687fa2c5d6935 choose_at_least_one_event_type_test_call: d3246dc88ce3534d2b8c2ca12b47edb4 mark_as_no_show: c9b37eb27f7fb452f7e11c23bc496ac6 @@ -3232,15 +3200,9 @@ checksums: verify_email_change: b7a83c7a3100b128e8d1a8f36a8171ba buy_credits: d5c34a6e8829e3a475dd5cda6d7e10ac credits: b21b88917ea0c8e2a71888756f71d0be - credits_used: 16485942c8499a1a7c1f8687bcd6565f - total_credits_remaining: bcc2d21f1843aa761ad394ed67ca8720 - credits_per_tip_org: 379de9ef3094d35104b55fa18bfca012 - credits_per_tip_teams: b54ff6fae0c83fb2040e827e442038eb - view_and_manage_credits: 6ed6c8ba6e2f5472ee4489002b5d282f + view_and_manage_credits: 4d483027287e2349b3d68cd0643c96f8 view_and_manage_credits_description: d6cdd13f5eec3810bc0c697d3ca4dd0e - credit_worth_description: 62c0d21706a8e70c4072d1785d5cbc1d buy_additional_credits: 4a3b39af3e55b16d932bb0b4b3e0f8b1 - view_additional_credits_expense_tip: 8fcfc210e71406f039c7f42b86cbd708 overview: 30c54e4dc4ce599b87d94be34a8617f5 organization_slug_taken: 492cc9dd76b78c9210c0c7dfafbfd040 you_cannot_create_an_organization_as_you_are_already_part_of_an_organization: f7df69b7f201bdfc1f26eb41006102a2 @@ -3421,13 +3383,10 @@ checksums: credit_limit_reached_message: d12b88a279d619ad18ff3c286ebec2b3 credit_limit_reached_message_user: d5446b226f841605083f7b2dd7b81065 current_credit_balance: 53c4f4d7c65f327c56483735a5df9831 - current_balance: 88bcb1c91fad643bb53d2df22f6cd29f notification_about_your_booking: a98d4c351d6ec6042e488f90b0864d41 monthly_credits: a95229fd9020089dfad6fb6cd92f7952 total_credits: b834a4f211978381eca9bdc33ea5e357 remaining_credits: 60a3f4d78699e2e60c69da9cecde9226 - remaining: 5db6eb99c266450c802a2bd345e5dce0 - total: f60dc0a14e9b1bace656c644de25de5b additional_credits: 1c05adf1d03583d8ae1bbdb4ed609687 routing_form_next_in_queue: 47946873f93e70e61476646edc7e5f1e routing_form_select_members_to_email: be35ce1ba18d8d6ed66c043cfe812a8b @@ -3505,11 +3464,6 @@ checksums: pbac_desc_view_workflows: 4e1394ded2ee6fe6d87601a4915b08d4 pbac_desc_update_workflows: aecf636063ef3b8a5ece8d6381e053f0 pbac_desc_delete_workflows: fabfe2406f079e08cf4b017afca797bd - pbac_resource_webhook: 70f95b2c27f2c3840b500fcaf79ee83c - pbac_desc_create_webhooks: 3df3e99c62fd661d12ae4d1817deec6c - pbac_desc_view_webhooks: c3c1a5113700fc39aff9596d29371ce5 - pbac_desc_update_webhooks: efe117c23053d397589dd9bc7e18d6dd - pbac_desc_delete_webhooks: 7877bc06e4a68ba21abf5ff702416b79 pbac_desc_manage_workflows: 9ff3998b178daad7fa575d1277345d31 pbac_desc_create_event_types: 0a58f83b33aa6ba7784de540b3107bbe pbac_desc_view_event_types: 390e116e8ff1ec3db6dc5079aaa80b7b @@ -3637,7 +3591,6 @@ checksums: webhook_trigger_event: c53db8127faf8f90ce9b59ff51ba69de webhook_created_at: d076c414e2d53c0c9fbd74e4af4eea95 webhook_type: fef7438e12f0d54e161afddd3d035031 - set_up_agent: 141aa933e5d8ef77b26fa187626a8fc4 webhook_title: aadf152d45b489f9ea2abeb55cd232db webhook_start_time: 294866d2c6a0929d39c56793c33df16e webhook_end_time: a72052fa163d4ad56a4fd8cc7f9f2c20 @@ -3669,9 +3622,6 @@ checksums: visit: 654d6dc335d93b0d9ac26f296bbf53cc location_custom_label_input_label: 944fd3bbb28cf884e36ba96ed2186c1c meeting_link: 892205c2e48aad28d01f62b8ef621560 - session_outcome: 9d2b5201132d2547ca3a39a9957f05a3 - call_created: 371c864ee5ae4d4fae314b1f7b41d2c2 - voicemail: 2af8872401efb9118756a773f706564d my_bookings: 2c5d89030d06458a78418ecf8cb8b372 phone: b9537ee90fc5b0116942e0af29d926cc free: 0326365539c004f6088656f692602078 @@ -3679,8 +3629,6 @@ checksums: user_name: b8d606a2f196f9efa369673c4981bdce expand_panel: d48708ca0c5c9ac3b0c25fd3165ace79 collapse_panel: 1c0fae9559e7e7b95b0009a2b96a47e2 - email_verification_required: 8fffa36870b7be7ffc7cade25e5c861a - invalid_verification_code: 53717540780ef28331e50a85396c814c you_have_one_team: 77ca75609e3693390e756100580f8d19 consider_consolidating_one_team_org: 17f63fc8749f3a2caac9d545bba4a511 consider_consolidating_multi_team_org: c800dc24c5b9d2e7551e00d4550ffbe1 @@ -3701,31 +3649,4 @@ checksums: before_scheduled_start_time: 35bbe398965e631eaf969ee89266ee84 cancel_booking_acknowledge_no_show_fee: 6e30c82004a60d5ec2daa40d93d3cce0 contact_organizer: d4d89322dbb7d3b447a45e09ffd48ba8 - booking_time_option: 7e06938417d0a12c1e8715ed48d20089 - booking_time_option_description: d95fcc1ab05319e2353b77cee04e185d - created_at_option: f98a3c3bc73494df9bbc14e4e47a018c - created_at_option_description: 56e6aa8ee2f0a994f2b48502b5ca00fd - call_details: 9e86ee5b1e7256e49bac97c9a7f1ea09 - call_id: 10b919948a3e41dca63ed01030298480 - call_information: 675d0f4210337e0931f0791645fd9873 - sentiment: 9ba5719c80c0136c2d0644217619aff6 - disconnect_reason: 14811352d9970ed1a7a5bed39b38254a - call_summary: e788600ac5116d7ad84b3498d65275af - transcription: 3e16d3e59834a54b196314e721c144c7 - event_details: 6f10d37f9d14b662b4ebc60d92e0f7c0 - agent: a5c36b9f3c5e71bfa2aa0cb196654512 - no_transcript_available: 4a31a08fa738150c97c8703f05fd11df - testing_sms_workflow_info_message: 23a2702763ff6f8a6c0e794581348bf3 - start_from_scratch_title: 6fc756927ca9ea22c26368cccd64a67e - start_from_scratch_description: 6e326932a833c6c175a5ba0eaa36267b - cal_ai_template_title: ce36995d69f1b83e111499976eed892a - cal_ai_template_description: ec18833a4c11e6a86711e5176ad7ebb6 - voice: 6c4e6b23882181c495f341063522e277 - select_voice: 5438c1ef61f6a67ccee96cb32982b35a - select_voice_for_agent: 35304e60c9e6007ceec8389ae2808016 - choose_a_voice_for_your_agent: 3b10b684c634c2749262bcd515afc584 - trait: cb30909e92dc4c76c7863377b1abbcec - voice_id: edd27f073cc7189691a860fba3d55ea4 - use_voice: 7febf772a86e81d908386ce0d2979588 - current_voice: db1c2997b80b16eaf100733500e3b3ca ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS: 18323f2f3fabb169a7cb50ff62433850 diff --git a/package.json b/package.json index 0d2e4acb200281..50620cb561fd5d 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "devDependencies": { "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "2.29.4", - "@faker-js/faker": "9.2.0", "@jetstreamapp/soql-parser-js": "^6.1.0", "@playwright/test": "^1.45.3", "@snaplet/copycat": "^4.1.0", diff --git a/packages/lib/currencyConversions.ts b/packages/app-store/_utils/payments/currencyConversions.ts similarity index 80% rename from packages/lib/currencyConversions.ts rename to packages/app-store/_utils/payments/currencyConversions.ts index 5a3c1b9b497ffa..da5c5e539abd6f 100644 --- a/packages/lib/currencyConversions.ts +++ b/packages/app-store/_utils/payments/currencyConversions.ts @@ -56,16 +56,3 @@ export const getCurrencySymbol = (currencyCode: string): string => { return "$"; } }; - -export const formatPrice = (price: number, currency: string | undefined, locale = "en") => { - switch (currency) { - case "BTC": - return `${price} sats`; - default: - currency = currency?.toUpperCase() || "USD"; - return `${Intl.NumberFormat(locale, { - style: "currency", - currency: currency, - }).format(convertFromSmallestToPresentableCurrencyUnit(price, currency))}`; - } -}; diff --git a/packages/app-store/_utils/payments/handlePaymentSuccess.ts b/packages/app-store/_utils/payments/handlePaymentSuccess.ts index dda7a8c9af7451..ef8ef72672cde4 100644 --- a/packages/app-store/_utils/payments/handlePaymentSuccess.ts +++ b/packages/app-store/_utils/payments/handlePaymentSuccess.ts @@ -7,7 +7,7 @@ import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirma import { getBooking } from "@calcom/features/bookings/lib/payment/getBooking"; import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params"; import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository"; -import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; +import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager"; import { HttpError as HttpCode } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; diff --git a/packages/app-store/_utils/setDefaultConferencingApp.ts b/packages/app-store/_utils/setDefaultConferencingApp.ts index 8d4c3059178819..deeac5f2b90c0a 100644 --- a/packages/app-store/_utils/setDefaultConferencingApp.ts +++ b/packages/app-store/_utils/setDefaultConferencingApp.ts @@ -1,10 +1,9 @@ +import type { LocationObject } from "@calcom/app-store/locations"; +import { getAppFromSlug } from "@calcom/app-store/utils"; +import { getBulkUserEventTypes } from "@calcom/lib/event-types/getBulkEventTypes"; import prisma from "@calcom/prisma"; import { userMetadata } from "@calcom/prisma/zod-utils"; -import type { LocationObject } from "../locations"; -import { getAppFromSlug } from "../utils"; -import { getBulkUserEventTypes } from "./getBulkEventTypes"; - const setDefaultConferencingApp = async (userId: number, appSlug: string) => { const eventTypes = await getBulkUserEventTypes(userId); const eventTypeIds = eventTypes.eventTypes.map((item) => item.id); diff --git a/packages/app-store/hitpay/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/hitpay/components/EventTypeAppSettingsInterface.tsx index 2388a47d0d66c9..daceb2899d227a 100644 --- a/packages/app-store/hitpay/components/EventTypeAppSettingsInterface.tsx +++ b/packages/app-store/hitpay/components/EventTypeAppSettingsInterface.tsx @@ -1,15 +1,15 @@ import { useState, useEffect } from "react"; import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; -import { - convertToSmallestCurrencyUnit, - convertFromSmallestToPresentableCurrencyUnit, -} from "@calcom/lib/currencyConversions"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Alert } from "@calcom/ui/components/alert"; import { Select } from "@calcom/ui/components/form"; import { TextField } from "@calcom/ui/components/form"; +import { Alert } from "@calcom/ui/components/alert"; +import { + convertToSmallestCurrencyUnit, + convertFromSmallestToPresentableCurrencyUnit, +} from "../lib/currencyConversions"; import { paymentOptions, currencyOptions } from "./constants"; const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 8ff7fed9144072..3be5e783f22b92 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -2,7 +2,6 @@ * TODO: Consolidate this file with BookingLocationService and add tests */ import type { TFunction } from "i18next"; -import { isValidPhoneNumber } from "libphonenumber-js"; import { z } from "zod"; import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData"; @@ -503,66 +502,3 @@ export const isAttendeeInputRequired = (locationType: string) => { } return location.attendeeInputType; }; - -export const locationsResolver = (t: TFunction) => { - return z - .array( - z - .object({ - type: z.string(), - address: z.string().optional(), - link: z.string().url().optional(), - phone: z - .string() - .refine((val) => isValidPhoneNumber(val)) - .optional(), - hostPhoneNumber: z - .string() - .refine((val) => isValidPhoneNumber(val)) - .optional(), - displayLocationPublicly: z.boolean().optional(), - credentialId: z.number().optional(), - teamName: z.string().optional(), - }) - .passthrough() - .superRefine((val, ctx) => { - if (val?.link) { - const link = val.link; - const eventLocationType = getEventLocationType(val.type); - if ( - eventLocationType && - !eventLocationType.default && - eventLocationType.linkType === "static" && - eventLocationType.urlRegExp - ) { - const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(link).success; - - if (!valid) { - const sampleUrl = eventLocationType.organizerInputPlaceholder; - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [eventLocationType?.defaultValueVariable ?? "link"], - message: t("invalid_url_error_message", { - label: eventLocationType.label, - sampleUrl: sampleUrl ?? "https://cal.com", - }), - }); - } - return; - } - - const valid = z.string().url().optional().safeParse(link).success; - - if (!valid) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [eventLocationType?.defaultValueVariable ?? "link"], - message: `Invalid URL`, - }); - } - } - return; - }) - ) - .optional(); -}; diff --git a/packages/app-store/paypal/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/paypal/components/EventTypeAppSettingsInterface.tsx index 7663c552c02ffa..dbcbc476e5a809 100644 --- a/packages/app-store/paypal/components/EventTypeAppSettingsInterface.tsx +++ b/packages/app-store/paypal/components/EventTypeAppSettingsInterface.tsx @@ -6,15 +6,15 @@ import { isAcceptedCurrencyCode, } from "@calcom/app-store/paypal/lib/currencyOptions"; import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; -import { - convertToSmallestCurrencyUnit, - convertFromSmallestToPresentableCurrencyUnit, -} from "@calcom/lib/currencyConversions"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Alert } from "@calcom/ui/components/alert"; import { Select } from "@calcom/ui/components/form"; import { TextField } from "@calcom/ui/components/form"; +import { Alert } from "@calcom/ui/components/alert"; +import { + convertToSmallestCurrencyUnit, + convertFromSmallestToPresentableCurrencyUnit, +} from "../../_utils/payments/currencyConversions"; import { PaypalPaymentOptions as paymentOptions } from "../zod"; type Option = { value: string; label: string }; diff --git a/packages/app-store/stripepayment/api/__tests__/portal.test.ts b/packages/app-store/stripepayment/api/__tests__/portal.test.ts deleted file mode 100644 index 8c13dc3c14befd..00000000000000 --- a/packages/app-store/stripepayment/api/__tests__/portal.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import type { NextApiRequest } from "next"; -import type { Session } from "next-auth"; -import { describe, it, expect, vi, beforeEach } from "vitest"; - -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { TeamRepository } from "@calcom/lib/server/repository/team"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { - BillingPortalServiceFactory, - TeamBillingPortalService, - OrganizationBillingPortalService, - UserBillingPortalService, -} from "../../lib/BillingPortalService"; -import * as customerModule from "../../lib/customer"; -import { validateAuthentication, buildReturnUrl } from "../portal"; - -// Mock dependencies -vi.mock("@calcom/features/pbac/services/permission-check.service"); -vi.mock("@calcom/lib/server/repository/team"); -vi.mock("../../lib/customer"); -vi.mock("../../lib/server"); -vi.mock("../../lib/subscriptions"); -vi.mock("@calcom/prisma", () => ({ - default: {}, -})); - -const mockPermissionService = vi.mocked(PermissionCheckService); -const mockTeamRepository = vi.mocked(TeamRepository); -const mockCustomerModule = vi.mocked(customerModule); - -interface RequestWithSession extends NextApiRequest { - session?: Session | null; -} - -interface MockPermissionService { - checkPermission: ReturnType; -} - -interface MockTeamRepository { - findById: ReturnType; -} - -describe("Portal API - Service-Based Architecture", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("validateAuthentication", () => { - it("should return user when session exists", () => { - const req = { - session: { - user: { id: 123 }, - hasValidLicense: true, - upId: "test-upid", - expires: "2024-12-31T23:59:59.999Z" - } as Session, - } as RequestWithSession; - - const result = validateAuthentication(req as NextApiRequest); - - expect(result).toEqual({ id: 123 }); - }); - - it("should return null when session is missing", () => { - const req = {} as NextApiRequest; - - const result = validateAuthentication(req); - - expect(result).toBeNull(); - }); - - it("should return null when user id is missing", () => { - const req = { - session: { - user: {} as any, - hasValidLicense: true, - upId: "test-upid", - expires: "2024-12-31T23:59:59.999Z" - } as Session, - } as RequestWithSession; - - const result = validateAuthentication(req as NextApiRequest); - - expect(result).toBeNull(); - }); - }); - - describe("buildReturnUrl", () => { - it("should return default URL when returnTo is not provided", () => { - const result = buildReturnUrl(); - - expect(result).toBe(`${WEBAPP_URL}/settings/billing`); - }); - - it("should return default URL when returnTo is not a string", () => { - const result = buildReturnUrl(123 as unknown as string); - - expect(result).toBe(`${WEBAPP_URL}/settings/billing`); - }); - - it("should return safe redirect URL when valid returnTo is provided", () => { - const returnTo = `${WEBAPP_URL}/settings/teams`; - - const result = buildReturnUrl(returnTo); - - expect(result).toBe(`${WEBAPP_URL}/settings/teams`); - }); - - it("should return WEBAPP_URL root when unsafe redirect URL is provided", () => { - const returnTo = "http://malicious-site.com"; - - const result = buildReturnUrl(returnTo); - - expect(result).toBe(`${WEBAPP_URL}/`); - }); - }); - - describe("BillingPortalServiceFactory", () => { - let mockTeamRepo: MockTeamRepository; - - beforeEach(() => { - mockTeamRepo = { - findById: vi.fn(), - }; - mockTeamRepository.mockImplementation(() => mockTeamRepo as unknown as TeamRepository); - }); - - it("should create OrganizationBillingPortalService for organizations", async () => { - const mockTeam = { id: 1, isOrganization: true, metadata: {} }; - mockTeamRepo.findById.mockResolvedValue(mockTeam); - - const service = await BillingPortalServiceFactory.createService(1); - - expect(service).toBeInstanceOf(OrganizationBillingPortalService); - }); - - it("should create TeamBillingPortalService for regular teams", async () => { - const mockTeam = { id: 1, isOrganization: false, metadata: {} }; - mockTeamRepo.findById.mockResolvedValue(mockTeam); - - const service = await BillingPortalServiceFactory.createService(1); - - expect(service).toBeInstanceOf(TeamBillingPortalService); - }); - - it("should throw error when team not found", async () => { - mockTeamRepo.findById.mockResolvedValue(null); - - await expect(BillingPortalServiceFactory.createService(1)).rejects.toThrow("Team not found"); - }); - - it("should create UserBillingPortalService", () => { - const service = BillingPortalServiceFactory.createUserService(); - - expect(service).toBeInstanceOf(UserBillingPortalService); - }); - }); - - describe("TeamBillingPortalService", () => { - let service: TeamBillingPortalService; - let mockPermissionServiceInstance: MockPermissionService; - - beforeEach(() => { - mockPermissionServiceInstance = { - checkPermission: vi.fn().mockResolvedValue(true), - }; - - vi.mocked(PermissionCheckService).mockImplementation( - () => mockPermissionServiceInstance as unknown as PermissionCheckService - ); - - const mockTeamRepo: MockTeamRepository = { - findById: vi.fn(), - }; - mockTeamRepository.mockImplementation(() => mockTeamRepo as unknown as TeamRepository); - - service = new TeamBillingPortalService(); - }); - - it("should check team.manageBilling permission", async () => { - const result = await service.checkPermissions(123, 456); - - expect(mockPermissionServiceInstance.checkPermission).toHaveBeenCalledWith({ - userId: 123, - teamId: 456, - permission: "team.manageBilling", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - expect(result).toBe(true); - }); - }); - - describe("OrganizationBillingPortalService", () => { - let service: OrganizationBillingPortalService; - let mockPermissionServiceInstance: MockPermissionService; - - beforeEach(() => { - mockPermissionServiceInstance = { - checkPermission: vi.fn().mockResolvedValue(true), - }; - - vi.mocked(PermissionCheckService).mockImplementation( - () => mockPermissionServiceInstance as unknown as PermissionCheckService - ); - - service = new OrganizationBillingPortalService(); - }); - - it("should check organization.manageBilling permission", async () => { - const result = await service.checkPermissions(123, 456); - - expect(mockPermissionServiceInstance.checkPermission).toHaveBeenCalledWith({ - userId: 123, - teamId: 456, - permission: "organization.manageBilling", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - expect(result).toBe(true); - }); - }); - - describe("UserBillingPortalService", () => { - let service: UserBillingPortalService; - - beforeEach(() => { - service = new UserBillingPortalService(); - mockCustomerModule.getStripeCustomerIdFromUserId = vi.fn(); - }); - - it("should get customer ID for user", async () => { - mockCustomerModule.getStripeCustomerIdFromUserId.mockResolvedValue("cus_123"); - - const result = await service.getCustomerId(123); - - expect(result).toBe("cus_123"); - expect(mockCustomerModule.getStripeCustomerIdFromUserId).toHaveBeenCalledWith(123); - }); - }); -}); diff --git a/packages/app-store/stripepayment/api/portal.ts b/packages/app-store/stripepayment/api/portal.ts index 2434bc96840a74..85fc320295bb34 100644 --- a/packages/app-store/stripepayment/api/portal.ts +++ b/packages/app-store/stripepayment/api/portal.ts @@ -1,59 +1,87 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import type { Session } from "next-auth"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import logger from "@calcom/lib/logger"; +import { TeamRepository } from "@calcom/lib/server/repository/team"; +import prisma from "@calcom/prisma"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; -import { BillingPortalServiceFactory } from "../lib/BillingPortalService"; +import { getStripeCustomerIdFromUserId } from "../lib/customer"; +import stripe from "../lib/server"; +import { getSubscriptionFromId } from "../lib/subscriptions"; -interface AuthenticatedUser { - id: number; -} - -interface RequestWithSession extends NextApiRequest { - session?: Session | null; -} +const getBillingPortalUrl = async (customerId: string, return_url: string) => { + const log = logger.getSubLogger({ prefix: ["getBillingPortalUrl"] }); + try { + const portalSession = await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url, + }); -export const validateAuthentication = (req: NextApiRequest): AuthenticatedUser | null => { - const userId = (req as RequestWithSession).session?.user?.id; - if (!userId) return null; - return { id: userId }; + return portalSession.url; + } catch (e) { + log.error(`Failed to create billing portal session for ${customerId}: ${e}`); + throw new Error("Failed to create billing portal session"); + } }; -export const buildReturnUrl = (returnTo?: string): string => { - const defaultUrl = `${WEBAPP_URL}/settings/billing`; +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST" && req.method !== "GET") + return res.status(405).json({ message: "Method not allowed" }); + + if (!req.session?.user?.id) return res.status(401).json({ message: "Not authenticated" }); - if (typeof returnTo !== "string") return defaultUrl; + const userId = req.session.user.id; + const teamId = req.query.teamId ? parseInt(req.query.teamId as string) : null; + let return_url = `${WEBAPP_URL}/settings/billing`; + if (!teamId) { + const customerId = await getStripeCustomerIdFromUserId(userId); + if (!customerId) return res.status(404).json({ message: "CustomerId not found" }); - const safeRedirectUrl = getSafeRedirectUrl(returnTo); - return safeRedirectUrl || defaultUrl; -}; + const billingPortalUrl = await getBillingPortalUrl(customerId, return_url); -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== "POST" && req.method !== "GET") { - return res.status(405).json({ message: "Method not allowed" }); + return res.redirect(302, billingPortalUrl); } - const user = validateAuthentication(req); - if (!user) { - return res.status(401).json({ message: "Not authenticated" }); + const teamRepository = new TeamRepository(prisma); + const team = await teamRepository.getTeamByIdIfUserIsAdmin({ + teamId, + userId, + }); + + if (!team) return res.status(404).json({ message: "Team not found" }); + + if (typeof req.query.returnTo === "string") { + const safeRedirectUrl = getSafeRedirectUrl(req.query.returnTo); + if (safeRedirectUrl) return_url = safeRedirectUrl; } - const teamId = req.query.teamId ? parseInt(req.query.teamId as string) : null; - const returnUrl = buildReturnUrl(req.query.returnTo as string); + const teamMetadataParsed = teamMetadataSchema.safeParse(team.metadata); - try { - if (!teamId) { - const userService = BillingPortalServiceFactory.createUserService(); - return await userService.processBillingPortal(user.id, returnUrl, res); - } - - const billingService = await BillingPortalServiceFactory.createService(teamId); - return await billingService.processBillingPortal(user.id, teamId, returnUrl, res); - } catch (error) { - if (error instanceof Error && error.message === "Team not found") { - return res.status(404).json({ message: "Team not found" }); - } - throw error; + if (!teamMetadataParsed.success) { + return res.status(400).json({ message: "Invalid team metadata" }); + } + + if (!teamMetadataParsed.data?.subscriptionId) { + return res.status(400).json({ message: "subscriptionId not found for team" }); + } + + const subscription = await getSubscriptionFromId(teamMetadataParsed.data.subscriptionId); + + if (!subscription) { + return res.status(400).json({ message: "Subscription not found" }); } + + if (!subscription.customer) { + return res.status(400).json({ message: "Subscription customer not found" }); + } + + const customerId = subscription.customer as string; + + if (!customerId) return res.status(400).json({ message: "CustomerId not found in stripe" }); + + const billingPortalUrl = await getBillingPortalUrl(customerId, return_url); + + res.redirect(302, billingPortalUrl); } diff --git a/packages/app-store/stripepayment/api/subscription.ts b/packages/app-store/stripepayment/api/subscription.ts index 07914b85354155..dfb31e60b6dced 100644 --- a/packages/app-store/stripepayment/api/subscription.ts +++ b/packages/app-store/stripepayment/api/subscription.ts @@ -2,7 +2,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import type Stripe from "stripe"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername"; import { WEBAPP_URL } from "@calcom/lib/constants"; import prisma from "@calcom/prisma"; @@ -13,12 +12,11 @@ import stripe from "../lib/server"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "GET") { - const session = await getServerSession({ req }); - const userId = session?.user?.id; + const userId = req.session?.user.id; let { intentUsername = null } = req.query; const { callbackUrl } = req.query; if (!userId || !intentUsername) { - res.status(404).json({ message: "Missing required parameters: userId or intentUsername" }); + res.status(404).end(); return; } if (intentUsername && typeof intentUsername === "object") { diff --git a/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx index ff048ed45cf63c..7d320576afb347 100644 --- a/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx +++ b/packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx @@ -2,10 +2,6 @@ import * as RadioGroup from "@radix-ui/react-radio-group"; import { useState, useEffect } from "react"; import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; -import { - convertToSmallestCurrencyUnit, - convertFromSmallestToPresentableCurrencyUnit, -} from "@calcom/lib/currencyConversions"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { RefundPolicy } from "@calcom/lib/payment/types"; import classNames from "@calcom/ui/classNames"; @@ -15,6 +11,10 @@ import { CheckboxField } from "@calcom/ui/components/form"; import { TextField } from "@calcom/ui/components/form"; import { RadioField } from "@calcom/ui/components/radio"; +import { + convertToSmallestCurrencyUnit, + convertFromSmallestToPresentableCurrencyUnit, +} from "../../_utils/payments/currencyConversions"; import { paymentOptions } from "../lib/constants"; import { currencyOptions } from "../lib/currencyOptions"; import { autoChargeNoShowFeeTimeUnitEnum } from "../zod"; diff --git a/packages/app-store/stripepayment/lib/BillingPortalService.ts b/packages/app-store/stripepayment/lib/BillingPortalService.ts deleted file mode 100644 index c12c637b8197b6..00000000000000 --- a/packages/app-store/stripepayment/lib/BillingPortalService.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Re-export all services for backward compatibility -export { - BillingPortalService, - TeamBillingPortalService, - OrganizationBillingPortalService, - UserBillingPortalService, - BillingPortalServiceFactory, -} from "./services"; - -export type { TeamEntity, BillingPortalResult } from "./services"; diff --git a/packages/app-store/stripepayment/lib/services/base/BillingPortalService.ts b/packages/app-store/stripepayment/lib/services/base/BillingPortalService.ts deleted file mode 100644 index d89afcc68d5443..00000000000000 --- a/packages/app-store/stripepayment/lib/services/base/BillingPortalService.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { NextApiResponse } from "next"; - -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; -import logger from "@calcom/lib/logger"; -import { TeamRepository } from "@calcom/lib/server/repository/team"; -import prisma from "@calcom/prisma"; - -import stripe from "../../server"; - -export interface TeamEntity { - id: number; - metadata: unknown; - isOrganization: boolean; -} - -export interface BillingPortalResult { - success: boolean; - customerId?: string; - portalUrl?: string; -} - -export abstract class BillingPortalService { - protected permissionService: PermissionCheckService; - protected teamRepository: TeamRepository; - protected contextName = "Team"; // Can be overridden by subclasses - - constructor() { - this.permissionService = new PermissionCheckService(); - this.teamRepository = new TeamRepository(prisma); - } - - /** - * Creates a billing portal URL for a Stripe customer - */ - protected async createBillingPortalUrl(customerId: string, returnUrl: string): Promise { - const log = logger.getSubLogger({ prefix: ["createBillingPortalUrl"] }); - - try { - const portalSession = await stripe.billingPortal.sessions.create({ - customer: customerId, - return_url: returnUrl, - }); - - return portalSession.url; - } catch (e) { - log.error(`Failed to create billing portal session for ${customerId}: ${e}`); - throw new Error("Failed to create billing portal session"); - } - } - - /** - * Builds a safe return URL for the billing portal - */ - protected buildReturnUrl(returnTo?: string): string { - const defaultUrl = `${WEBAPP_URL}/settings/billing`; - - if (typeof returnTo !== "string") return defaultUrl; - - const safeRedirectUrl = getSafeRedirectUrl(returnTo); - return safeRedirectUrl || defaultUrl; - } - - /** - * Abstract method to check permissions - implemented by subclasses - */ - abstract checkPermissions(userId: number, teamId: number): Promise; - - /** - * Abstract method to get customer ID - implemented by subclasses - */ - abstract getCustomerId(teamId: number): Promise; - - /** - * Process billing portal request for a team - */ - async processBillingPortal( - userId: number, - teamId: number, - returnUrl: string, - res: NextApiResponse - ): Promise { - // Check permissions - const hasPermission = await this.checkPermissions(userId, teamId); - if (!hasPermission) { - res.status(403).json({ message: "Forbidden" }); - return; - } - - // Get customer ID - const customerId = await this.getCustomerId(teamId); - if (!customerId) { - res.status(400).json({ - message: `${this.contextName} billing not properly configured. Please contact support.`, - }); - return; - } - - // Create portal URL and redirect - const billingPortalUrl = await this.createBillingPortalUrl(customerId, returnUrl); - res.redirect(302, billingPortalUrl); - } -} diff --git a/packages/app-store/stripepayment/lib/services/factory/BillingPortalServiceFactory.ts b/packages/app-store/stripepayment/lib/services/factory/BillingPortalServiceFactory.ts deleted file mode 100644 index ece4731ebd05d4..00000000000000 --- a/packages/app-store/stripepayment/lib/services/factory/BillingPortalServiceFactory.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { TeamRepository } from "@calcom/lib/server/repository/team"; -import prisma from "@calcom/prisma"; - -import type { BillingPortalService } from "../base/BillingPortalService"; -import { OrganizationBillingPortalService } from "../organization/OrganizationBillingPortalService"; -import { TeamBillingPortalService } from "../team/TeamBillingPortalService"; -import { UserBillingPortalService } from "../user/UserBillingPortalService"; - -/** - * Factory to create the appropriate billing portal service based on team type - */ -export class BillingPortalServiceFactory { - /** - * Determines team type and returns the appropriate service - */ - static async createService(teamId: number): Promise { - const teamRepository = new TeamRepository(prisma); - const team = await teamRepository.findById({ id: teamId }); - - if (!team) { - throw new Error("Team not found"); - } - - if (team.isOrganization) { - return new OrganizationBillingPortalService(); - } - - return new TeamBillingPortalService(); - } - - /** - * Creates a user billing portal service - */ - static createUserService(): UserBillingPortalService { - return new UserBillingPortalService(); - } -} diff --git a/packages/app-store/stripepayment/lib/services/index.ts b/packages/app-store/stripepayment/lib/services/index.ts deleted file mode 100644 index 7ccd24cf69f972..00000000000000 --- a/packages/app-store/stripepayment/lib/services/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { BillingPortalService } from "./base/BillingPortalService"; -export type { TeamEntity, BillingPortalResult } from "./base/BillingPortalService"; -export { TeamBillingPortalService } from "./team/TeamBillingPortalService"; -export { OrganizationBillingPortalService } from "./organization/OrganizationBillingPortalService"; -export { UserBillingPortalService } from "./user/UserBillingPortalService"; -export { BillingPortalServiceFactory } from "./factory/BillingPortalServiceFactory"; diff --git a/packages/app-store/stripepayment/lib/services/organization/OrganizationBillingPortalService.ts b/packages/app-store/stripepayment/lib/services/organization/OrganizationBillingPortalService.ts deleted file mode 100644 index d58c0eae588525..00000000000000 --- a/packages/app-store/stripepayment/lib/services/organization/OrganizationBillingPortalService.ts +++ /dev/null @@ -1,55 +0,0 @@ -import logger from "@calcom/lib/logger"; -import { MembershipRole } from "@calcom/prisma/enums"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -import { getSubscriptionFromId } from "../../subscriptions"; -import { BillingPortalService } from "../base/BillingPortalService"; - -/** - * Billing portal service for organizations - */ -export class OrganizationBillingPortalService extends BillingPortalService { - constructor() { - super(); - this.contextName = "Organization"; - } - - async checkPermissions(userId: number, teamId: number): Promise { - return await this.permissionService.checkPermission({ - userId, - teamId, - permission: "organization.manageBilling", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - } - - async getCustomerId(teamId: number): Promise { - const log = logger.getSubLogger({ prefix: ["OrganizationBillingPortalService", "getCustomerId"] }); - - const team = await this.teamRepository.findById({ id: teamId }); - if (!team) return null; - - const teamMetadataParsed = teamMetadataSchema.safeParse(team.metadata); - - if (!teamMetadataParsed.success || !teamMetadataParsed.data?.subscriptionId) { - return null; - } - - try { - const subscription = await getSubscriptionFromId(teamMetadataParsed.data.subscriptionId); - - if (!subscription?.customer) { - log.warn("Subscription found but no customer ID", { - teamId, - subscriptionId: teamMetadataParsed.data.subscriptionId, - }); - return null; - } - - return subscription.customer as string; - } catch (error) { - log.error("Failed to retrieve subscription", { teamId, error }); - return null; - } - } -} diff --git a/packages/app-store/stripepayment/lib/services/team/TeamBillingPortalService.ts b/packages/app-store/stripepayment/lib/services/team/TeamBillingPortalService.ts deleted file mode 100644 index 77eb559bcb59e0..00000000000000 --- a/packages/app-store/stripepayment/lib/services/team/TeamBillingPortalService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import logger from "@calcom/lib/logger"; -import { MembershipRole } from "@calcom/prisma/enums"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -import { getSubscriptionFromId } from "../../subscriptions"; -import { BillingPortalService } from "../base/BillingPortalService"; - -/** - * Billing portal service for regular teams - */ -export class TeamBillingPortalService extends BillingPortalService { - async checkPermissions(userId: number, teamId: number): Promise { - return await this.permissionService.checkPermission({ - userId, - teamId, - permission: "team.manageBilling", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - } - - async getCustomerId(teamId: number): Promise { - const log = logger.getSubLogger({ prefix: ["TeamBillingPortalService", "getCustomerId"] }); - - const team = await this.teamRepository.findById({ id: teamId }); - if (!team) return null; - - const teamMetadataParsed = teamMetadataSchema.safeParse(team.metadata); - - if (!teamMetadataParsed.success || !teamMetadataParsed.data?.subscriptionId) { - return null; - } - - try { - const subscription = await getSubscriptionFromId(teamMetadataParsed.data.subscriptionId); - - if (!subscription?.customer) { - log.warn("Subscription found but no customer ID", { - teamId, - subscriptionId: teamMetadataParsed.data.subscriptionId, - }); - return null; - } - - return subscription.customer as string; - } catch (error) { - log.error("Failed to retrieve subscription", { teamId, error }); - return null; - } - } -} diff --git a/packages/app-store/stripepayment/lib/services/user/UserBillingPortalService.ts b/packages/app-store/stripepayment/lib/services/user/UserBillingPortalService.ts deleted file mode 100644 index 25cd484ca712d6..00000000000000 --- a/packages/app-store/stripepayment/lib/services/user/UserBillingPortalService.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { NextApiResponse } from "next"; - -import logger from "@calcom/lib/logger"; - -import { getStripeCustomerIdFromUserId } from "../../customer"; -import stripe from "../../server"; - -/** - * Billing portal service for individual users - */ -export class UserBillingPortalService { - /** - * Get customer ID for a user - */ - async getCustomerId(userId: number): Promise { - return await getStripeCustomerIdFromUserId(userId); - } - - /** - * Process billing portal request for a user - */ - async processBillingPortal(userId: number, returnUrl: string, res: NextApiResponse): Promise { - const customerId = await this.getCustomerId(userId); - if (!customerId) { - res.status(404).json({ message: "CustomerId not found" }); - return; - } - - const billingPortalUrl = await this.createBillingPortalUrl(customerId, returnUrl); - res.redirect(302, billingPortalUrl); - } - - /** - * Creates a billing portal URL for a Stripe customer - */ - private async createBillingPortalUrl(customerId: string, returnUrl: string): Promise { - const log = logger.getSubLogger({ prefix: ["createBillingPortalUrl"] }); - - try { - const portalSession = await stripe.billingPortal.sessions.create({ - customer: customerId, - return_url: returnUrl, - }); - - return portalSession.url; - } catch (e) { - log.error(`Failed to create billing portal session for ${customerId}: ${e}`); - throw new Error("Failed to create billing portal session"); - } - } -} diff --git a/packages/app-store/test-setup.ts b/packages/app-store/test-setup.ts index 8443124a2cb4fd..696855e7d793f8 100644 --- a/packages/app-store/test-setup.ts +++ b/packages/app-store/test-setup.ts @@ -32,6 +32,9 @@ vi.mock("@calcom/ui/classNames", () => ({ }, })); +vi.mock("@calcom/lib/event-types/getEventTypesByViewer", () => ({})); +vi.mock("@calcom/lib/event-types/getEventTypesPublic", () => ({})); + global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), diff --git a/packages/app-store/vital/lib/reschedule.ts b/packages/app-store/vital/lib/reschedule.ts index 4f492995263042..cd0258990c07c5 100644 --- a/packages/app-store/vital/lib/reschedule.ts +++ b/packages/app-store/vital/lib/reschedule.ts @@ -6,7 +6,7 @@ import { CalendarEventBuilder } from "@calcom/lib/builders/CalendarEvent/builder import { CalendarEventDirector } from "@calcom/lib/builders/CalendarEvent/director"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { deleteMeeting } from "@calcom/app-store/videoClient"; +import { deleteMeeting } from "@calcom/lib/videoClient"; import prisma from "@calcom/prisma"; import type { Booking, BookingReference, User } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; diff --git a/packages/app-store/wipemycalother/lib/reschedule.ts b/packages/app-store/wipemycalother/lib/reschedule.ts index 308538f51bdd99..82a535dc95e2da 100644 --- a/packages/app-store/wipemycalother/lib/reschedule.ts +++ b/packages/app-store/wipemycalother/lib/reschedule.ts @@ -6,7 +6,7 @@ import { CalendarEventBuilder } from "@calcom/lib/builders/CalendarEvent/builder import { CalendarEventDirector } from "@calcom/lib/builders/CalendarEvent/director"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { deleteMeeting } from "@calcom/app-store/videoClient"; +import { deleteMeeting } from "@calcom/lib/videoClient"; import prisma from "@calcom/prisma"; import type { Booking, BookingReference, User } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; diff --git a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts index 23198c810361d0..48b31fbc9fa274 100644 --- a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts @@ -180,7 +180,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => return { topic: event.title, type: 2, // Means that this is a scheduled meeting - start_time: dayjs(event.startTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"), + start_time: dayjs(event.startTime).format("YYYY-MM-DDTHH:mm:ss"), duration: (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / 60000, //schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?) timezone: event.organizer.timeZone, diff --git a/packages/emails/src/templates/BaseScheduledEmail.tsx b/packages/emails/src/templates/BaseScheduledEmail.tsx index 0e45b55a1a1fc1..e99074429037ac 100644 --- a/packages/emails/src/templates/BaseScheduledEmail.tsx +++ b/packages/emails/src/templates/BaseScheduledEmail.tsx @@ -1,7 +1,7 @@ import type { TFunction } from "i18next"; import dayjs from "@calcom/dayjs"; -import { formatPrice } from "@calcom/lib/currencyConversions"; +import { formatPrice } from "@calcom/lib/price"; import { TimeFormat } from "@calcom/lib/timeFormat"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; diff --git a/packages/embeds/embed-core/src/embed.css b/packages/embeds/embed-core/src/embed.css index 741b1d19c62179..0792fdb415c7a2 100644 --- a/packages/embeds/embed-core/src/embed.css +++ b/packages/embeds/embed-core/src/embed.css @@ -6,9 +6,4 @@ min-height: 300px; margin: 0 auto; width: 100%; - /** - * Following properties are added to ensure that the embedding page isn't able to affect these properties - */ - display: block !important; /* A website made all iframes' display flex breaking the embed */ - color-scheme: unset !important; /* A website had explicitly set color-scheme to something for all iframes */ } diff --git a/packages/lib/auth/hashPassword.ts b/packages/features/auth/lib/hashPassword.ts similarity index 100% rename from packages/lib/auth/hashPassword.ts rename to packages/features/auth/lib/hashPassword.ts diff --git a/packages/lib/auth/isPasswordValid.ts b/packages/features/auth/lib/isPasswordValid.ts similarity index 100% rename from packages/lib/auth/isPasswordValid.ts rename to packages/features/auth/lib/isPasswordValid.ts diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 53fd456464d472..8783c3ad30afa0 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -16,7 +16,6 @@ import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/crea import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; -import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { GOOGLE_CALENDAR_SCOPES, @@ -47,6 +46,7 @@ import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils"; import { getOrgUsernameFromEmail } from "../signup/utils/getOrgUsernameFromEmail"; import { ErrorCode } from "./ErrorCode"; import { dub } from "./dub"; +import { isPasswordValid } from "./isPasswordValid"; import CalComAdapter from "./next-auth-custom-adapter"; import { verifyPassword } from "./verifyPassword"; diff --git a/packages/features/auth/signup/handlers/calcomHandler.ts b/packages/features/auth/signup/handlers/calcomHandler.ts index adecb8e14eb293..f1bce058f5ded3 100644 --- a/packages/features/auth/signup/handlers/calcomHandler.ts +++ b/packages/features/auth/signup/handlers/calcomHandler.ts @@ -2,12 +2,12 @@ import { cookies, headers } from "next/headers"; import { NextResponse } from "next/server"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships"; import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar"; import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest"; import { HttpError } from "@calcom/lib/http-error"; diff --git a/packages/features/auth/signup/handlers/selfHostedHandler.ts b/packages/features/auth/signup/handlers/selfHostedHandler.ts index 6b2028b2e8845a..5da0c05c53b023 100644 --- a/packages/features/auth/signup/handlers/selfHostedHandler.ts +++ b/packages/features/auth/signup/handlers/selfHostedHandler.ts @@ -1,9 +1,9 @@ import { NextResponse } from "next/server"; import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail"; import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { isUsernameReservedDueToMigration } from "@calcom/lib/server/username"; diff --git a/packages/features/auth/signup/utils/createOrUpdateMemberships.ts b/packages/features/auth/signup/utils/createOrUpdateMemberships.ts index 0df016f7e79365..e6c34e37dd4489 100644 --- a/packages/features/auth/signup/utils/createOrUpdateMemberships.ts +++ b/packages/features/auth/signup/utils/createOrUpdateMemberships.ts @@ -1,4 +1,4 @@ -import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; +import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; import { prisma } from "@calcom/prisma"; import type { Team, User, OrganizationSettings } from "@calcom/prisma/client"; diff --git a/packages/features/auth/signup/utils/organization.ts b/packages/features/auth/signup/utils/organization.ts index 5b5e27c926a067..44008dddcc3a64 100644 --- a/packages/features/auth/signup/utils/organization.ts +++ b/packages/features/auth/signup/utils/organization.ts @@ -1,4 +1,4 @@ -import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; +import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; import { prisma } from "@calcom/prisma"; import type { Team, OrganizationSettings } from "@calcom/prisma/client"; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx index f21af3756ffd0a..141b758c93ac50 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx @@ -1,19 +1,18 @@ -import { TooltipProvider } from "@radix-ui/react-tooltip"; import { render, fireEvent, screen } from "@testing-library/react"; import * as React from "react"; import type { UseFormReturn } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form"; import { expect, vi } from "vitest"; -import PhoneInput from "@calcom/features/components/phone-input/PhoneInput"; +import { TooltipProvider } from "@calcom/ui/components/tooltip"; import { getBookingFieldsWithSystemFields } from "../../../lib/getBookingFields"; import { BookingFields } from "./BookingFields"; // Mock PhoneInput to avoid calling the lazy import -vi.mock("@calcom/features/components/phone-input", () => { +vi.mock("@calcom/features/components/phone-input/PhoneInput", () => { return { - default: PhoneInput, + default: () =>
, }; }); diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx index 6197e948af73a4..a717d031e1789e 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx @@ -5,7 +5,7 @@ import { getOrganizerInputLocationTypes } from "@calcom/app-store/locations"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect"; -import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField"; +import FormBuilderField from "@calcom/features/form-builder/FormBuilderField"; import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes"; import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml"; import { useLocale } from "@calcom/lib/hooks/useLocale"; diff --git a/packages/features/bookings/Booker/components/Header.tsx b/packages/features/bookings/Booker/components/Header.tsx index 1d9799d4bc3468..52f8e1a32e9ba6 100644 --- a/packages/features/bookings/Booker/components/Header.tsx +++ b/packages/features/bookings/Booker/components/Header.tsx @@ -5,7 +5,6 @@ import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import dayjs from "@calcom/dayjs"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; -import { useInitializeWeekStart } from "@calcom/features/bookings/Booker/components/hooks/useInitializeWeekStart"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; @@ -26,7 +25,6 @@ export function Header({ eventSlug, isMyLink, renderOverlay, - isCalendarView, }: { extraDays: number; isMobile: boolean; @@ -35,16 +33,14 @@ export function Header({ eventSlug: string; isMyLink: boolean; renderOverlay?: () => JSX.Element | null; - isCalendarView?: boolean; }) { const { t, i18n } = useLocale(); const isEmbed = useIsEmbed(); - const isPlatform = useIsPlatform(); const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow); const selectedDateString = useBookerStoreContext((state) => state.selectedDate); const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate); const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate); - const isMonthView = isCalendarView !== undefined ? !isCalendarView : layout === BookerLayouts.MONTH_VIEW; + const isMonthView = layout === BookerLayouts.MONTH_VIEW; const today = dayjs(); const selectedDate = selectedDateString ? dayjs(selectedDateString) : today; const selectedDateMin3DaysDifference = useMemo(() => { @@ -52,8 +48,6 @@ export function Header({ return diff > 3 || diff < -3; }, [today, selectedDate]); - useInitializeWeekStart(isPlatform, isCalendarView ?? false); - const onLayoutToggle = useCallback( (newLayout: string) => { if (layout === newLayout || !newLayout) return; @@ -136,10 +130,7 @@ export function Header({ )} diff --git a/packages/features/bookings/Booker/components/LargeCalendar.tsx b/packages/features/bookings/Booker/components/LargeCalendar.tsx index 839b5a6fdc67a6..0151d399d2fb3d 100644 --- a/packages/features/bookings/Booker/components/LargeCalendar.tsx +++ b/packages/features/bookings/Booker/components/LargeCalendar.tsx @@ -2,10 +2,10 @@ import { useMemo, useEffect } from "react"; import dayjs from "@calcom/dayjs"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; -import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots"; import type { BookerEvent } from "@calcom/features/bookings/types"; import { Calendar } from "@calcom/features/calendars/weeklyview"; import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events"; +import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state"; import { localStorage } from "@calcom/lib/webstorage"; import type { useScheduleForEventReturnType } from "../utils/event"; @@ -34,7 +34,24 @@ export const LargeCalendar = ({ const eventDuration = selectedEventDuration || event?.data?.length || 30; - const availableSlots = useAvailableTimeSlots({ schedule, eventDuration }); + const availableSlots = useMemo(() => { + const availableTimeslots: CalendarAvailableTimeslots = {}; + if (!schedule) return availableTimeslots; + if (!schedule.slots) return availableTimeslots; + + for (const day in schedule.slots) { + availableTimeslots[day] = schedule.slots[day].map((slot) => { + const { time, ...rest } = slot; + return { + start: dayjs(time).toDate(), + end: dayjs(time).add(eventDuration, "minutes").toDate(), + ...rest, + }; + }); + } + + return availableTimeslots; + }, [schedule, eventDuration]); const startDate = selectedDate ? dayjs(selectedDate).toDate() : dayjs().toDate(); const endDate = dayjs(startDate) diff --git a/packages/features/bookings/Booker/components/hooks/useAvailableTimeSlots.ts b/packages/features/bookings/Booker/components/hooks/useAvailableTimeSlots.ts deleted file mode 100644 index b88856c06bd152..00000000000000 --- a/packages/features/bookings/Booker/components/hooks/useAvailableTimeSlots.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useMemo } from "react"; - -import dayjs from "@calcom/dayjs"; -import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state"; -import type { IGetAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; - -interface UseAvailableTimeSlotsProps { - eventDuration: number; - schedule?: IGetAvailableSlots; -} - -export const useAvailableTimeSlots = ({ schedule, eventDuration }: UseAvailableTimeSlotsProps) => { - return useMemo(() => { - const availableTimeslots: CalendarAvailableTimeslots = {}; - if (!schedule || !schedule.slots) return availableTimeslots; - - for (const day in schedule.slots) { - availableTimeslots[day] = schedule.slots[day].map((slot) => { - const { time, ...rest } = slot; - return { - start: dayjs(time).toDate(), - end: dayjs(time).add(eventDuration, "minutes").toDate(), - ...rest, - }; - }); - } - - return availableTimeslots; - }, [schedule, eventDuration]); -}; diff --git a/packages/features/bookings/Booker/components/hooks/useInitializeWeekStart.ts b/packages/features/bookings/Booker/components/hooks/useInitializeWeekStart.ts deleted file mode 100644 index c2d3a38b22d7dd..00000000000000 --- a/packages/features/bookings/Booker/components/hooks/useInitializeWeekStart.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from "react"; - -import dayjs from "@calcom/dayjs"; -import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; - -export const useInitializeWeekStart = (isPlatform: boolean, isCalendarView: boolean) => { - const today = dayjs(); - const weekStart = today.startOf("week").format("YYYY-MM-DD"); - const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate); - - useEffect(() => { - if (isPlatform && isCalendarView) { - setSelectedDate({ date: weekStart, omitUpdatingParams: true }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); -}; diff --git a/packages/features/bookings/Booker/components/hooks/usePrefetch.ts b/packages/features/bookings/Booker/components/hooks/usePrefetch.ts deleted file mode 100644 index 1f5cf300e72171..00000000000000 --- a/packages/features/bookings/Booker/components/hooks/usePrefetch.ts +++ /dev/null @@ -1,56 +0,0 @@ -import dayjs from "@calcom/dayjs"; -import type { BookerState } from "@calcom/features/bookings/Booker/types"; -import { BookerLayouts } from "@calcom/prisma/zod-utils"; - -interface UsePrefetchParams { - date: string; - month: string | null; - bookerLayout: { - layout: string; - extraDays: number; - columnViewExtraDays: { current: number }; - }; - bookerState: BookerState; -} - -export const usePrefetch = ({ date, month, bookerLayout, bookerState }: UsePrefetchParams) => { - const dateMonth = dayjs(date).month(); - const monthAfterAdding1Month = dayjs(date).add(1, "month").month(); - const monthAfterAddingExtraDays = dayjs(date).add(bookerLayout.extraDays, "day").month(); - const monthAfterAddingExtraDaysColumnView = dayjs(date) - .add(bookerLayout.columnViewExtraDays.current, "day") - .month(); - - const isValidDate = dayjs(date).isValid(); - const twoWeeksAfter = dayjs(month).startOf("month").add(2, "week"); - const isSameMonth = dayjs().isSame(dayjs(month), "month"); - const isAfter2Weeks = dayjs().isAfter(twoWeeksAfter); - - const prefetchNextMonth = - (bookerLayout.layout === BookerLayouts.WEEK_VIEW && - !!bookerLayout.extraDays && - !isNaN(dateMonth) && - !isNaN(monthAfterAddingExtraDays) && - dateMonth !== monthAfterAddingExtraDays) || - (bookerLayout.layout === BookerLayouts.COLUMN_VIEW && - !isNaN(dateMonth) && - !isNaN(monthAfterAddingExtraDaysColumnView) && - dateMonth !== monthAfterAddingExtraDaysColumnView) || - ((bookerLayout.layout === BookerLayouts.MONTH_VIEW || bookerLayout.layout === "mobile") && - (!isValidDate || isSameMonth) && - isAfter2Weeks); - - const monthCount = - ((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") || - bookerLayout.layout === BookerLayouts.COLUMN_VIEW) && - !isNaN(monthAfterAdding1Month) && - !isNaN(monthAfterAddingExtraDaysColumnView) && - monthAfterAdding1Month !== monthAfterAddingExtraDaysColumnView - ? 2 - : undefined; - - return { - prefetchNextMonth, - monthCount, - }; -}; diff --git a/packages/features/bookings/components/event-meta/Price.tsx b/packages/features/bookings/components/event-meta/Price.tsx index 8fdc2b9d98534b..094df67e407945 100644 --- a/packages/features/bookings/components/event-meta/Price.tsx +++ b/packages/features/bookings/components/event-meta/Price.tsx @@ -1,6 +1,6 @@ import dynamic from "next/dynamic"; -import { formatPrice } from "@calcom/lib/currencyConversions"; +import { formatPrice } from "@calcom/lib/price"; import type { EventPrice } from "../../types"; diff --git a/packages/features/bookings/lib/get-booking.ts b/packages/features/bookings/lib/get-booking.ts index d14f682be6a5d2..b66fd78de534cc 100644 --- a/packages/features/bookings/lib/get-booking.ts +++ b/packages/features/bookings/lib/get-booking.ts @@ -1,3 +1,5 @@ +import type { z } from "zod"; + import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import slugify from "@calcom/lib/slugify"; import type { PrismaClient } from "@calcom/prisma"; @@ -113,8 +115,10 @@ export const getBookingWithResponses = < ) => { return { ...booking, - responses: isSeatedEvent ? booking.responses : booking.responses || getResponsesFromOldBooking(booking), - } as Omit & { responses: Record }; + responses: isSeatedEvent + ? bookingResponsesDbSchema.parse(booking.responses || {}) + : bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)), + } as Omit & { responses: z.infer }; }; export default getBooking; diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 24e86413baec21..79e76e31aca393 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -17,7 +17,7 @@ import { } from "@calcom/features/webhooks/lib/scheduleTrigger"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index b85fde89512e99..f53f0598c81bad 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -11,8 +11,8 @@ import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; -import type { EventManagerUser } from "@calcom/features/bookings/lib/EventManager"; -import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; +import type { EventManagerUser } from "@calcom/lib/EventManager"; +import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index f466bd50ed151e..dab2adbc6db9a9 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -3,7 +3,6 @@ import short, { uuid } from "short-uuid"; import { v5 as uuidv5 } from "uuid"; import processExternalId from "@calcom/app-store/_utils/calendars/processExternalId"; -import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData"; import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata"; import { getLocationValueForDB, @@ -17,7 +16,6 @@ import dayjs from "@calcom/dayjs"; import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder"; import getICalUID from "@calcom/emails/lib/getICalUID"; import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder"; -import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; import type { BookingDataSchemaGetter } from "@calcom/features/bookings/lib/dto/types"; import type { CreateRegularBookingData, CreateBookingMeta } from "@calcom/features/bookings/lib/dto/types"; import type { CheckBookingAndDurationLimitsService } from "@calcom/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits"; @@ -26,11 +24,9 @@ import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhoo import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache"; import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; import { getEventName, updateHostInEventName } from "@calcom/features/eventtypes/lib/eventNaming"; import type { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { getFullName } from "@calcom/features/form-builder/utils"; -import { handleAnalyticsEvents } from "@calcom/features/tasker/tasks/analytics/handleAnalyticsEvents"; import { UsersRepository } from "@calcom/features/users/users.repository"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; @@ -40,9 +36,12 @@ import { scheduleTrigger, } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; +import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager"; +import { handleAnalyticsEvents } from "@calcom/lib/analyticsManager/handleAnalyticsEvents"; import { groupHostsByGroupId } from "@calcom/lib/bookings/hostGroupUtils"; import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils"; import { DEFAULT_GROUP_ID } from "@calcom/lib/constants"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; import { enrichHostsWithDelegationCredentials, getFirstDelegationConferencingCredentialAppLocation, @@ -55,6 +54,7 @@ import { getErrorFromUnknown } from "@calcom/lib/errors"; import { extractBaseEmail } from "@calcom/lib/extract-base-email"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; +import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; import type { CheckBookingLimitsService } from "@calcom/lib/intervalLimits/server/checkBookingLimits"; diff --git a/packages/features/bookings/lib/handleNewBooking/getEventType.ts b/packages/features/bookings/lib/handleNewBooking/getEventType.ts index bb5313375b3994..27d83294ba6ab2 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventType.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventType.ts @@ -1,4 +1,4 @@ -import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { withReporting } from "@calcom/lib/sentryWrapper"; import { getBookingFieldsWithSystemFields } from "../getBookingFields"; diff --git a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts index 7e60482df243a9..c8ed5213602454 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts @@ -1,7 +1,7 @@ import type { LocationObject } from "@calcom/app-store/locations"; import { workflowSelect } from "@calcom/ee/workflows/lib/getAllWorkflows"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; -import type { DefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +import type { DefaultEvent } from "@calcom/lib/defaultEvents"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { withSelectedCalendars } from "@calcom/lib/server/repository/user"; diff --git a/packages/features/bookings/lib/handleNewBooking/test/booking-validations.test.ts b/packages/features/bookings/lib/handleNewBooking/test/booking-validations.test.ts deleted file mode 100644 index eed0db94b98ff3..00000000000000 --- a/packages/features/bookings/lib/handleNewBooking/test/booking-validations.test.ts +++ /dev/null @@ -1,365 +0,0 @@ -/** - * Booking Validation Specifications - * These specifications verify the business rules and validation behavior for booking creation - */ -import prismaMock from "../../../../../../tests/libs/__mocks__/prisma"; -import { - createBookingScenario, - TestData, - getOrganizer, - getBooker, - getScenarioData, - getGoogleCalendarCredential, - mockCalendarToHaveNoBusySlots, -} from "@calcom/web/test/utils/bookingScenario/bookingScenario"; -import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; -import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; - -import { afterEach, vi } from "vitest"; -import { describe, expect } from "vitest"; - -import { BookingStatus } from "@calcom/prisma/enums"; -import { test } from "@calcom/web/test/fixtures/fixtures"; - -import { getNewBookingHandler } from "./getNewBookingHandler"; - -function addToBlacklistedEmails(emails: string[]) { - process.env.BLACKLISTED_GUEST_EMAILS = emails.join(","); -} - -function resetBlacklistedEmails() { - delete process.env.BLACKLISTED_GUEST_EMAILS; -} - -afterEach(() => { - resetBlacklistedEmails(); -}); - -describe("Booking Validation Specifications", () => { - setupAndTeardown(); - - describe("Email Blacklist Validation", () => { - test("when email is in BLACKLISTED_GUEST_EMAILS, allow the user to book only if they are logged in with that email", async () => { - const handleNewBooking = getNewBookingHandler(); - const blockedEmail = "organizer@example.com"; // Use organizer's email as the blocked one - - const booker = getBooker({ - email: blockedEmail, - name: "Organizer", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - emailVerified: new Date(), - }); - - addToBlacklistedEmails(["organizer@example.com", "spam@test.com"]); - - await createBookingScenario( - getScenarioData({ - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"]], - }) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", {}); - - const mockBookingData = getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: "New York" }, - }, - }, - }); - - // Non logged in user should not be able to book - await expect(handleNewBooking({ - bookingData: mockBookingData, - })).rejects.toThrow( - "Attendee email has been blocked. Make sure to login as organizer@example.com to use this email for creating a booking." - ); - - // Should allow booking when the user who owns the blacklisted email is logged in - const createdBooking = await handleNewBooking({ - bookingData: mockBookingData, - userId: 101, // Same as organizer who owns the blacklisted email - }); - - expect(createdBooking).toEqual( - expect.objectContaining({ - id: expect.any(Number), - uid: expect.any(String), - status: BookingStatus.ACCEPTED, - }) - ); - }); - - test("prevents booking when blacklisted email is not verified in the system", async () => { - const handleNewBooking = getNewBookingHandler(); - const blockedEmail = "blocked@example.com"; - - const booker = getBooker({ - email: blockedEmail, - name: "Unverified User", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - emailVerified: null, - }); - - // Mock environment variable for blacklisted emails - addToBlacklistedEmails(["blocked@example.com"]); - - await createBookingScenario( - getScenarioData({ - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"]], - }) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", {}); - - const mockBookingData = getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: "New York" }, - }, - }, - }); - - // Should prevent booking when blacklisted email has no verified user in database - await expect(handleNewBooking({ - bookingData: mockBookingData, - })).rejects.toThrow("Cannot use this email to create the booking."); - }); - }); - - describe("Active Bookings Limit Validation", () => { - test("allows booking when user is under their active booking limit", async () => { - vi.setSystemTime(new Date("2025-01-01")); - const plus1DateString = "2025-01-02"; - - const handleNewBooking = getNewBookingHandler(); - - const booker = getBooker({ - email: "booker@example.com", - name: "Booker", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - }); - - // Create test scenario with event type that has maxActiveBookingsPerBooker limit - await createBookingScenario( - getScenarioData({ - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - // Two bookings allowed for the booker - maxActiveBookingsPerBooker: 2, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"]], - bookings: [ - { - uid: "existing-booking-1", - eventTypeId: 1, - userId: organizer.id, - startTime: `${plus1DateString}T10:00:00.000Z`, - endTime: `${plus1DateString}T10:30:00.000Z`, - title: "Existing Booking", - status: BookingStatus.ACCEPTED, - // Booker already has a booking in future - attendees: [{ - email: booker.email, - }], - }, - ], - }) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", {}); - - - const mockBookingData = getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: "New York" }, - }, - }, - }); - - // Should allow booking when user has not reached their limit (1 booking < 2 limit) - const createdBooking = await handleNewBooking({ - bookingData: mockBookingData, - }); - - expect(createdBooking).toEqual( - expect.objectContaining({ - id: expect.any(Number), - uid: expect.any(String), - status: BookingStatus.ACCEPTED, - }) - ); - - // Second booking should be rejected - await expect(handleNewBooking({ - bookingData: mockBookingData, - })).rejects.toThrow("booker_limit_exceeded_error"); - }); - - test("enforces booking limits with reschedule option when enabled", async () => { - vi.setSystemTime(new Date("2025-01-01")); - const plus1DateString = "2025-01-02"; - const plus2DateString = "2025-01-03"; - - const handleNewBooking = getNewBookingHandler(); - - const booker = getBooker({ - email: "booker@example.com", - name: "Booker", - }); - - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - }); - - // Create test scenario with event type that has reschedule option enabled - await createBookingScenario( - getScenarioData({ - eventTypes: [ - { - id: 1, - slotInterval: 30, - length: 30, - maxActiveBookingsPerBooker: 2, - maxActiveBookingPerBookerOfferReschedule: true, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"]], - bookings: [ - { - uid: "existing-booking-1", - eventTypeId: 1, - userId: organizer.id, - startTime: `${plus1DateString}T10:00:00.000Z`, - endTime: `${plus1DateString}T10:30:00.000Z`, - title: "Existing Booking", - status: BookingStatus.ACCEPTED, - attendees: [{ - email: booker.email, - }], - }, - { - uid: "existing-booking-2", - eventTypeId: 1, - userId: organizer.id, - startTime: `${plus2DateString}T10:00:00.000Z`, - endTime: `${plus2DateString}T10:30:00.000Z`, - title: "Existing Booking", - status: BookingStatus.ACCEPTED, - attendees: [{ - email: booker.email, - }], - }, - ], - }) - ); - - await mockCalendarToHaveNoBusySlots("googlecalendar", {}); - - const mockBookingData = getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: "New York" }, - }, - }, - }); - - try { - await handleNewBooking({ - bookingData: mockBookingData, - }); - } catch (error) { - expect(error.message).toEqual("booker_limit_exceeded_error_reschedule"); - expect(error.data).toEqual( - expect.objectContaining({ - rescheduleUid: "existing-booking-1", - }) - ); - } - }); - }); -}); diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts index 727243fce4589b..6625287dc7845e 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts @@ -783,7 +783,7 @@ describe("Round Robin handleNewBooking", () => { describe("Seated Round Robin Event", () => { test("For second seat booking, organizer remains the same with no team members included", async () => { const handleNewBooking = getNewBookingHandler(); - const EventManager = (await import("@calcom/features/bookings/lib/EventManager")).default; + const EventManager = (await import("@calcom/lib/EventManager")).default; const eventManagerSpy = vi.spyOn(EventManager.prototype, "updateCalendarAttendees"); diff --git a/packages/features/bookings/lib/handlePayment.ts b/packages/features/bookings/lib/handlePayment.ts index c970119046cd4a..373e490853613a 100644 --- a/packages/features/bookings/lib/handlePayment.ts +++ b/packages/features/bookings/lib/handlePayment.ts @@ -1,10 +1,10 @@ import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; +import type { AppCategories, Prisma, EventType } from "@calcom/prisma/client"; +import { convertToSmallestCurrencyUnit } from "@calcom/app-store/_utils/payments/currencyConversions"; import type { Fields } from "@calcom/features/bookings/lib/getBookingFields"; import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes"; -import { convertToSmallestCurrencyUnit } from "@calcom/lib/currencyConversions"; -import type { AppCategories, Prisma, EventType } from "@calcom/prisma/client"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; diff --git a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts index 1a59292c841e0b..372f5fdd1cae3c 100644 --- a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts +++ b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts @@ -10,7 +10,7 @@ import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import { WorkflowRepository } from "@calcom/lib/server/repository/workflow"; -import { updateMeeting } from "@calcom/app-store/videoClient"; +import { updateMeeting } from "@calcom/lib/videoClient"; import prisma from "@calcom/prisma"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { bookingCancelAttendeeSeatSchema } from "@calcom/prisma/zod-utils"; diff --git a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts index 640533b9199380..94f3a0de9233eb 100644 --- a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts +++ b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts @@ -10,7 +10,7 @@ import { allowDisablingAttendeeConfirmationEmails, allowDisablingHostConfirmationEmails, } from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; diff --git a/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts b/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts index 846aa640e9f4af..906833a405e150 100644 --- a/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts +++ b/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts @@ -2,7 +2,7 @@ import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { getAllDelegationCredentialsForUserIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server"; import { getDelegationCredentialOrFindRegularCredential } from "@calcom/lib/delegationCredential/server"; -import { deleteMeeting } from "@calcom/app-store/videoClient"; +import { deleteMeeting } from "@calcom/lib/videoClient"; import prisma from "@calcom/prisma"; import type { Attendee } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts index bcad39ff092bc3..97e50055a9079d 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts @@ -2,7 +2,7 @@ import { cloneDeep } from "lodash"; import { sendRescheduledSeatEmailAndSMS } from "@calcom/emails"; -import type EventManager from "@calcom/features/bookings/lib/EventManager"; +import type EventManager from "@calcom/lib/EventManager"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import type { Person, CalendarEvent } from "@calcom/types/Calendar"; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts index d61544611885c1..17762083037cbb 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts @@ -3,7 +3,7 @@ import { cloneDeep } from "lodash"; import { uuid } from "short-uuid"; import { sendRescheduledEmailsAndSMS } from "@calcom/emails"; -import type EventManager from "@calcom/features/bookings/lib/EventManager"; +import type EventManager from "@calcom/lib/EventManager"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts index 5fd6fb858528ab..80cdf9d66fb603 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts @@ -2,7 +2,7 @@ import { cloneDeep } from "lodash"; import { sendRescheduledEmailsAndSMS } from "@calcom/emails"; -import type EventManager from "@calcom/features/bookings/lib/EventManager"; +import type EventManager from "@calcom/lib/EventManager"; import prisma from "@calcom/prisma"; import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar"; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/ownerRescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/ownerRescheduleSeatedBooking.ts index 193f582f2c6c14..28b3b731c0c80c 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/ownerRescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/ownerRescheduleSeatedBooking.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line no-restricted-imports -import type EventManager from "@calcom/features/bookings/lib/EventManager"; +import type EventManager from "@calcom/lib/EventManager"; import type { createLoggerWithEventDetails } from "../../../handleNewBooking/logger"; import type { diff --git a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts index a7c962a5aa7544..5b573adaf76e5c 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line no-restricted-imports import dayjs from "@calcom/dayjs"; import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; import { HttpError } from "@calcom/lib/http-error"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; diff --git a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts deleted file mode 100644 index 933e80cb2b4a3c..00000000000000 --- a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; - -import * as constants from "@calcom/lib/constants"; -import { BookingStatus } from "@calcom/prisma/client"; -import type { JsonValue } from "@calcom/types/Json"; - -import { - determineReschedulePreventionRedirect, - type ReschedulePreventionRedirectInput, - type ReschedulePreventionRedirectResult, -} from "./determineReschedulePreventionRedirect"; - -// Mock the constants module -vi.mock("@calcom/lib/constants", async () => { - const actual = (await vi.importActual("@calcom/lib/constants")) as typeof import("@calcom/lib/constants"); - return { - ...actual, - ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS: undefined, // Default to undefined, will be overridden in tests - }; -}); - -const createTestBooking = (overrides?: { - uid?: string; - status?: BookingStatus; - endTime?: Date | null; - responses?: JsonValue; - eventType?: { - disableRescheduling?: boolean | null; - allowReschedulingPastBookings?: boolean | null; - allowBookingFromCancelledBookingReschedule?: boolean | null; - teamId?: number | null; - } | null; - dynamicEventSlugRef?: string | null; -}) => ({ - uid: overrides?.uid || "test-booking-uid", - status: overrides?.status || BookingStatus.ACCEPTED, - endTime: overrides?.endTime !== undefined ? overrides.endTime : futureDate(5), - responses: overrides?.responses || { - name: "John Doe", - email: "john.doe@example.com", - }, - eventType: - overrides?.eventType !== undefined - ? overrides.eventType - : // Default values from DB - { - disableRescheduling: false, - allowReschedulingPastBookings: false, - allowBookingFromCancelledBookingReschedule: false, - teamId: null, - }, - dynamicEventSlugRef: overrides?.dynamicEventSlugRef !== undefined ? overrides.dynamicEventSlugRef : null, -}); - -const createReschedulePreventionRedirectInput = (overrides?: { - booking?: ReturnType; - eventUrl?: string; - forceRescheduleForCancelledBooking?: boolean; -}): ReschedulePreventionRedirectInput => ({ - booking: overrides?.booking || createTestBooking(), - eventUrl: overrides?.eventUrl || "https://example.com/event", - forceRescheduleForCancelledBooking: overrides?.forceRescheduleForCancelledBooking || false, -}); - -const daysAgo = (days: number) => new Date(Date.now() - days * 24 * 60 * 60 * 1000); -const futureDate = (days: number) => new Date(Date.now() + days * 24 * 60 * 60 * 1000); - -const expectRedirectToBookingDetailsPage = ( - result: ReschedulePreventionRedirectResult, - bookingUid: string -) => { - expect(result).toBe(`/booking/${bookingUid}`); -}; - -const expectRedirectToEventBookingUrl = (result: ReschedulePreventionRedirectResult, eventUrl: string) => { - expect(result).toBe(eventUrl); -}; - -const expectRedirectToEventBookingPageWithParams = ({ - result, - eventUrl, - params, -}: { - result: ReschedulePreventionRedirectResult; - eventUrl: string; - params: Record; -}) => { - console.log("expectRedirectToEventBookingPageWithParams", { result, eventUrl, params }); - if (!result) { - throw new Error("We expected a redirect result"); - } - - const actualRedirectUrlObject = new URL(result); - Object.entries(params).forEach(([key, value]) => { - expect(actualRedirectUrlObject.searchParams.get(key)).toBe(value); - }); - expect(`${actualRedirectUrlObject.origin + actualRedirectUrlObject.pathname}`).toBe(eventUrl); -}; - -const expectToNotPreventReschedule = (result: ReschedulePreventionRedirectResult) => { - expect(result).toBeNull(); -}; - -describe("determineReschedulePreventionRedirect", () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2024-01-15T10:00:00Z")); - // Reset mocked constant to default before each test - vi.mocked(constants).ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS = undefined; - }); - - afterEach(() => { - vi.useRealTimers(); - vi.clearAllMocks(); - }); - - describe("when booking status is accepted and booking is in future", () => { - it("should not prevent reschedule if disableRescheduling is false", () => { - const testData = { - booking: createTestBooking({ - status: BookingStatus.ACCEPTED, - endTime: futureDate(5), - }), - }; - - const input = createReschedulePreventionRedirectInput(testData); - const result = determineReschedulePreventionRedirect(input); - - expectToNotPreventReschedule(result); - }); - - it("should redirect to booking details/status page when rescheduling is disabled(disableRescheduling is true)", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.ACCEPTED, - endTime: futureDate(5), - eventType: { - disableRescheduling: true, - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectRedirectToBookingDetailsPage(result, input.booking.uid); - }); - }); - - describe("when booking status is cancelled", () => { - it("should redirect to booking details/status page by default", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.CANCELLED, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectRedirectToBookingDetailsPage(result, input.booking.uid); - }); - - it("should redirect to new booking page when `allowBookingFromCancelledBookingReschedule` is true", () => { - const testData = { - booking: createTestBooking({ - status: BookingStatus.CANCELLED, - eventType: { - allowBookingFromCancelledBookingReschedule: true, - }, - }), - eventUrl: "https://example.com/event", - }; - - const input = createReschedulePreventionRedirectInput(testData); - const result = determineReschedulePreventionRedirect(input); - - expectRedirectToEventBookingUrl(result, testData.eventUrl); - }); - - it("should not prevent reschedule when `forceRescheduleForCancelledBooking` is true regardless of `allowBookingFromCancelledBookingReschedule`", () => { - const inputWhenallowBookingFromCancelledBookingRescheduleIsFalse = - createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.CANCELLED, - eventType: { - allowBookingFromCancelledBookingReschedule: false, - }, - }), - forceRescheduleForCancelledBooking: true, - }); - - const resultWhenallowBookingFromCancelledBookingRescheduleIsFalse = - determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsFalse); - expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsFalse); - - const inputWhenallowBookingFromCancelledBookingRescheduleIsTrue = - createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.CANCELLED, - eventType: { - allowBookingFromCancelledBookingReschedule: true, - }, - }), - forceRescheduleForCancelledBooking: true, - }); - const resultWhenallowBookingFromCancelledBookingRescheduleIsTrue = - determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsTrue); - - expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsTrue); - }); - }); - - describe("when booking status is rejected", () => { - it("should redirect to booking details/status page by default", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.REJECTED, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectRedirectToBookingDetailsPage(result, input.booking.uid); - }); - - it("should redirect to booking details/status page even when `allowBookingFromCancelledBookingReschedule` is true as that config doesn't apply to rejected bookings", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.REJECTED, - eventType: { - allowBookingFromCancelledBookingReschedule: true, - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectRedirectToBookingDetailsPage(result, input.booking.uid); - }); - - it("Current Behavior: Current behaviour is to not prevent reschedule. But EXPECTED(should redirect to booking details page even when `forceRescheduleForCancelledBooking` as that config doesn't apply to rejected bookings). ", () => { - const inputWhenallowBookingFromCancelledBookingRescheduleIsFalse = - createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.REJECTED, - eventType: { - allowBookingFromCancelledBookingReschedule: false, - }, - }), - forceRescheduleForCancelledBooking: true, - }); - - const resultWhenallowBookingFromCancelledBookingRescheduleIsFalse = - determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsFalse); - // expectRedirectToBookingDetailsPage( - // resultWhenallowBookingFromCancelledBookingRescheduleIsFalse, - // inputWhenallowBookingFromCancelledBookingRescheduleIsFalse.booking.uid - // ); - expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsFalse); - - const inputWhenallowBookingFromCancelledBookingRescheduleIsTrue = - createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - status: BookingStatus.REJECTED, - eventType: { - allowBookingFromCancelledBookingReschedule: true, - }, - }), - forceRescheduleForCancelledBooking: true, - }); - const resultWhenallowBookingFromCancelledBookingRescheduleIsTrue = - determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsTrue); - - // expectRedirectToBookingDetailsPage( - // resultWhenallowBookingFromCancelledBookingRescheduleIsTrue, - // inputWhenallowBookingFromCancelledBookingRescheduleIsTrue.booking.uid - // ); - expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsTrue); - }); - }); - - describe("when booking is in the past - Default behaviour(without ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS)", () => { - it("should redirect to new booking page with prefilled params when allowReschedulingPastBookings is false", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - endTime: daysAgo(5), - responses: { - name: "John Doe", - email: "john.doe@example.com", - }, - eventType: { - allowReschedulingPastBookings: false, - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectRedirectToEventBookingPageWithParams({ - result, - eventUrl: "https://example.com/event", - params: { - name: "John Doe", - email: "john.doe@example.com", - }, - }); - }); - - it("should not prevent reschedule when allowReschedulingPastBookings is true", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - endTime: daysAgo(5), - eventType: { - disableRescheduling: false, - allowReschedulingPastBookings: true, - allowBookingFromCancelledBookingReschedule: false, - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectToNotPreventReschedule(result); - }); - }); - - describe("when booking is in the past - New behaviour(based on ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS)", () => { - beforeEach(() => { - vi.mocked(constants).ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS = "123,456,789"; - }); - - it("should redirect to booking status page instead of event URL when the event's teamId is in the environment variable", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - uid: "team-booking-uid", - status: BookingStatus.ACCEPTED, - endTime: daysAgo(3), - eventType: { - teamId: 456, // This team ID is in the environment variable - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectRedirectToBookingDetailsPage(result, input.booking.uid); - }); - - it("should redirect to event URL when the event's teamId is not in the environment variable", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - uid: "non-team-booking-uid", - status: BookingStatus.ACCEPTED, - endTime: daysAgo(3), - eventType: { - disableRescheduling: false, - allowReschedulingPastBookings: false, - allowBookingFromCancelledBookingReschedule: false, - teamId: 999, // This team ID is NOT in the environment variable - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - // Should redirect to eventUrl with name/email params (fallback behavior) - expectRedirectToEventBookingPageWithParams({ - result, - eventUrl: "https://example.com/event", - params: { - name: "John Doe", - email: "john.doe@example.com", - }, - }); - }); - - it("should redirect to event URL when booking has no team (individual event)", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - uid: "no-team-booking-uid", - status: BookingStatus.ACCEPTED, - endTime: daysAgo(3), - eventType: { - disableRescheduling: false, - allowReschedulingPastBookings: false, - allowBookingFromCancelledBookingReschedule: false, - teamId: null, // No team ID - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - // Should redirect to eventUrl with name/email params (fallback behavior) - expectRedirectToEventBookingPageWithParams({ - result, - eventUrl: "https://example.com/event", - params: { - name: "John Doe", - email: "john.doe@example.com", - }, - }); - }); - - it("should not prevent reschedule when allowReschedulingPastBookings is true even if the teamId is in the environment variable", () => { - const input = createReschedulePreventionRedirectInput({ - booking: createTestBooking({ - uid: "allowed-past-reschedule-uid", - status: BookingStatus.ACCEPTED, - endTime: daysAgo(3), - eventType: { - allowReschedulingPastBookings: true, // Past reschedule explicitly allowed - teamId: 456, // Team ID is in environment variable - }, - }), - }); - const result = determineReschedulePreventionRedirect(input); - - expectToNotPreventReschedule(result); - }); - }); -}); diff --git a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.ts b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.ts deleted file mode 100644 index 8ecf4671223cae..00000000000000 --- a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { URLSearchParams } from "url"; - -import { getFullName } from "@calcom/features/form-builder/utils"; -import { ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS } from "@calcom/lib/constants"; -import { getSafe } from "@calcom/lib/getSafe"; -import { BookingStatus } from "@calcom/prisma/enums"; -import type { JsonValue } from "@calcom/types/Json"; - -export type ReschedulePreventionRedirectInput = { - booking: { - uid: string; - status: BookingStatus; - endTime: Date | null; - responses?: JsonValue; - eventType: { - disableRescheduling: boolean; - allowReschedulingPastBookings: boolean; - allowBookingFromCancelledBookingReschedule: boolean; - teamId: number | null; - }; - }; - eventUrl: string; - forceRescheduleForCancelledBooking?: boolean; - bookingSeat?: { - data: JsonValue; - booking: { - uid: string; - id: number; - }; - }; -}; - -export type ReschedulePreventionRedirectResult = string | null; - -/** - * - * Parses the PAST_BOOKING_RESCHEDULE_NO_BOOKING_BEHAVIOUR environment variable and checks if the given team ID has the changed behaviour for past booking reschedule enabled - * - * The behaviour is that it does not allow allowing booking through the reschedule link of a past booking by default. If allowReschedulingPastBookings is true, then this behaviour isn't applicable - */ -function isPastBookingRescheduleBehaviourToPreventBooking(teamId: number | null | undefined): boolean { - if (!teamId) { - return false; - } - - if (!ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS) { - return false; - } - - const configuredTeamIds = ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS.split(",") - .map((id) => id.trim()) - .filter((id) => id !== "") - .map((id) => parseInt(id, 10)) - .filter((id) => !isNaN(id)); - - return configuredTeamIds.includes(teamId); -} - -/** - * Determines the appropriate redirect URL for a reschedule request based on booking status and event type settings - * Returns the redirect URL string if a redirect is needed, null if reschedule should proceed normally - */ -export function determineReschedulePreventionRedirect( - input: ReschedulePreventionRedirectInput -): ReschedulePreventionRedirectResult { - const { booking, eventUrl, forceRescheduleForCancelledBooking, bookingSeat } = input; - - const isDisabledRescheduling = booking.eventType.disableRescheduling; - if (isDisabledRescheduling) { - return `/booking/${booking.uid}`; - } - - const isNonRescheduleableBooking = - booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED; - const isForcedRescheduleForCancelledBooking = forceRescheduleForCancelledBooking; - - if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) { - const canBookThroughCancelledBookingRescheduleLink = - booking.eventType.allowBookingFromCancelledBookingReschedule; - const allowedToBeBookedThroughCancelledBookingRescheduleLink = - booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink; - - return allowedToBeBookedThroughCancelledBookingRescheduleLink ? eventUrl : `/booking/${booking.uid}`; - } - - const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date(); - if (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) { - // Check if this team should apply the redirect behavior for past bookings - const isNewPastBookingRescheduleBehaviour = isPastBookingRescheduleBehaviourToPreventBooking( - booking.eventType.teamId - ); - - if (isNewPastBookingRescheduleBehaviour) { - return `/booking/${booking.uid}`; - } - - const destinationUrlSearchParams = new URLSearchParams(); - const responses = bookingSeat ? getSafe(bookingSeat.data, ["responses"]) : booking.responses; - const name = getFullName(getSafe(responses, ["name"])); - const email = getSafe(responses, ["email"]); - - if (name) destinationUrlSearchParams.set("name", name); - if (email) destinationUrlSearchParams.set("email", email); - - const searchParamsString = destinationUrlSearchParams.toString(); - return searchParamsString ? `${eventUrl}?${searchParamsString}` : eventUrl; - } - - // Allow reschedule to proceed - default behaviour - return null; -} diff --git a/packages/features/calendar-view/LargeCalendar.tsx b/packages/features/calendar-view/LargeCalendar.tsx deleted file mode 100644 index 0659df79dab82b..00000000000000 --- a/packages/features/calendar-view/LargeCalendar.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useMemo, useEffect } from "react"; - -import dayjs from "@calcom/dayjs"; -import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; -import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots"; -import type { BookerEvent } from "@calcom/features/bookings/types"; -import { Calendar } from "@calcom/features/calendars/weeklyview"; -import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events"; -import { localStorage } from "@calcom/lib/webstorage"; - -import { useOverlayCalendarStore } from "../bookings/Booker/components/OverlayCalendar/store"; -import type { useScheduleForEventReturnType } from "../bookings/Booker/utils/event"; -import { getQueryParam } from "../bookings/Booker/utils/query-param"; - -export const LargeCalendar = ({ - extraDays, - schedule, - isLoading, - event, -}: { - extraDays: number; - schedule?: useScheduleForEventReturnType["data"]; - isLoading: boolean; - event: { - data?: Pick | null; - }; -}) => { - const selectedDate = useBookerStoreContext((state) => state.selectedDate); - const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration); - const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates); - const displayOverlay = - getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault"); - - const eventDuration = selectedEventDuration || event?.data?.length || 30; - - const availableSlots = useAvailableTimeSlots({ schedule, eventDuration }); - - const startDate = selectedDate ? dayjs(selectedDate).toDate() : dayjs().toDate(); - const endDate = dayjs(startDate) - .add(extraDays - 1, "day") - .toDate(); - - // HACK: force rerender when overlay events change - // Sine we dont use react router here we need to force rerender (ATOM SUPPORT) - // eslint-disable-next-line @typescript-eslint/no-empty-function - useEffect(() => {}, [displayOverlay]); - - const overlayEventsForDate = useMemo(() => { - if (!overlayEvents || !displayOverlay) return []; - return overlayEvents.map((event, id) => { - return { - id, - start: dayjs(event.start).toDate(), - end: dayjs(event.end).toDate(), - title: "Busy", - options: { - status: "ACCEPTED", - }, - } as CalendarEvent; - }); - }, [overlayEvents, displayOverlay]); - - return ( -
- -
- ); -}; diff --git a/packages/features/credentials/deleteCredential.test.ts b/packages/features/credentials/deleteCredential.test.ts index ebe279d33e8e97..c30570bc78f316 100644 --- a/packages/features/credentials/deleteCredential.test.ts +++ b/packages/features/credentials/deleteCredential.test.ts @@ -5,7 +5,7 @@ import { import { describe, test, expect, beforeEach } from "vitest"; -import { PrismaAppRepository } from "@calcom/features/apps/repository/PrismaAppRepository"; +import { PrismaAppRepository } from "@calcom/lib/server/repository/PrismaAppRepository"; import { CredentialRepository } from "@calcom/lib/server/repository/credential"; import { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository"; diff --git a/packages/features/credentials/handleDeleteCredential.ts b/packages/features/credentials/handleDeleteCredential.ts index c7f32af94f00d9..66963ba2459226 100644 --- a/packages/features/credentials/handleDeleteCredential.ts +++ b/packages/features/credentials/handleDeleteCredential.ts @@ -14,7 +14,7 @@ import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/sc import { buildNonDelegationCredential } from "@calcom/lib/delegationCredential/server"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; -import { DailyLocationType } from "@calcom/app-store/locations"; +import { DailyLocationType } from "@calcom/lib/location"; import { getTranslation } from "@calcom/lib/server/i18n"; import { bookingMinimalSelect, prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; diff --git a/packages/features/data-table/components/filters/DateRangeFilter.tsx b/packages/features/data-table/components/filters/DateRangeFilter.tsx index 96e6464c5d0f82..2456400bbd9763 100644 --- a/packages/features/data-table/components/filters/DateRangeFilter.tsx +++ b/packages/features/data-table/components/filters/DateRangeFilter.tsx @@ -4,7 +4,6 @@ import { useState, useEffect, useCallback } from "react"; import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants"; import classNames from "@calcom/ui/classNames"; import { Badge } from "@calcom/ui/components/badge"; import { Button, buttonClasses } from "@calcom/ui/components/button"; @@ -30,7 +29,6 @@ import { getDateRangeFromPreset, type PresetOption, } from "../../lib/dateRange"; -import { preserveLocalTime } from "../../lib/preserveLocalTime"; import type { FilterableColumn, DateRangeFilterOptions } from "../../lib/types"; import { ZDateRangeFilterValue, ColumnFilterType } from "../../lib/types"; import { useFilterPopoverOpen } from "./useFilterPopoverOpen"; @@ -50,8 +48,9 @@ export const DateRangeFilter = ({ }: DateRangeFilterProps) => { const { open, onOpenChange } = useFilterPopoverOpen(column.id); const filterValue = useFilterValue(column.id, ZDateRangeFilterValue); - const { updateFilter, removeFilter, timeZone: givenTimeZone } = useDataTable(); + const { updateFilter, removeFilter } = useDataTable(); const range = options?.range ?? "past"; + const endOfDay = options?.endOfDay ?? false; const forceCustom = range === "custom"; const forcePast = range === "past"; @@ -71,19 +70,6 @@ export const DateRangeFilter = ({ : DEFAULT_PRESET ); - const convertTimestamp = useCallback( - (timestamp: string) => { - if (!options?.convertToTimeZone) { - return timestamp; - } - if (!givenTimeZone || CURRENT_TIMEZONE === givenTimeZone) { - return timestamp; - } - return preserveLocalTime(timestamp, CURRENT_TIMEZONE, givenTimeZone); - }, - [options?.convertToTimeZone, givenTimeZone] - ); - const updateValues = useCallback( ({ preset, startDate, endDate }: { preset: PresetOption; startDate?: Dayjs; endDate?: Dayjs }) => { setSelectedPreset(preset); @@ -94,14 +80,14 @@ export const DateRangeFilter = ({ updateFilter(column.id, { type: ColumnFilterType.DATE_RANGE, data: { - startDate: convertTimestamp(startDate.toDate().toISOString()), - endDate: convertTimestamp(endDate.toDate().toISOString()), + startDate: startDate.toDate().toISOString(), + endDate: (endOfDay ? endDate.endOf("day") : endDate).toDate().toISOString(), preset: preset.value, }, }); } }, - [column.id, updateFilter, convertTimestamp] + [column.id, endOfDay] ); useEffect(() => { @@ -140,12 +126,10 @@ export const DateRangeFilter = ({ startDate?: Date | undefined; endDate?: Date | undefined; }) => { - // DateRangePicker returns the beginning of the day, - // so we need to update `endDate` to the end of the day. updateValues({ preset: CUSTOM_PRESET, startDate: startDate ? dayjs(startDate) : getDefaultStartDate(), - endDate: endDate ? dayjs(endDate).add(1, "day").subtract(1, "millisecond") : undefined, + endDate: endDate ? dayjs(endDate) : undefined, }); }; diff --git a/packages/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime.ts b/packages/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime.ts index 4f91d41ef72e65..549e6192e7a597 100644 --- a/packages/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime.ts +++ b/packages/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime.ts @@ -1,20 +1,28 @@ import { useMemo } from "react"; -import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants"; +import dayjs from "@calcom/dayjs"; import { preserveLocalTime } from "../lib/preserveLocalTime"; import { useDataTable } from "./useDataTable"; /** * Converts a timestamp to maintain the same local time in a different timezone. - * Fore more info, read packages/features/data-table/lib/preserveLocalTime.ts + * + * For example, if it's midnight (00:00) in Paris time: + * - Input : "2025-05-22T22:00:00.000Z" (Midnight/00:00 in Paris) + * - Output: "2025-05-22T15:00:00.000Z" (Midnight/00:00 in Seoul) + * + * This ensures that times like midnight (00:00) or end of day (23:59) + * remain at those exact local times when converting between timezones. + * The output timestamp is based on the timezone in the user's profile settings. */ export function useChangeTimeZoneWithPreservedLocalTime(isoString: string) { const { timeZone: profileTimeZone } = useDataTable(); return useMemo(() => { - if (!profileTimeZone || CURRENT_TIMEZONE === profileTimeZone) { + const currentTimeZone = dayjs.tz.guess(); + if (!profileTimeZone || currentTimeZone === profileTimeZone) { return isoString; } - return preserveLocalTime(isoString, CURRENT_TIMEZONE, profileTimeZone); + return preserveLocalTime(isoString, currentTimeZone, profileTimeZone); }, [isoString, profileTimeZone]); } diff --git a/packages/features/data-table/lib/preserveLocalTime.ts b/packages/features/data-table/lib/preserveLocalTime.ts index b51eeb98c7b00a..783e24ac82d683 100644 --- a/packages/features/data-table/lib/preserveLocalTime.ts +++ b/packages/features/data-table/lib/preserveLocalTime.ts @@ -10,12 +10,6 @@ import dayjs from "@calcom/dayjs"; * This ensures that times like midnight (00:00) or end of day (23:59) * remain at those exact local times when converting between timezones. * The output timestamp is based on the timezone in the user's profile settings. - * - * For example, the profile timezone is Asia/Seoul, - * but the current user is in Europe/Paris. - * `Date` pickers will normally emit timestamps in the user's local timezone. (00:00:00 ~ 23:59:59 in Paris time) - * but what we really want is to fetch the data based on the user's profile timezone. (00:00:00 ~ 23:59:59 in Seoul time) - * That's why we need to convert the timestamp to the user's profile timezone. */ export const preserveLocalTime = (isoString: string, originalTimeZone: string, targetTimeZone: string) => { // Parse the input time diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts index 033d58179d5367..2589d7d661f8d1 100644 --- a/packages/features/data-table/lib/types.ts +++ b/packages/features/data-table/lib/types.ts @@ -116,7 +116,7 @@ export const ZFilterValue = z.union([ export type DateRangeFilterOptions = { range?: "past" | "custom"; - convertToTimeZone?: boolean; + endOfDay?: boolean; }; export type TextFilterOptions = { diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts index e19106ab6a7688..702f93b9ca0529 100644 --- a/packages/features/ee/billing/credit-service.test.ts +++ b/packages/features/ee/billing/credit-service.test.ts @@ -59,16 +59,14 @@ vi.mock("@calcom/lib/server/repository/membership"); vi.mock("@calcom/lib/server/repository/team"); vi.mock("@calcom/emails/email-manager"); vi.mock("../workflows/lib/reminders/reminderScheduler", () => ({ - cancelScheduledMessagesAndScheduleEmails: vi.fn().mockResolvedValue(undefined), + cancelScheduledMessagesAndScheduleEmails: vi.fn(), })); const creditService = new CreditService(); vi.spyOn(creditService, "_getAllCreditsForTeam").mockResolvedValue({ - totalMonthlyCredits: 10, totalRemainingMonthlyCredits: 5, additionalCredits: 0, - totalCreditsUsedThisMonth: 5, }); vi.spyOn(creditService, "_getTeamWithAvailableCredits").mockResolvedValue({ @@ -235,7 +233,6 @@ describe("CreditService", () => { totalMonthlyCredits: 500, totalRemainingMonthlyCredits: 20, additionalCredits: 60, - totalCreditsUsedThisMonth: 480, }); await creditService.chargeCredits({ @@ -294,7 +291,6 @@ describe("CreditService", () => { totalMonthlyCredits: 500, totalRemainingMonthlyCredits: -1, additionalCredits: 0, - totalCreditsUsedThisMonth: 501, }); await creditService.chargeCredits({ @@ -325,7 +321,6 @@ describe("CreditService", () => { totalMonthlyCredits: 500, totalRemainingMonthlyCredits: 100, additionalCredits: 50, - totalCreditsUsedThisMonth: 400, }); const result = await creditService.getUserOrTeamToCharge({ @@ -345,7 +340,6 @@ describe("CreditService", () => { totalMonthlyCredits: 500, totalRemainingMonthlyCredits: 0, additionalCredits: 50, - totalCreditsUsedThisMonth: 500, }); const result = await creditService.getUserOrTeamToCharge({ @@ -489,18 +483,13 @@ describe("CreditService", () => { describe("getAllCreditsForTeam", () => { it("should calculate total and remaining credits correctly", async () => { - vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) - .mockResolvedValueOnce({ - additionalCredits: 100, - expenseLogs: [ - { credits: 50, date: new Date() }, - { credits: 30, date: new Date() }, - ], - }) - .mockResolvedValueOnce({ - additionalCredits: 100, - expenseLogs: [{ credits: 80, date: new Date() }], - }); + vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs).mockResolvedValue({ + additionalCredits: 100, + expenseLogs: [ + { credits: 50, date: new Date() }, + { credits: 30, date: new Date() }, + ], + }); vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500); @@ -509,79 +498,22 @@ describe("CreditService", () => { totalMonthlyCredits: 500, totalRemainingMonthlyCredits: 420, // 500 - (50 + 30) additionalCredits: 100, - totalCreditsUsedThisMonth: 80, // 50 + 30 }); }); it("should handle no expense logs", async () => { - vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) - .mockResolvedValueOnce({ - additionalCredits: 100, - expenseLogs: [], - }) - .mockResolvedValueOnce({ - additionalCredits: 100, - expenseLogs: [], - }); - - vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500); - - const result = await creditService.getAllCreditsForTeam(1); - expect(result).toEqual({ - totalMonthlyCredits: 500, - totalRemainingMonthlyCredits: 500, + vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs).mockResolvedValue({ additionalCredits: 100, - totalCreditsUsedThisMonth: 0, // no expenses - }); - }); - - it("should calculate total credits including additional credits for the month", async () => { - vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) - .mockResolvedValueOnce({ - additionalCredits: 150, - expenseLogs: [ - { credits: 80, date: new Date() }, - { credits: 40, date: new Date() }, - ], - }) - .mockResolvedValueOnce({ - additionalCredits: 150, - expenseLogs: [ - { credits: 25, date: new Date() }, - { credits: 15, date: new Date() }, - ], - }); - - vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500); - - const result = await creditService.getAllCreditsForTeam(1); - expect(result).toEqual({ - totalMonthlyCredits: 500, - totalRemainingMonthlyCredits: 380, // 500 - (80 + 40) - additionalCredits: 150, - totalCreditsUsedThisMonth: 120, // 80 + 40 + expenseLogs: [], }); - }); - - it("should handle zero additional credits", async () => { - vi.mocked(CreditsRepository.findCreditBalanceWithExpenseLogs) - .mockResolvedValueOnce({ - additionalCredits: 0, - expenseLogs: [{ credits: 100, date: new Date() }], - }) - .mockResolvedValueOnce({ - additionalCredits: 0, - expenseLogs: [], - }); vi.spyOn(CreditService.prototype, "getMonthlyCredits").mockResolvedValue(500); const result = await creditService.getAllCreditsForTeam(1); expect(result).toEqual({ totalMonthlyCredits: 500, - totalRemainingMonthlyCredits: 400, // 500 - 100 - additionalCredits: 0, - totalCreditsUsedThisMonth: 100, + totalRemainingMonthlyCredits: 500, + additionalCredits: 100, }); }); }); @@ -772,7 +704,6 @@ describe("CreditService", () => { totalMonthlyCredits: 500, totalRemainingMonthlyCredits: 200, additionalCredits: 100, - totalCreditsUsedThisMonth: 300, }); const result = await creditService.getTeamWithAvailableCredits(1); expect(result).toEqual({ @@ -896,7 +827,6 @@ describe("CreditService", () => { totalMonthlyCredits: 500, totalRemainingMonthlyCredits: 100, additionalCredits: 100, - totalCreditsUsedThisMonth: 400, }); await creditService.chargeCredits({ diff --git a/packages/features/ee/billing/credit-service.ts b/packages/features/ee/billing/credit-service.ts index 3f80f16d816fac..067b3f3dde5146 100644 --- a/packages/features/ee/billing/credit-service.ts +++ b/packages/features/ee/billing/credit-service.ts @@ -651,7 +651,6 @@ export class CreditService { totalMonthlyCredits: 0, totalRemainingMonthlyCredits: 0, additionalCredits: creditBalance?.additionalCredits ?? 0, - totalCreditsUsedThisMonth: 0, }; } @@ -659,7 +658,6 @@ export class CreditService { totalMonthlyCredits: 0, totalRemainingMonthlyCredits: 0, additionalCredits: 0, - totalCreditsUsedThisMonth: 0, }; } @@ -679,14 +677,10 @@ export class CreditService { const totalMonthlyCreditsUsed = creditBalance?.expenseLogs.reduce((sum, log) => sum + (log?.credits ?? 0), 0) || 0; - const additionalCredits = creditBalance?.additionalCredits ?? 0; - const totalCreditsUsedThisMonth = totalMonthlyCreditsUsed; - return { totalMonthlyCredits, totalRemainingMonthlyCredits: Math.max(totalMonthlyCredits - totalMonthlyCreditsUsed, 0), - additionalCredits, - totalCreditsUsedThisMonth, + additionalCredits: creditBalance?.additionalCredits ?? 0, }; } } diff --git a/packages/features/ee/dsync/lib/handleGroupEvents.ts b/packages/features/ee/dsync/lib/handleGroupEvents.ts index f067d7e206bada..fecc8c0715a2a1 100644 --- a/packages/features/ee/dsync/lib/handleGroupEvents.ts +++ b/packages/features/ee/dsync/lib/handleGroupEvents.ts @@ -3,7 +3,7 @@ import type { DirectorySyncEvent, Group } from "@boxyhq/saml-jackson"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { addNewMembersToEventTypes } from "@calcom/features/ee/teams/lib/queries"; +import { addNewMembersToEventTypes } from "@calcom/lib/server/queries/teams"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; import prisma from "@calcom/prisma"; import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums"; diff --git a/packages/features/ee/impersonation/lib/ImpersonationProvider.ts b/packages/features/ee/impersonation/lib/ImpersonationProvider.ts index 671acba3b400c3..bd9c340f5a9581 100644 --- a/packages/features/ee/impersonation/lib/ImpersonationProvider.ts +++ b/packages/features/ee/impersonation/lib/ImpersonationProvider.ts @@ -3,7 +3,6 @@ import CredentialsProvider from "next-auth/providers/credentials"; import { z } from "zod"; import { ensureOrganizationIsReviewed } from "@calcom/ee/organizations/lib/ensureOrganizationIsReviewed"; -import { getOrgFullOrigin, subdomainSuffix } from "@calcom/ee/organizations/lib/orgDomains"; import { getSession } from "@calcom/features/auth/lib/getSession"; import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; @@ -54,8 +53,6 @@ const auditAndReturnNextUser = async ( }, }); - const profileOrg = impersonatedUser.profile?.organization; - const obj = { id: impersonatedUser.id, username: impersonatedUser.username, @@ -66,18 +63,6 @@ const auditAndReturnNextUser = async ( organizationId: impersonatedUser.organizationId, locale: impersonatedUser.locale, profile: impersonatedUser.profile, - // Add org object if the user belongs to an organization - ...(profileOrg && { - org: { - id: profileOrg.id, - name: profileOrg.name, - slug: profileOrg.slug || "", - logoUrl: profileOrg.logoUrl, - fullDomain: getOrgFullOrigin(profileOrg.slug || ""), - domainSuffix: subdomainSuffix(), - role: profileOrg.members?.[0]?.role || MembershipRole.MEMBER, - }, - }), }; if (!isReturningToSelf) { diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index dbf145bbbc1459..65aec9de056125 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -12,7 +12,7 @@ import { getBooking } from "@calcom/features/bookings/lib/payment/getBooking"; import stripe from "@calcom/features/ee/payments/server/stripe"; import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params"; import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository"; -import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; +import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager"; import { IS_PRODUCTION } from "@calcom/lib/constants"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import { HttpError as HttpCode } from "@calcom/lib/http-error"; diff --git a/packages/features/ee/round-robin/handleRescheduleEventManager.ts b/packages/features/ee/round-robin/handleRescheduleEventManager.ts index 166b1b05ac4f0e..90201ced9cfb42 100644 --- a/packages/features/ee/round-robin/handleRescheduleEventManager.ts +++ b/packages/features/ee/round-robin/handleRescheduleEventManager.ts @@ -4,8 +4,8 @@ import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/book import type { EventType } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials"; import { getVideoCallDetails } from "@calcom/features/bookings/lib/handleNewBooking/getVideoCallDetails"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; -import type { EventManagerInitParams } from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; +import type { EventManagerInitParams } from "@calcom/lib/EventManager"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import { BookingReferenceRepository } from "@calcom/lib/server/repository/bookingReference"; diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts index 1a9a50efda16c8..d48ab97bc15bdd 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts @@ -22,7 +22,7 @@ import { BookingRepository } from "@calcom/lib/server/repository/booking"; import { SchedulingType, BookingStatus, WorkflowMethods } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; -vi.mock("@calcom/features/bookings/lib/EventManager"); +vi.mock("@calcom/lib/EventManager"); vi.mock("@calcom/app-store/utils", () => ({ getAppFromSlug: vi.fn(), })); @@ -177,7 +177,7 @@ type ConferenceResult = { }; const mockEventManagerReschedule = async (config?: MockEventManagerConfig) => { - const EventManager = (await import("@calcom/features/bookings/lib/EventManager")).default; + const EventManager = (await import("@calcom/lib/EventManager")).default; // eslint-disable-next-line @typescript-eslint/no-explicit-any const spy = vi.spyOn(EventManager.prototype as any, "reschedule"); diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index 11b1221c7f393c..7cef29f663f06d 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -23,7 +23,7 @@ import { import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; import { SENDER_NAME } from "@calcom/lib/constants"; import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; @@ -433,7 +433,6 @@ export const roundRobinManualReassignment = async ({ // send email with event updates to attendees await sendRoundRobinUpdatedEmailsAndSMS({ calEvent: evtWithoutCancellationReason, - eventTypeMetadata: eventType?.metadata as EventTypeMetadata, }); } diff --git a/packages/features/ee/round-robin/roundRobinReassignment.test.ts b/packages/features/ee/round-robin/roundRobinReassignment.test.ts index 375c1384e5c3ff..3c1f91b7ccfaf9 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.test.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.test.ts @@ -21,7 +21,7 @@ import { BookingRepository } from "@calcom/lib/server/repository/booking"; import { SchedulingType, BookingStatus, WorkflowMethods } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; -vi.mock("@calcom/features/bookings/lib/EventManager"); +vi.mock("@calcom/lib/EventManager"); const testDestinationCalendar = { integration: "test-calendar", @@ -61,7 +61,7 @@ describe("roundRobinReassignment test", () => { test("reassign new round robin organizer", async ({ emails }) => { const roundRobinReassignment = (await import("./roundRobinReassignment")).default; - const EventManager = (await import("@calcom/features/bookings/lib/EventManager")).default; + const EventManager = (await import("@calcom/lib/EventManager")).default; const eventManagerSpy = vi.spyOn(EventManager.prototype as any, "reschedule"); eventManagerSpy.mockResolvedValue({ referencesToCreate: [] }); @@ -194,7 +194,7 @@ describe("roundRobinReassignment test", () => { // TODO: add fixed hosts test test("Reassign round robin host with fixed host as organizer", async () => { const roundRobinReassignment = (await import("./roundRobinReassignment")).default; - const EventManager = (await import("@calcom/features/bookings/lib/EventManager")).default; + const EventManager = (await import("@calcom/lib/EventManager")).default; const eventManagerSpy = vi.spyOn(EventManager.prototype as any, "reschedule"); diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index 796600d711dece..f0793d1207dc77 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -19,7 +19,7 @@ import AssignmentReasonRecorder, { RRReassignmentType, } from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder"; import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; import { enrichHostsWithDelegationCredentials, enrichUserWithDelegationCredentialsIncludeServiceAccountKey, @@ -483,7 +483,6 @@ export const roundRobinReassignment = async ({ // send email with event updates to attendees await sendRoundRobinUpdatedEmailsAndSMS({ calEvent: evtWithoutCancellationReason, - eventTypeMetadata: eventType?.metadata as EventTypeMetadata, }); } diff --git a/packages/features/ee/sso/lib/saml.ts b/packages/features/ee/sso/lib/saml.ts index a409338e7ffb08..877a8a9c03ac1f 100644 --- a/packages/features/ee/sso/lib/saml.ts +++ b/packages/features/ee/sso/lib/saml.ts @@ -1,7 +1,7 @@ import type { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || ""; export const isSAMLLoginEnabled = samlDatabaseUrl.length > 0; diff --git a/packages/features/ee/support/lib/intercom/provider.tsx b/packages/features/ee/support/lib/intercom/provider.tsx index 3a6ff6f2b2f4f8..45fbda0b05f235 100644 --- a/packages/features/ee/support/lib/intercom/provider.tsx +++ b/packages/features/ee/support/lib/intercom/provider.tsx @@ -1,6 +1,5 @@ "use client"; -import { useSession } from "next-auth/react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, type FC } from "react"; import { IntercomProvider } from "react-use-intercom"; @@ -32,9 +31,6 @@ const Provider: FC<{ children: React.ReactNode }> = ({ children }) => { const searchParams = useSearchParams(); const pathname = usePathname(); const router = useRouter(); - const { data: session } = useSession(); - - const isBeingImpersonated = !!session?.user?.impersonatedBy?.id; const shouldOpenSupport = pathname === "/event-types" && (searchParams?.has("openPlain") || searchParams?.has("openSupport")); @@ -69,7 +65,7 @@ const Provider: FC<{ children: React.ReactNode }> = ({ children }) => { const isOnboardingPage = pathname?.startsWith("/getting-started"); const isCalVideoPage = pathname?.startsWith("/video/"); - if (isOnboardingPage || isCalVideoPage || isBeingImpersonated) { + if (isOnboardingPage || isCalVideoPage) { return <>{children}; } diff --git a/packages/features/ee/teams/components/AddNewTeamMembers.tsx b/packages/features/ee/teams/components/AddNewTeamMembers.tsx index 5c03b60da7549e..7699bcb77862e7 100644 --- a/packages/features/ee/teams/components/AddNewTeamMembers.tsx +++ b/packages/features/ee/teams/components/AddNewTeamMembers.tsx @@ -113,7 +113,7 @@ export const AddNewTeamMembersForm = ({ teamId, isOrg }: { teamId: number; isOrg /> ))} - {totalFetched > 0 && ( + {totalFetched && (
- {!isWhatsappAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && - !isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && ( -
- {_isSenderIsNeeded ? ( - <> -
-
- -
- -
- -
{t("sender_id_info")}
-
-
- {form.formState.errors.steps && - form.formState?.errors?.steps[step.stepNumber - 1]?.sender && ( -

{t("sender_id_error_message")}

- )} - - ) : ( - <> -
- - -
- - )} -
- )} {isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && !stepAgentId && (
diff --git a/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts b/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts index 75ec635ce0f8fc..564e23653d7ff2 100644 --- a/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts +++ b/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts @@ -1,7 +1,7 @@ import type { getEventTypeResponse } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB"; import { scheduleEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager"; import type { Workflow } from "@calcom/features/ee/workflows/lib/types"; -import type { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +import type { getDefaultEvent } from "@calcom/lib/defaultEvents"; import logger from "@calcom/lib/logger"; import { withReporting } from "@calcom/lib/sentryWrapper"; import { WorkflowTriggerEvents, TimeUnit, WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums"; diff --git a/packages/features/eventtypes/components/EventTypeLayout.tsx b/packages/features/eventtypes/components/EventTypeLayout.tsx index 363c403303fd1b..6f0e54aebb9652 100644 --- a/packages/features/eventtypes/components/EventTypeLayout.tsx +++ b/packages/features/eventtypes/components/EventTypeLayout.tsx @@ -276,11 +276,7 @@ function EventTypeSingleLayout({
}> - - -
- }> + }>
setEditModalOpen(true); setWebhookToEdit(webhook); }} - // TODO (SEAN): Implement Permissions here when we have event-types PR merged - permissions={{ - canEditWebhook: !webhookLockedStatus.disabled, - canDeleteWebhook: !webhookLockedStatus.disabled, - }} /> ); })} diff --git a/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx b/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx index 0cae9b1b9ad7ea..87e27a22002f14 100644 --- a/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx +++ b/packages/features/eventtypes/components/tabs/workflows/EventWorkfowsTab.tsx @@ -25,10 +25,7 @@ import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions"; -type PartialWorkflowType = Pick< - WorkflowType, - "name" | "activeOn" | "isOrg" | "steps" | "id" | "readOnly" | "permissions" ->; +type PartialWorkflowType = Pick; type ItemProps = { workflow: PartialWorkflowType; @@ -45,6 +42,14 @@ const WorkflowListItem = (props: ItemProps) => { const { workflow, eventType, isActive } = props; const { t } = useLocale(); + const [activeEventTypeIds, setActiveEventTypeIds] = useState( + workflow.activeOn?.map((active) => { + if (active.eventType) { + return active.eventType.id; + } + }) ?? [] + ); + const utils = trpc.useUtils(); const activateEventTypeMutation = trpc.viewer.workflows.activateEventType.useMutation({ @@ -132,7 +137,7 @@ const WorkflowListItem = (props: ItemProps) => {
- {workflow.permissions?.canUpdate && ( + {!workflow.readOnly && (
- - - - - {options.map((option) => ( - { - onChange(option.value); - setOpen(false); - }} - className="flex items-center gap-2 px-4 py-3"> -
-
{option.label}
-
{option.description}
-
- {selectedOption.value === option.value && ( - - )} -
- ))} -
-
-
- - ); -}; diff --git a/packages/features/insights/filters/Download/Download.tsx b/packages/features/insights/filters/Download/Download.tsx index ed911a8e2d7464..3a40d7602dec55 100644 --- a/packages/features/insights/filters/Download/Download.tsx +++ b/packages/features/insights/filters/Download/Download.tsx @@ -15,7 +15,6 @@ import { import { showToast, showProgressToast, hideProgressToast } from "@calcom/ui/components/toast"; import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters"; -import { extractDateRangeFromColumnFilters } from "../../lib/bookingUtils"; type RawData = RouterOutputs["viewer"]["insights"]["rawData"]["data"][number]; @@ -24,7 +23,7 @@ const BATCH_SIZE = 100; const Download = () => { const { t } = useLocale(); const insightsBookingParams = useInsightsBookingParameters(); - const { startDate, endDate } = extractDateRangeFromColumnFilters(insightsBookingParams.columnFilters); + const { startDate, endDate } = insightsBookingParams; const [isDownloading, setIsDownloading] = useState(false); const utils = trpc.useUtils(); diff --git a/packages/features/insights/hooks/useInsightsBookingParameters.ts b/packages/features/insights/hooks/useInsightsBookingParameters.ts index 1330346f9d1140..cbe87615cb4674 100644 --- a/packages/features/insights/hooks/useInsightsBookingParameters.ts +++ b/packages/features/insights/hooks/useInsightsBookingParameters.ts @@ -1,14 +1,12 @@ import { useMemo } from "react"; import dayjs from "@calcom/dayjs"; +import { ZDateRangeFilterValue } from "@calcom/features/data-table"; +import { useChangeTimeZoneWithPreservedLocalTime } from "@calcom/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime"; import { useColumnFilters } from "@calcom/features/data-table/hooks/useColumnFilters"; import { useDataTable } from "@calcom/features/data-table/hooks/useDataTable"; -import { - getDefaultStartDate, - getDefaultEndDate, - DEFAULT_PRESET, -} from "@calcom/features/data-table/lib/dateRange"; -import { ColumnFilterType, type ColumnFilter } from "@calcom/features/data-table/lib/types"; +import { useFilterValue } from "@calcom/features/data-table/hooks/useFilterValue"; +import { getDefaultStartDate, getDefaultEndDate } from "@calcom/features/data-table/lib/dateRange"; import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants"; import { useInsightsOrgTeams } from "./useInsightsOrgTeams"; @@ -16,35 +14,33 @@ import { useInsightsOrgTeams } from "./useInsightsOrgTeams"; export function useInsightsBookingParameters() { const { scope, selectedTeamId } = useInsightsOrgTeams(); const { timeZone } = useDataTable(); - const columnFilters = useColumnFilters(); - const columnFiltersWithDefaultDateRange = useMemo(() => { - const hasDateRangeFilter = columnFilters.find( - (filter) => filter.id === "startTime" || filter.id === "createdAt" - ); - if (hasDateRangeFilter) { - return columnFilters; - } else { - return [ - ...columnFilters, - { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: dayjs(getDefaultStartDate().toISOString()).startOf("day").toISOString(), - endDate: dayjs(getDefaultEndDate().toISOString()).endOf("day").toISOString(), - preset: DEFAULT_PRESET.value, - }, - }, - } satisfies ColumnFilter, - ]; - } - }, [columnFilters]); + + const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data; + // TODO for future: this preserving local time & startOf & endOf should be handled + // from DateRangeFilter out of the box. + // When we do it, we also need to remove those timezone handling logic from the backend side at the same time. + const startDate = useChangeTimeZoneWithPreservedLocalTime( + useMemo(() => { + return dayjs(createdAtRange?.startDate ?? getDefaultStartDate().toISOString()) + .startOf("day") + .toISOString(); + }, [createdAtRange?.startDate]) + ); + const endDate = useChangeTimeZoneWithPreservedLocalTime( + useMemo(() => { + return dayjs(createdAtRange?.endDate ?? getDefaultEndDate().toISOString()) + .endOf("day") + .toISOString(); + }, [createdAtRange?.endDate]) + ); + const columnFilters = useColumnFilters({ exclude: ["createdAt"] }); return { scope, selectedTeamId, + startDate, + endDate, timeZone: timeZone || CURRENT_TIMEZONE, - columnFilters: columnFiltersWithDefaultDateRange, + columnFilters, }; } diff --git a/packages/features/insights/lib/__tests__/bookingUtils.test.ts b/packages/features/insights/lib/__tests__/bookingUtils.test.ts deleted file mode 100644 index 05c2db0d1c594a..00000000000000 --- a/packages/features/insights/lib/__tests__/bookingUtils.test.ts +++ /dev/null @@ -1,538 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { ColumnFilterType, type ColumnFilter } from "@calcom/features/data-table/lib/types"; - -import { extractDateRangeFromColumnFilters, replaceDateRangeColumnFilter } from "../bookingUtils"; - -describe("extractDateRangeFromColumnFilters", () => { - const mockStartTimeFilter: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - preset: "thisMonth", - }, - }, - }; - - const mockCreatedAtFilter: ColumnFilter = { - id: "createdAt", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: "2024-02-01T00:00:00.000Z", - endDate: "2024-02-29T23:59:59.999Z", - preset: "lastMonth", - }, - }, - }; - - const mockTextFilter: ColumnFilter = { - id: "status", - value: { - type: ColumnFilterType.TEXT, - data: { - operator: "equals" as const, - operand: "confirmed", - }, - }, - }; - - const mockSingleSelectFilter: ColumnFilter = { - id: "eventType", - value: { - type: ColumnFilterType.SINGLE_SELECT, - data: "meeting", - }, - }; - - describe("successful extraction", () => { - it("should extract date range from startTime filter", () => { - const result = extractDateRangeFromColumnFilters([mockStartTimeFilter, mockTextFilter]); - - expect(result).toEqual({ - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - dateTarget: "startTime", - }); - }); - - it("should extract date range from createdAt filter", () => { - const result = extractDateRangeFromColumnFilters([mockCreatedAtFilter, mockSingleSelectFilter]); - - expect(result).toEqual({ - startDate: "2024-02-01T00:00:00.000Z", - endDate: "2024-02-29T23:59:59.999Z", - dateTarget: "createdAt", - }); - }); - - it("should find startTime filter when it comes after other filters", () => { - const result = extractDateRangeFromColumnFilters([ - mockTextFilter, - mockStartTimeFilter, - mockSingleSelectFilter, - ]); - - expect(result).toEqual({ - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - dateTarget: "startTime", - }); - }); - - it("should ignore non-date-range filters", () => { - const result = extractDateRangeFromColumnFilters([ - mockTextFilter, - mockSingleSelectFilter, - mockStartTimeFilter, - ]); - - expect(result).toEqual({ - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - dateTarget: "startTime", - }); - }); - }); - - describe("error cases", () => { - it("should throw error when no column filters provided", () => { - expect(() => extractDateRangeFromColumnFilters()).toThrow("No date range filter found"); - }); - - it("should throw error when column filters is undefined", () => { - expect(() => extractDateRangeFromColumnFilters(undefined)).toThrow("No date range filter found"); - }); - - it("should handle empty column filters array", () => { - expect(() => extractDateRangeFromColumnFilters([])).toThrow("No date range filter found"); - }); - - it("should throw error when no date range filters exist", () => { - expect(() => extractDateRangeFromColumnFilters([mockTextFilter, mockSingleSelectFilter])).toThrow( - "No date range filter found" - ); - }); - - it("should throw error when date range filter has missing startDate", () => { - const filterWithMissingStartDate: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: null, - endDate: "2024-01-31T23:59:59.999Z", - preset: "thisMonth", - }, - }, - }; - - expect(() => extractDateRangeFromColumnFilters([filterWithMissingStartDate])).toThrow( - "No date range filter found" - ); - }); - - it("should throw error when date range filter has missing endDate", () => { - const filterWithMissingEndDate: ColumnFilter = { - id: "createdAt", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: "2024-01-01T00:00:00.000Z", - endDate: null, - preset: "thisMonth", - }, - }, - }; - - expect(() => extractDateRangeFromColumnFilters([filterWithMissingEndDate])).toThrow( - "No date range filter found" - ); - }); - - it("should throw error when date range filter has both dates missing", () => { - const filterWithMissingDates: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: null, - endDate: null, - preset: "thisMonth", - }, - }, - }; - - expect(() => extractDateRangeFromColumnFilters([filterWithMissingDates])).toThrow( - "No date range filter found" - ); - }); - - it("should throw error when startTime/createdAt filter is not DATE_RANGE type", () => { - const invalidStartTimeFilter: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.TEXT, - data: { - operator: "equals" as const, - operand: "some-date", - }, - }, - }; - - expect(() => extractDateRangeFromColumnFilters([invalidStartTimeFilter])).toThrow( - "No date range filter found" - ); - }); - }); - - describe("edge cases", () => { - it("should work with only one date range filter among many others", () => { - const numberFilter: ColumnFilter = { - id: "number-filter", - value: { - type: ColumnFilterType.NUMBER, - data: { operator: "gt" as const, operand: 5 }, - }, - }; - - const anotherTextFilter: ColumnFilter = { - id: "another-text", - value: { - type: ColumnFilterType.TEXT, - data: { operator: "contains" as const, operand: "test" }, - }, - }; - - const manyFilters = [ - mockTextFilter, - mockSingleSelectFilter, - anotherTextFilter, - mockStartTimeFilter, - numberFilter, - ]; - - const result = extractDateRangeFromColumnFilters(manyFilters); - - expect(result).toEqual({ - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - dateTarget: "startTime", - }); - }); - }); -}); - -describe("replaceDateRangeColumnFilter", () => { - const mockStartTimeFilter: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - preset: "thisMonth", - }, - }, - }; - - const mockCreatedAtFilter: ColumnFilter = { - id: "createdAt", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: "2024-02-01T00:00:00.000Z", - endDate: "2024-02-29T23:59:59.999Z", - preset: "lastMonth", - }, - }, - }; - - const mockTextFilter: ColumnFilter = { - id: "status", - value: { - type: ColumnFilterType.TEXT, - data: { - operator: "equals" as const, - operand: "confirmed", - }, - }, - }; - - const mockSingleSelectFilter: ColumnFilter = { - id: "eventType", - value: { - type: ColumnFilterType.SINGLE_SELECT, - data: "meeting", - }, - }; - - const newStartDate = "2024-03-01T00:00:00.000Z"; - const newEndDate = "2024-03-31T23:59:59.999Z"; - - describe("successful replacement", () => { - it("should replace startTime filter with new dates", () => { - const columnFilters = [mockStartTimeFilter, mockTextFilter]; - - const result = replaceDateRangeColumnFilter({ - columnFilters, - newStartDate, - newEndDate, - }); - - expect(result).toEqual([ - { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: newStartDate, - endDate: newEndDate, - preset: "thisMonth", // Preserves original preset - }, - }, - }, - mockTextFilter, // Non-date filters remain unchanged - ]); - }); - - it("should replace createdAt filter with new dates", () => { - const columnFilters = [mockCreatedAtFilter, mockSingleSelectFilter]; - - const result = replaceDateRangeColumnFilter({ - columnFilters, - newStartDate, - newEndDate, - }); - - expect(result).toEqual([ - { - id: "createdAt", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: newStartDate, - endDate: newEndDate, - preset: "lastMonth", // Preserves original preset - }, - }, - }, - mockSingleSelectFilter, // Non-date filters remain unchanged - ]); - }); - - it("should preserve preset value when replacing filter", () => { - const customPresetFilter: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - preset: "customPreset", - }, - }, - }; - - const result = replaceDateRangeColumnFilter({ - columnFilters: [customPresetFilter], - newStartDate, - newEndDate, - }); - - expect(result![0].value).toMatchObject({ - data: expect.objectContaining({ - preset: "customPreset", - }), - }); - }); - - it("should leave non-date-range filters unchanged", () => { - const columnFilters = [mockTextFilter, mockSingleSelectFilter, mockStartTimeFilter]; - - const result = replaceDateRangeColumnFilter({ - columnFilters, - newStartDate, - newEndDate, - }); - - expect(result).toEqual([ - mockTextFilter, // Unchanged - mockSingleSelectFilter, // Unchanged - { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: newStartDate, - endDate: newEndDate, - preset: "thisMonth", - }, - }, - }, - ]); - }); - - it("should handle startTime filter replacement among other filters", () => { - const columnFilters = [mockTextFilter, mockStartTimeFilter, mockSingleSelectFilter]; - - const result = replaceDateRangeColumnFilter({ - columnFilters, - newStartDate, - newEndDate, - }); - - expect(result).toEqual([ - mockTextFilter, // Unchanged - { - id: "startTime", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: newStartDate, - endDate: newEndDate, - preset: "thisMonth", - }, - }, - }, - mockSingleSelectFilter, // Unchanged - ]); - }); - - it("should maintain filter structure when replacing", () => { - const result = replaceDateRangeColumnFilter({ - columnFilters: [mockStartTimeFilter], - newStartDate, - newEndDate, - }); - - expect(result?.[0]).toHaveProperty("id"); - expect(result?.[0]).toHaveProperty("value"); - expect(result?.[0].value).toHaveProperty("type", ColumnFilterType.DATE_RANGE); - expect(result?.[0].value).toHaveProperty("data"); - expect(result?.[0].value.data).toHaveProperty("startDate"); - expect(result?.[0].value.data).toHaveProperty("endDate"); - expect(result?.[0].value.data).toHaveProperty("preset"); - }); - }); - - describe("edge cases", () => { - it("should return undefined when no column filters provided", () => { - const result = replaceDateRangeColumnFilter({ - columnFilters: undefined, - newStartDate, - newEndDate, - }); - - expect(result).toBeUndefined(); - }); - - it("should handle empty column filters array", () => { - const result = replaceDateRangeColumnFilter({ - columnFilters: [], - newStartDate, - newEndDate, - }); - - expect(result).toEqual([]); - }); - - it("should return unchanged filters when no date range filters exist", () => { - const columnFilters = [mockTextFilter, mockSingleSelectFilter]; - - const result = replaceDateRangeColumnFilter({ - columnFilters, - newStartDate, - newEndDate, - }); - - expect(result).toEqual(columnFilters); - }); - - it("should not modify filters with startTime/createdAt id but wrong type", () => { - const invalidStartTimeFilter: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.TEXT, - data: { - operator: "equals" as const, - operand: "some-date", - }, - }, - }; - - const result = replaceDateRangeColumnFilter({ - columnFilters: [invalidStartTimeFilter], - newStartDate, - newEndDate, - }); - - expect(result).toEqual([invalidStartTimeFilter]); // Should remain unchanged - }); - - it("should handle filters with similar but different ids", () => { - const similarIdFilter: ColumnFilter = { - id: "startTimeRange", // Similar but not exact match - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: "2024-01-01T00:00:00.000Z", - endDate: "2024-01-31T23:59:59.999Z", - preset: "thisMonth", - }, - }, - }; - - const result = replaceDateRangeColumnFilter({ - columnFilters: [similarIdFilter], - newStartDate, - newEndDate, - }); - - expect(result).toEqual([similarIdFilter]); // Should remain unchanged - }); - - it("should handle mixed case scenarios", () => { - const invalidStartTimeTextFilter: ColumnFilter = { - id: "startTime", - value: { - type: ColumnFilterType.TEXT, // Wrong type, should be ignored - data: { operator: "equals" as const, operand: "test" }, - }, - }; - - const mixedFilters = [ - mockTextFilter, - invalidStartTimeTextFilter, - mockCreatedAtFilter, // Valid date range filter - mockSingleSelectFilter, - ]; - - const result = replaceDateRangeColumnFilter({ - columnFilters: mixedFilters, - newStartDate, - newEndDate, - }); - - expect(result).toEqual([ - mockTextFilter, // Unchanged - invalidStartTimeTextFilter, // Unchanged (wrong type) - { - id: "createdAt", - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: newStartDate, - endDate: newEndDate, - preset: "lastMonth", // Replaced - }, - }, - }, - mockSingleSelectFilter, // Unchanged - ]); - }); - }); -}); diff --git a/packages/features/insights/lib/bookingUtils.ts b/packages/features/insights/lib/bookingUtils.ts deleted file mode 100644 index 6ddf794af1cdeb..00000000000000 --- a/packages/features/insights/lib/bookingUtils.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ColumnFilterType } from "@calcom/features/data-table/lib/types"; -import { type ColumnFilter } from "@calcom/features/data-table/lib/types"; -import { isDateRangeFilterValue } from "@calcom/features/data-table/lib/utils"; - -export function extractDateRangeFromColumnFilters(columnFilters?: ColumnFilter[]) { - if (!columnFilters) throw new Error("No date range filter found"); - - for (const filter of columnFilters) { - if ((filter.id === "startTime" || filter.id === "createdAt") && isDateRangeFilterValue(filter.value)) { - const dateFilter = filter.value as Extract< - ColumnFilter["value"], - { type: ColumnFilterType.DATE_RANGE } - >; - if (dateFilter.data.startDate && dateFilter.data.endDate) { - return { - startDate: dateFilter.data.startDate, - endDate: dateFilter.data.endDate, - dateTarget: filter.id, - }; - } - } - } - - throw new Error("No date range filter found"); -} - -export function replaceDateRangeColumnFilter({ - columnFilters, - newStartDate, - newEndDate, -}: { - columnFilters?: ColumnFilter[]; - newStartDate: string; - newEndDate: string; -}) { - if (!columnFilters) { - return undefined; - } - - return columnFilters.map((filter) => { - if ((filter.id === "startTime" || filter.id === "createdAt") && isDateRangeFilterValue(filter.value)) { - return { - id: filter.id, - value: { - type: ColumnFilterType.DATE_RANGE, - data: { - startDate: newStartDate, - endDate: newEndDate, - preset: filter.value.data.preset, - }, - }, - } satisfies ColumnFilter; - } else { - return filter; - } - }); -} diff --git a/packages/features/insights/server/hasInsightsPermission.ts b/packages/features/insights/server/hasInsightsPermission.ts new file mode 100644 index 00000000000000..a28c66a5489ce5 --- /dev/null +++ b/packages/features/insights/server/hasInsightsPermission.ts @@ -0,0 +1,39 @@ +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; +import { MembershipRepository } from "@calcom/lib/server/repository/membership"; +import { MembershipRole } from "@calcom/prisma/enums"; + +export async function hasInsightsPermission({ + userId, + organizationId, +}: { + userId: number; + organizationId: number | null | undefined; +}) { + if (organizationId) { + const permissionCheckService = new PermissionCheckService(); + return await permissionCheckService.checkPermission({ + userId, + teamId: organizationId, + permission: "insights.read", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER], // even members can see their own data + }); + } else { + const permissionCheckService = new PermissionCheckService(); + const teamIds = await MembershipRepository.findUserTeamIds({ userId }); + + for (const teamId of teamIds) { + const hasPermission = await permissionCheckService.checkPermission({ + userId, + teamId, + permission: "insights.read", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER], // even members can see their own data + }); + + if (hasPermission) { + return true; + } + } + } + + return false; +} diff --git a/packages/features/insights/server/raw-data.schema.ts b/packages/features/insights/server/raw-data.schema.ts index 309e6c7c01fb41..ba2384ec46b03f 100644 --- a/packages/features/insights/server/raw-data.schema.ts +++ b/packages/features/insights/server/raw-data.schema.ts @@ -103,6 +103,8 @@ export const routedToPerPeriodCsvInputSchema = routingRepositoryBaseInputSchema. export const bookingRepositoryBaseInputSchema = z.object({ scope: z.union([z.literal("user"), z.literal("team"), z.literal("org")]), selectedTeamId: z.number().optional(), + startDate: z.string(), + endDate: z.string(), timeZone: z.string(), columnFilters: z.array(ZColumnFilter).optional(), }); diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts index aa56f0ef00043c..238eca8482c3bd 100644 --- a/packages/features/insights/server/routing-events.ts +++ b/packages/features/insights/server/routing-events.ts @@ -8,7 +8,6 @@ import { isValidRoutingFormFieldType, } from "@calcom/app-store/routing-forms/lib/FieldTypes"; import { zodFields as routingFormFieldsSchema } from "@calcom/app-store/routing-forms/zod"; -import dayjs from "@calcom/dayjs"; import { WEBAPP_URL } from "@calcom/lib/constants"; import type { InsightsRoutingBaseService } from "@calcom/lib/server/service/InsightsRoutingBaseService"; import { readonlyPrisma as prisma } from "@calcom/prisma"; @@ -72,7 +71,6 @@ class RoutingEventsInsights { teamId: { in: teamIds, }, - ...(routingFormId && { id: routingFormId }), } : { userId: userId ?? -1, @@ -121,11 +119,9 @@ class RoutingEventsInsights { static async getRoutingFormPaginatedResponsesForDownload({ headersPromise, dataPromise, - timeZone, }: { headersPromise: ReturnType; dataPromise: ReturnType; - timeZone: string; }) { const [headers, data] = await Promise.all([headersPromise, dataPromise]); @@ -161,31 +157,11 @@ class RoutingEventsInsights { "Response ID": item.id, "Form Name": item.formName, "Submitted At": item.createdAt.toISOString(), - "Submitted At_date": dayjs(item.createdAt).tz(timeZone).format("YYYY-MM-DD"), - "Submitted At_time": dayjs(item.createdAt).tz(timeZone).format("HH:mm:ss"), "Has Booking": item.bookingUid !== null, "Booking Status": item.bookingStatus || "NO_BOOKING", "Booking Created At": item.bookingCreatedAt?.toISOString() || "", - "Booking Created At_date": item.bookingCreatedAt - ? dayjs(item.bookingCreatedAt).tz(timeZone).format("YYYY-MM-DD") - : "", - "Booking Created At_time": item.bookingCreatedAt - ? dayjs(item.bookingCreatedAt).tz(timeZone).format("HH:mm:ss") - : "", "Booking Start Time": item.bookingStartTime?.toISOString() || "", - "Booking Start Time_date": item.bookingStartTime - ? dayjs(item.bookingStartTime).tz(timeZone).format("YYYY-MM-DD") - : "", - "Booking Start Time_time": item.bookingStartTime - ? dayjs(item.bookingStartTime).tz(timeZone).format("HH:mm:ss") - : "", "Booking End Time": item.bookingEndTime?.toISOString() || "", - "Booking End Time_date": item.bookingEndTime - ? dayjs(item.bookingEndTime).tz(timeZone).format("YYYY-MM-DD") - : "", - "Booking End Time_time": item.bookingEndTime - ? dayjs(item.bookingEndTime).tz(timeZone).format("HH:mm:ss") - : "", "Assignment Reason": item.bookingAssignmentReason || "", "Routed To Name": item.bookingUserName || "", "Routed To Email": item.bookingUserEmail || "", diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index a287db00aeff6b..5cddecf9b357cc 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -1,10 +1,6 @@ import { z } from "zod"; import dayjs from "@calcom/dayjs"; -import { - extractDateRangeFromColumnFilters, - replaceDateRangeColumnFilter, -} from "@calcom/features/insights/lib/bookingUtils"; import { insightsRoutingServiceInputSchema, insightsRoutingServicePaginatedInputSchema, @@ -22,6 +18,7 @@ import { router } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; +import { hasInsightsPermission } from "./hasInsightsPermission"; import { getTimeView, getDateRanges, type GetDateRangesParams } from "./insightsDateUtils"; import { RoutingEventsInsights } from "./routing-events"; import { VirtualQueuesInsights } from "./virtual-queues"; @@ -239,6 +236,21 @@ const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, getRa }); }); +const insightsPbacProcedure = userBelongsToTeamProcedure.use(async ({ ctx, next }) => { + const hasPermission = await hasInsightsPermission({ + userId: ctx.user.id, + organizationId: ctx.user.organizationId, + }); + + if (!hasPermission) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return next({ + ctx, + }); +}); + const userSelect = { id: true, name: true, @@ -301,18 +313,24 @@ export interface IResultTeamList { */ function createInsightsBookingService( ctx: { user: { id: number; organizationId: number | null } }, - input: z.infer + input: z.infer, + dateTarget: "createdAt" | "startTime" = "createdAt" ) { - const { scope, selectedTeamId, columnFilters } = input; + const { scope, selectedTeamId, startDate, endDate, columnFilters } = input; return getInsightsBookingService({ options: { scope, userId: ctx.user.id, - orgId: ctx.user.organizationId, + orgId: ctx.user.organizationId ?? 0, ...(selectedTeamId && { teamId: selectedTeamId }), }, filters: { ...(columnFilters && { columnFilters }), + dateRange: { + target: dateTarget, + startDate, + endDate, + }, }, }); } @@ -337,7 +355,7 @@ function createInsightsRoutingService( } export const insightsRouter = router({ - bookingKPIStats: userBelongsToTeamProcedure + bookingKPIStats: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { const currentPeriodService = createInsightsBookingService(ctx, input); @@ -347,15 +365,10 @@ export const insightsRouter = router({ // Calculate previous period dates and create service for previous period const previousPeriodDates = currentPeriodService.calculatePreviousPeriodDates(); - const previousPeriodColumnFilters = replaceDateRangeColumnFilter({ - columnFilters: input.columnFilters, - newStartDate: previousPeriodDates.startDate, - newEndDate: previousPeriodDates.endDate, - }); - const previousPeriodService = createInsightsBookingService(ctx, { ...input, - columnFilters: previousPeriodColumnFilters, + startDate: previousPeriodDates.startDate, + endDate: previousPeriodDates.endDate, }); // Get previous period stats @@ -442,33 +455,30 @@ export const insightsRouter = router({ }, }; }), - eventTrends: userBelongsToTeamProcedure - .input(bookingRepositoryBaseInputSchema) - .query(async ({ ctx, input }) => { - const { columnFilters, timeZone } = input; - const { startDate, endDate } = extractDateRangeFromColumnFilters(columnFilters); + eventTrends: insightsPbacProcedure.input(bookingRepositoryBaseInputSchema).query(async ({ ctx, input }) => { + const { startDate, endDate, timeZone } = input; + + // Calculate timeView and dateRanges + const timeView = getTimeView(startDate, endDate); + const dateRanges = getDateRanges({ + startDate, + endDate, + timeView, + timeZone, + weekStart: ctx.user.weekStart, + }); - // Calculate timeView and dateRanges - const timeView = getTimeView(startDate, endDate); - const dateRanges = getDateRanges({ - startDate, - endDate, - timeView, + const insightsBookingService = createInsightsBookingService(ctx, input); + try { + return await insightsBookingService.getEventTrendsStats({ timeZone, - weekStart: ctx.user.weekStart, + dateRanges, }); - - const insightsBookingService = createInsightsBookingService(ctx, input); - try { - return await insightsBookingService.getEventTrendsStats({ - timeZone, - dateRanges, - }); - } catch (e) { - throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); - } - }), - popularEvents: userBelongsToTeamProcedure + } catch (e) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } + }), + popularEvents: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ input, ctx }) => { const insightsBookingService = createInsightsBookingService(ctx, input); @@ -479,11 +489,10 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - averageEventDuration: userBelongsToTeamProcedure + averageEventDuration: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { - const { columnFilters, timeZone } = input; - const { startDate, endDate, dateTarget } = extractDateRangeFromColumnFilters(columnFilters); + const { startDate, endDate, timeZone } = input; const insightsBookingService = createInsightsBookingService(ctx, input); @@ -507,7 +516,6 @@ export const insightsRouter = router({ select: { eventLength: true, createdAt: true, - startTime: true, }, }); @@ -519,9 +527,7 @@ export const insightsRouter = router({ } for (const booking of allBookings) { - const periodStart = dayjs(dateTarget === "startTime" ? booking.startTime : booking.createdAt) - .startOf(startOfEndOf) - .format("ll"); + const periodStart = dayjs(booking.createdAt).startOf(startOfEndOf).format("ll"); if (resultMap.has(periodStart)) { const current = resultMap.get(periodStart); if (!current) continue; @@ -540,68 +546,57 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - membersWithMostCancelledBookings: userBelongsToTeamProcedure + membersWithMostCancelledBookings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ input, ctx }) => { const insightsBookingService = createInsightsBookingService(ctx, input); try { - return await insightsBookingService.getMembersStatsWithCount({ - type: "cancelled", - sortOrder: "DESC", - }); + return await insightsBookingService.getMembersStatsWithCount("cancelled", "DESC"); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - membersWithMostCompletedBookings: userBelongsToTeamProcedure + membersWithMostCompletedBookings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ input, ctx }) => { - const insightsBookingService = createInsightsBookingService(ctx, input); + const insightsBookingService = createInsightsBookingService(ctx, input, "startTime"); try { - return await insightsBookingService.getMembersStatsWithCount({ - type: "accepted", - sortOrder: "DESC", - completed: true, - }); + return await insightsBookingService.getMembersStatsWithCount("accepted", "DESC"); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - membersWithLeastCompletedBookings: userBelongsToTeamProcedure + membersWithLeastCompletedBookings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ input, ctx }) => { - const insightsBookingService = createInsightsBookingService(ctx, input); + const insightsBookingService = createInsightsBookingService(ctx, input, "startTime"); try { - return await insightsBookingService.getMembersStatsWithCount({ - type: "accepted", - sortOrder: "ASC", - completed: true, - }); + return await insightsBookingService.getMembersStatsWithCount("accepted", "ASC"); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - membersWithMostBookings: userBelongsToTeamProcedure + membersWithMostBookings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ input, ctx }) => { const insightsBookingService = createInsightsBookingService(ctx, input); try { - return await insightsBookingService.getMembersStatsWithCount({ type: "all", sortOrder: "DESC" }); + return await insightsBookingService.getMembersStatsWithCount("all", "DESC"); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - membersWithLeastBookings: userBelongsToTeamProcedure + membersWithLeastBookings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ input, ctx }) => { const insightsBookingService = createInsightsBookingService(ctx, input); try { - return await insightsBookingService.getMembersStatsWithCount({ type: "all", sortOrder: "ASC" }); + return await insightsBookingService.getMembersStatsWithCount("all", "ASC"); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } @@ -759,7 +754,7 @@ export const insightsRouter = router({ return usersInTeam.map((membership) => membership.user); }), - eventTypeList: userBelongsToTeamProcedure + eventTypeList: insightsPbacProcedure .input( z.object({ teamId: z.coerce.number().nullish(), @@ -785,36 +780,36 @@ export const insightsRouter = router({ return eventTypeList; }), - recentRatings: userBelongsToTeamProcedure + recentRatings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { const insightsBookingService = createInsightsBookingService(ctx, input); return await insightsBookingService.getRecentRatingsStats(); }), - membersWithMostNoShow: userBelongsToTeamProcedure + membersWithMostNoShow: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ input, ctx }) => { const insightsBookingService = createInsightsBookingService(ctx, input); try { - return await insightsBookingService.getMembersStatsWithCount({ type: "noShow", sortOrder: "DESC" }); + return await insightsBookingService.getMembersStatsWithCount("noShow", "DESC"); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - membersWithHighestRatings: userBelongsToTeamProcedure + membersWithHighestRatings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { const insightsBookingService = createInsightsBookingService(ctx, input); return await insightsBookingService.getMembersRatingStats("DESC"); }), - membersWithLowestRatings: userBelongsToTeamProcedure + membersWithLowestRatings: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { const insightsBookingService = createInsightsBookingService(ctx, input); return await insightsBookingService.getMembersRatingStats("ASC"); }), - rawData: userBelongsToTeamProcedure + rawData: insightsPbacProcedure .input( bookingRepositoryBaseInputSchema.extend({ limit: z.number().max(100).optional(), @@ -822,7 +817,7 @@ export const insightsRouter = router({ }) ) .query(async ({ ctx, input }) => { - const { limit, offset, timeZone } = input; + const { limit, offset } = input; const insightsBookingService = createInsightsBookingService(ctx, input); @@ -830,14 +825,13 @@ export const insightsRouter = router({ return await insightsBookingService.getCsvData({ limit: limit ?? 100, offset: offset ?? 0, - timeZone, }); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - getRoutingFormsForFilters: userBelongsToTeamProcedure + getRoutingFormsForFilters: insightsPbacProcedure .input(z.object({ userId: z.number().optional(), teamId: z.number().optional(), isAll: z.boolean() })) .query(async ({ ctx, input }) => { const { teamId, isAll } = input; @@ -848,13 +842,13 @@ export const insightsRouter = router({ organizationId: ctx.user.organizationId ?? undefined, }); }), - routingFormsByStatus: userBelongsToTeamProcedure + routingFormsByStatus: insightsPbacProcedure .input(insightsRoutingServiceInputSchema) .query(async ({ ctx, input }) => { const insightsRoutingService = createInsightsRoutingService(ctx, input); return await insightsRoutingService.getRoutingFormStats(); }), - routingFormResponses: userBelongsToTeamProcedure + routingFormResponses: insightsPbacProcedure .input(insightsRoutingServicePaginatedInputSchema) .query(async ({ ctx, input }) => { const insightsRoutingService = createInsightsRoutingService(ctx, input); @@ -864,7 +858,7 @@ export const insightsRouter = router({ offset: input.offset, }); }), - routingFormResponsesForDownload: userBelongsToTeamProcedure + routingFormResponsesForDownload: insightsPbacProcedure .input(insightsRoutingServicePaginatedInputSchema) .query(async ({ ctx, input }) => { const headersPromise = RoutingEventsInsights.getRoutingFormHeaders({ @@ -887,10 +881,9 @@ export const insightsRouter = router({ return await RoutingEventsInsights.getRoutingFormPaginatedResponsesForDownload({ headersPromise, dataPromise, - timeZone: ctx.user.timeZone, }); }), - getRoutingFormFieldOptions: userBelongsToTeamProcedure + getRoutingFormFieldOptions: insightsPbacProcedure .input( z.object({ userId: z.number().optional(), @@ -907,7 +900,7 @@ export const insightsRouter = router({ }); return options; }), - failedBookingsByField: userBelongsToTeamProcedure + failedBookingsByField: insightsPbacProcedure .input(insightsRoutingServiceInputSchema) .query(async ({ ctx, input }) => { const insightsRoutingService = createInsightsRoutingService(ctx, input); @@ -917,7 +910,7 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - routingFormResponsesHeaders: userBelongsToTeamProcedure + routingFormResponsesHeaders: insightsPbacProcedure .input( z.object({ userId: z.number().optional(), @@ -937,7 +930,7 @@ export const insightsRouter = router({ return headers || []; }), - routedToPerPeriod: userBelongsToTeamProcedure + routedToPerPeriod: insightsPbacProcedure .input(routedToPerPeriodInputSchema) .query(async ({ ctx, input }) => { const { period, limit, searchQuery, ...rest } = input; @@ -953,7 +946,7 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - routedToPerPeriodCsv: userBelongsToTeamProcedure + routedToPerPeriodCsv: insightsPbacProcedure .input(routedToPerPeriodCsvInputSchema) .query(async ({ ctx, input }) => { const { period, searchQuery, ...rest } = input; @@ -986,7 +979,7 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - getRoutingFunnelData: userBelongsToTeamProcedure + getRoutingFunnelData: insightsPbacProcedure .input(routingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { const timeView = getTimeView(input.startDate, input.endDate); @@ -1004,11 +997,11 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - bookingsByHourStats: userBelongsToTeamProcedure + bookingsByHourStats: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { const { timeZone } = input; - const insightsBookingService = createInsightsBookingService(ctx, input); + const insightsBookingService = createInsightsBookingService(ctx, input, "startTime"); try { return await insightsBookingService.getBookingsByHourStats({ @@ -1018,10 +1011,10 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), - recentNoShowGuests: userBelongsToTeamProcedure + recentNoShowGuests: insightsPbacProcedure .input(bookingRepositoryBaseInputSchema) .query(async ({ ctx, input }) => { - const insightsBookingService = createInsightsBookingService(ctx, input); + const insightsBookingService = createInsightsBookingService(ctx, input, "startTime"); try { return await insightsBookingService.getRecentNoShowGuests(); diff --git a/packages/features/instant-meeting/handleInstantMeeting.test.ts b/packages/features/instant-meeting/handleInstantMeeting.test.ts index 74909bd02b0b1c..1cdccb34145a79 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.test.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.test.ts @@ -20,7 +20,7 @@ vi.mock("@calcom/features/notifications/sendNotification", () => ({ sendNotification: vi.fn(), })); -vi.mock("@calcom/app-store/videoClient", () => ({ +vi.mock("@calcom/lib/videoClient", () => ({ createInstantMeetingWithCalVideo: vi.fn().mockResolvedValue({ type: "daily_video", id: "MOCK_INSTANT_MEETING_ID", diff --git a/packages/features/instant-meeting/handleInstantMeeting.ts b/packages/features/instant-meeting/handleInstantMeeting.ts index 557216612d2a1a..c5fccbfe4ae1f5 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.ts @@ -22,7 +22,7 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { createInstantMeetingWithCalVideo } from "@calcom/app-store/videoClient"; +import { createInstantMeetingWithCalVideo } from "@calcom/lib/videoClient"; import prisma from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums"; diff --git a/packages/features/pbac/PBAC_REFACTORING_GUIDE.md b/packages/features/pbac/PBAC_REFACTORING_GUIDE.md deleted file mode 100644 index dba73fc0c1f90f..00000000000000 --- a/packages/features/pbac/PBAC_REFACTORING_GUIDE.md +++ /dev/null @@ -1,86 +0,0 @@ -# PBAC Refactoring Guide - -Quick guide for refactoring Cal.com handlers to use Permission-Based Access Control (PBAC) instead of membership-based queries. - -## Core Pattern: Team Filtering with PBAC - -**Before (Membership-based)**: -```typescript -const teamsToQuery = ( - await prisma.membership.findMany({ - where: { - userId: ctx.user.id, - accepted: true, - NOT: [ - { - role: MembershipRole.MEMBER, - team: { isPrivate: true }, - }, - ], - }, - select: { teamId: true }, - }) -).map((membership) => membership.teamId); -``` - -**After (PBAC-based)**: -```typescript -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; - -const permissionCheckService = new PermissionCheckService(); -const teamsToQuery = await permissionCheckService.getTeamIdsWithPermission( - ctx.user.id, - "team.listMembers" -); -``` - -## Key Points - -### 1. Use Direct Permission Strings -- Use `"team.listMembers"` directly instead of `PermissionMapper.toPermissionString()` -- Simpler and more readable - -### 2. No Fallback Logic Needed -- `getTeamIdsWithPermission()` handles errors internally -- Returns empty array `[]` when user has no permissions (this is legitimate) -- Don't assume empty array means PBAC failure - -### 3. Common Team Permissions -- `"team.listMembers"` - List team members -- `"team.read"` - View team details -- `"team.update"` - Edit team settings -- `"team.invite"` - Invite members -- `"team.remove"` - Remove members - -## Step-by-Step Refactoring - -1. **Add import**: - ```typescript - import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; - ``` - -2. **Replace membership query**: - ```typescript - const permissionCheckService = new PermissionCheckService(); - const teamsToQuery = await permissionCheckService.getTeamIdsWithPermission( - ctx.user.id, - "team.listMembers" - ); - ``` - -3. **Keep existing privacy checks** - PBAC doesn't replace organization-level privacy logic - -## Example: listSimpleMembers.handler.ts - -**What changed**: -- Replaced 17 lines of membership query with 2 lines of PBAC -- Removed unnecessary imports and fallback logic -- Used direct permission string `"team.listMembers"` - -**Result**: Cleaner, more maintainable code that respects fine-grained permissions. - -## Verification - -- Run `yarn type-check:ci --force` -- Ensure existing privacy checks remain intact -- Test with users who have different permission levels diff --git a/packages/features/pbac/client/components/WorkflowTabPermissionGuard.tsx b/packages/features/pbac/client/components/WorkflowTabPermissionGuard.tsx deleted file mode 100644 index 3eec868a93cc28..00000000000000 --- a/packages/features/pbac/client/components/WorkflowTabPermissionGuard.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; - -import { useWorkflowPermission } from "../hooks/useEventPermission"; - -interface WorkflowTabPermissionGuardProps { - children: ReactNode; - fallback?: ReactNode; -} - -export const WorkflowTabPermissionGuard = ({ - children, - fallback = null, -}: WorkflowTabPermissionGuardProps) => { - const { hasPermission } = useWorkflowPermission("workflow.read"); - - if (!hasPermission) { - return <>{fallback}; - } - - return <>{children}; -}; diff --git a/packages/features/pbac/client/context/EventPermissionContext.tsx b/packages/features/pbac/client/context/EventPermissionContext.tsx deleted file mode 100644 index c63725a9723d39..00000000000000 --- a/packages/features/pbac/client/context/EventPermissionContext.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client"; - -import { createContext, useContext } from "react"; -import { createStore, useStore } from "zustand"; - -export interface EventPermissions { - eventTypes: { - canRead: boolean; - canCreate: boolean; - canUpdate: boolean; - canDelete: boolean; - }; - workflows: { - canRead: boolean; - canCreate: boolean; - canUpdate: boolean; - canDelete: boolean; - }; -} - -interface EventPermissionStore { - permissions: EventPermissions; - setPermissions: (permissions: EventPermissions) => void; - hasEventTypePermission: (permission: keyof EventPermissions["eventTypes"]) => boolean; - hasWorkflowPermission: (permission: keyof EventPermissions["workflows"]) => boolean; -} - -const createEventPermissionStore = ( - initialPermissions: EventPermissions = { - eventTypes: { - canRead: false, - canCreate: false, - canUpdate: false, - canDelete: false, - }, - workflows: { - canRead: false, - canCreate: false, - canUpdate: false, - canDelete: false, - }, - } -) => - createStore()((set, get) => ({ - permissions: initialPermissions, - setPermissions: (permissions) => set({ permissions }), - hasEventTypePermission: (permission) => { - const { permissions } = get(); - return permissions.eventTypes[permission]; - }, - hasWorkflowPermission: (permission) => { - const { permissions } = get(); - return permissions.workflows[permission]; - }, - })); - -type EventPermissionStoreApi = ReturnType; - -const EventPermissionContext = createContext(undefined); - -interface EventPermissionProviderProps { - children: React.ReactNode; - initialPermissions: EventPermissions; -} - -export const EventPermissionProvider = ({ children, initialPermissions }: EventPermissionProviderProps) => { - const store = createEventPermissionStore(initialPermissions); - - return {children}; -}; - -export const useEventPermissionStore = (selector: (store: EventPermissionStore) => T): T => { - const eventPermissionStoreContext = useContext(EventPermissionContext); - - if (!eventPermissionStoreContext) { - throw new Error("useEventPermissionStore must be used within EventPermissionProvider"); - } - - return useStore(eventPermissionStoreContext, selector); -}; diff --git a/packages/features/pbac/client/hooks/useEventPermission.ts b/packages/features/pbac/client/hooks/useEventPermission.ts deleted file mode 100644 index a4529ac02bac9c..00000000000000 --- a/packages/features/pbac/client/hooks/useEventPermission.ts +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import type { EventPermissions } from "../context/EventPermissionContext"; -import { useEventPermissionStore } from "../context/EventPermissionContext"; - -export const useEventTypePermission = (permission: keyof EventPermissions["eventTypes"]) => { - return useEventPermissionStore((state) => ({ - hasPermission: state.hasEventTypePermission(permission), - permissions: state.permissions.eventTypes, - })); -}; - -export const useWorkflowPermission = (permission: keyof EventPermissions["workflows"]) => { - return useEventPermissionStore((state) => ({ - hasPermission: state.hasWorkflowPermission(permission), - permissions: state.permissions.workflows, - })); -}; - -export const useAllEventPermissions = () => { - return useEventPermissionStore((state) => state.permissions); -}; - -// Convenience hooks for common permissions -export const useCanCreateEventTypes = () => { - return useEventPermissionStore((state) => state.permissions.eventTypes.canCreate); -}; - -export const useCanUpdateEventTypes = () => { - return useEventPermissionStore((state) => state.permissions.eventTypes.canUpdate); -}; - -export const useCanDeleteEventTypes = () => { - return useEventPermissionStore((state) => state.permissions.eventTypes.canDelete); -}; - -export const useCanReadWorkflows = () => { - return useEventPermissionStore((state) => state.permissions.workflows.canRead); -}; - -export const useCanCreateWorkflows = () => { - return useEventPermissionStore((state) => state.permissions.workflows.canCreate); -}; - -export const useCanUpdateWorkflows = () => { - return useEventPermissionStore((state) => state.permissions.workflows.canUpdate); -}; - -export const useCanDeleteWorkflows = () => { - return useEventPermissionStore((state) => state.permissions.workflows.canDelete); -}; diff --git a/packages/features/pbac/domain/repositories/IPermissionRepository.ts b/packages/features/pbac/domain/repositories/IPermissionRepository.ts index 7c32bf944fb590..0ecc6f7c0d8535 100644 --- a/packages/features/pbac/domain/repositories/IPermissionRepository.ts +++ b/packages/features/pbac/domain/repositories/IPermissionRepository.ts @@ -37,11 +37,6 @@ export interface IPermissionRepository { customRoleId: string | null; } | null>; - getTeamById(teamId: number): Promise<{ - id: number; - parentId: number | null; - } | null>; - checkRolePermission(roleId: string, permission: PermissionString): Promise; checkRolePermissions(roleId: string, permissions: PermissionString[]): Promise; diff --git a/packages/features/pbac/domain/repositories/IRoleRepository.ts b/packages/features/pbac/domain/repositories/IRoleRepository.ts index 4a82e3eb90caa7..a52e5f08316684 100644 --- a/packages/features/pbac/domain/repositories/IRoleRepository.ts +++ b/packages/features/pbac/domain/repositories/IRoleRepository.ts @@ -20,5 +20,4 @@ export interface IRoleRepository { } ): Promise; getPermissions(roleId: string): Promise; - reassignUsersToRole(fromRoleId: string, toRoleId: string): Promise; } diff --git a/packages/features/pbac/domain/types/permission-registry.ts b/packages/features/pbac/domain/types/permission-registry.ts index 3cfbc54d9dfe01..89850633c08610 100644 --- a/packages/features/pbac/domain/types/permission-registry.ts +++ b/packages/features/pbac/domain/types/permission-registry.ts @@ -9,7 +9,6 @@ export enum Resource { Role = "role", RoutingForm = "routingForm", Workflow = "workflow", - Webhook = "webhook", } export enum CrudAction { @@ -265,7 +264,7 @@ export const PERMISSION_REGISTRY: PermissionRegistry = { category: "team", i18nKey: "pbac_action_invite", descriptionI18nKey: "pbac_desc_invite_team_members", - dependsOn: ["team.read", "team.listMembers", "role.read"], + dependsOn: ["team.read", "team.listMembers"], }, [CustomAction.Remove]: { description: "Remove team members", @@ -285,7 +284,7 @@ export const PERMISSION_REGISTRY: PermissionRegistry = { category: "team", i18nKey: "pbac_action_change_member_role", descriptionI18nKey: "pbac_desc_change_team_member_role", - dependsOn: ["team.read", "team.listMembers", "role.read"], + dependsOn: ["team.read", "team.listMembers"], }, [CustomAction.Impersonate]: { description: "Impersonate team members", @@ -294,13 +293,6 @@ export const PERMISSION_REGISTRY: PermissionRegistry = { descriptionI18nKey: "pbac_desc_impersonate_team_members", dependsOn: ["team.read", "team.listMembers"], }, - [CustomAction.ManageBilling]: { - description: "Manage billing", - category: "team", - i18nKey: "pbac_action_manage_billing", - descriptionI18nKey: "pbac_desc_manage_billing", - scope: [], // Empty scope because this permission is only used for TEAM billing outside of an org. (We dont want to show this in the UI) - }, }, [Resource.Organization]: { _resource: { @@ -524,36 +516,4 @@ export const PERMISSION_REGISTRY: PermissionRegistry = { dependsOn: ["routingForm.read"], }, }, - [Resource.Webhook]: { - _resource: { - i18nKey: "pbac_resource_webhook", - }, - [CrudAction.Create]: { - description: "Create webhooks", - category: "webhook", - i18nKey: "pbac_action_create", - descriptionI18nKey: "pbac_desc_create_webhooks", - dependsOn: ["webhook.read"], - }, - [CrudAction.Read]: { - description: "View webhooks", - category: "webhook", - i18nKey: "pbac_action_read", - descriptionI18nKey: "pbac_desc_view_webhooks", - }, - [CrudAction.Update]: { - description: "Update webhooks", - category: "webhook", - i18nKey: "pbac_action_update", - descriptionI18nKey: "pbac_desc_update_webhooks", - dependsOn: ["webhook.read"], - }, - [CrudAction.Delete]: { - description: "Delete webhooks", - category: "webhook", - i18nKey: "pbac_action_delete", - descriptionI18nKey: "pbac_desc_delete_webhooks", - dependsOn: ["webhook.read"], - }, - }, }; diff --git a/packages/features/pbac/infrastructure/repositories/PermissionRepository.ts b/packages/features/pbac/infrastructure/repositories/PermissionRepository.ts index 425d38bd4f0609..5fe5435131f62f 100644 --- a/packages/features/pbac/infrastructure/repositories/PermissionRepository.ts +++ b/packages/features/pbac/infrastructure/repositories/PermissionRepository.ts @@ -94,16 +94,6 @@ export class PermissionRepository implements IPermissionRepository { }); } - async getTeamById(teamId: number) { - return this.client.team.findUnique({ - where: { id: teamId }, - select: { - id: true, - parentId: true, - }, - }); - } - async checkRolePermission(roleId: string, permission: PermissionString): Promise { const { resource, action } = parsePermissionString(permission); const hasPermission = await this.client.rolePermission.findFirst({ diff --git a/packages/features/pbac/infrastructure/repositories/RoleRepository.ts b/packages/features/pbac/infrastructure/repositories/RoleRepository.ts index 1e35d3e541be34..280fd4014adbbe 100644 --- a/packages/features/pbac/infrastructure/repositories/RoleRepository.ts +++ b/packages/features/pbac/infrastructure/repositories/RoleRepository.ts @@ -82,13 +82,6 @@ export class RoleRepository { ]); } - async reassignUsersToRole(fromRoleId: string, toRoleId: string): Promise { - await this.client.membership.updateMany({ - where: { customRoleId: fromRoleId }, - data: { customRoleId: toRoleId }, - }); - } - async update( roleId: string, permissionChanges: { diff --git a/packages/features/pbac/lib/constants.ts b/packages/features/pbac/lib/constants.ts index f84a44ec359964..9d6ca73c120902 100644 --- a/packages/features/pbac/lib/constants.ts +++ b/packages/features/pbac/lib/constants.ts @@ -1,22 +1,13 @@ import { MembershipRole } from "@calcom/prisma/enums"; -/** - * Enum for default role IDs - */ -export enum DefaultPBACRole { - OWNER_ROLE = "owner_role", - ADMIN_ROLE = "admin_role", - MEMBER_ROLE = "member_role", -} - /** * Default role IDs used in the PBAC system * These IDs match the ones created in the migration */ export const DEFAULT_ROLE_IDS = { - [MembershipRole.OWNER]: DefaultPBACRole.OWNER_ROLE, - [MembershipRole.ADMIN]: DefaultPBACRole.ADMIN_ROLE, - [MembershipRole.MEMBER]: DefaultPBACRole.MEMBER_ROLE, + [MembershipRole.OWNER]: "owner_role", + [MembershipRole.ADMIN]: "admin_role", + [MembershipRole.MEMBER]: "member_role", } as const; /** diff --git a/packages/features/pbac/services/__tests__/permission-check.service.test.ts b/packages/features/pbac/services/__tests__/permission-check.service.test.ts index 64bdc5cce579b7..8b1dc954ee4a5c 100644 --- a/packages/features/pbac/services/__tests__/permission-check.service.test.ts +++ b/packages/features/pbac/services/__tests__/permission-check.service.test.ts @@ -37,7 +37,6 @@ describe("PermissionCheckService", () => { getMembershipByMembershipId: vi.fn(), getMembershipByUserAndTeam: vi.fn(), getOrgMembership: vi.fn(), - getTeamById: vi.fn(), getUserMemberships: vi.fn(), checkRolePermission: vi.fn(), checkRolePermissions: vi.fn(), @@ -68,14 +67,28 @@ describe("PermissionCheckService", () => { describe("checkPermission", () => { it("should check permission with PBAC enabled", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({ + const membership = { id: 1, teamId: 1, userId: 1, + accepted: true, + role: "ADMIN" as MembershipRole, customRoleId: "admin_role", + disableImpersonation: false, + createdAt: new Date(), + updatedAt: new Date(), + }; + + (MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(membership); + mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); + mockRepository.getMembershipByMembershipId.mockResolvedValueOnce({ + id: membership.id, + teamId: membership.teamId, + userId: membership.userId, + customRoleId: membership.customRoleId, team: { parentId: null }, }); + mockRepository.getOrgMembership.mockResolvedValueOnce(null); mockRepository.checkRolePermission.mockResolvedValueOnce(true); const result = await service.checkPermission({ @@ -86,8 +99,12 @@ describe("PermissionCheckService", () => { }); expect(result).toBe(true); + expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenCalledWith({ + userId: 1, + teamId: 1, + }); expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac"); - expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1); + expect(mockRepository.getMembershipByMembershipId).toHaveBeenCalledWith(1); expect(mockRepository.checkRolePermission).toHaveBeenCalledWith("admin_role", "eventType.create"); }); @@ -123,8 +140,7 @@ describe("PermissionCheckService", () => { expect(mockRepository.checkRolePermission).not.toHaveBeenCalled(); }); - it("should return false if membership not found when PBAC disabled", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false); + it("should return false if membership not found", async () => { (MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(null); const result = await service.checkPermission({ @@ -137,47 +153,21 @@ describe("PermissionCheckService", () => { expect(result).toBe(false); }); - it("should check org-level permissions when user has no team membership but PBAC is enabled", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce(null); - mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 2 }); - mockRepository.getOrgMembership.mockResolvedValueOnce({ - id: 100, - teamId: 2, - userId: 1, - customRoleId: "org_admin_role", - }); - mockRepository.checkRolePermission.mockResolvedValueOnce(true); - - const result = await service.checkPermission({ - userId: 1, - teamId: 1, - permission: "eventType.create", - fallbackRoles: ["ADMIN", "OWNER"], - }); - - expect(result).toBe(true); - expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1); - expect(mockRepository.getTeamById).toHaveBeenCalledWith(1); - expect(mockRepository.getOrgMembership).toHaveBeenCalledWith(1, 2); - expect(mockRepository.checkRolePermission).toHaveBeenCalledWith("org_admin_role", "eventType.create"); - }); - - it("should return false if PBAC enabled but no customRoleId on team or org", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({ + it("should return false if PBAC enabled but no customRoleId", async () => { + const membership = { id: 1, teamId: 1, userId: 1, + accepted: true, + role: "ADMIN" as MembershipRole, customRoleId: null, - team: { parentId: 2 }, - }); - mockRepository.getOrgMembership.mockResolvedValueOnce({ - id: 100, - teamId: 2, - userId: 1, - customRoleId: null, - }); + disableImpersonation: false, + createdAt: new Date(), + updatedAt: new Date(), + }; + + (MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(membership); + mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); const result = await service.checkPermission({ userId: 1, @@ -192,14 +182,28 @@ describe("PermissionCheckService", () => { describe("checkPermissions", () => { it("should check multiple permissions with PBAC enabled", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({ + const membership = { id: 1, teamId: 1, userId: 1, + accepted: true, + role: "ADMIN" as MembershipRole, customRoleId: "admin_role", + disableImpersonation: false, + createdAt: new Date(), + updatedAt: new Date(), + }; + + (MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(membership); + mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); + mockRepository.getMembershipByMembershipId.mockResolvedValueOnce({ + id: membership.id, + teamId: membership.teamId, + userId: membership.userId, + customRoleId: membership.customRoleId, team: { parentId: null }, }); + mockRepository.getOrgMembership.mockResolvedValueOnce(null); mockRepository.checkRolePermissions.mockResolvedValueOnce(true); const result = await service.checkPermissions({ @@ -210,8 +214,12 @@ describe("PermissionCheckService", () => { }); expect(result).toBe(true); + expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenCalledWith({ + userId: 1, + teamId: 1, + }); expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac"); - expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1); + expect(mockRepository.getMembershipByMembershipId).toHaveBeenCalledWith(1); expect(mockRepository.checkRolePermissions).toHaveBeenCalledWith("admin_role", [ "eventType.create", "team.invite", @@ -251,12 +259,25 @@ describe("PermissionCheckService", () => { }); it("should return false when permissions array is empty", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({ + const membership = { id: 1, teamId: 1, userId: 1, + accepted: true, + role: "MEMBER" as MembershipRole, // Change to MEMBER so fallback also fails customRoleId: "admin_role", + disableImpersonation: false, + createdAt: new Date(), + updatedAt: new Date(), + }; + + (MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(membership); + mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); + mockRepository.getMembershipByMembershipId.mockResolvedValueOnce({ + id: membership.id, + teamId: membership.teamId, + userId: membership.userId, + customRoleId: membership.customRoleId, team: { parentId: null }, }); mockRepository.getOrgMembership.mockResolvedValueOnce(null); @@ -272,35 +293,6 @@ describe("PermissionCheckService", () => { expect(result).toBe(false); expect(mockRepository.checkRolePermissions).toHaveBeenCalledWith("admin_role", []); }); - - it("should check org-level permissions when user has no team membership with checkPermissions", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce(null); - mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 2 }); - mockRepository.getOrgMembership.mockResolvedValueOnce({ - id: 100, - teamId: 2, - userId: 1, - customRoleId: "org_admin_role", - }); - mockRepository.checkRolePermissions.mockResolvedValueOnce(true); - - const result = await service.checkPermissions({ - userId: 1, - teamId: 1, - permissions: ["eventType.create", "team.invite"], - fallbackRoles: ["ADMIN", "OWNER"], - }); - - expect(result).toBe(true); - expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1); - expect(mockRepository.getTeamById).toHaveBeenCalledWith(1); - expect(mockRepository.getOrgMembership).toHaveBeenCalledWith(1, 2); - expect(mockRepository.checkRolePermissions).toHaveBeenCalledWith("org_admin_role", [ - "eventType.create", - "team.invite", - ]); - }); }); describe("getUserPermissions", () => { @@ -400,33 +392,6 @@ describe("PermissionCheckService", () => { }); describe("getResourcePermissions", () => { - it("should return org permissions when user has no team membership but has org membership", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce(null); - mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 2 }); - mockRepository.getOrgMembership.mockResolvedValueOnce({ - id: 100, - teamId: 2, - userId: 1, - customRoleId: "org_role", - }); - mockRepository.getResourcePermissionsByRoleId.mockResolvedValueOnce(["create", "read", "update"]); - - const result = await service.getResourcePermissions({ - userId: 1, - teamId: 1, - resource: Resource.EventType, - }); - - expect(result).toEqual(["eventType.create", "eventType.read", "eventType.update"]); - expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1); - expect(mockRepository.getTeamById).toHaveBeenCalledWith(1); - expect(mockRepository.getOrgMembership).toHaveBeenCalledWith(1, 2); - expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledWith( - "org_role", - Resource.EventType - ); - }); it("should return empty array when PBAC is disabled", async () => { mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false); diff --git a/packages/features/pbac/services/__tests__/role.service.test.ts b/packages/features/pbac/services/__tests__/role.service.test.ts index 29ead3b38c9c2b..712b3bf2764f7c 100644 --- a/packages/features/pbac/services/__tests__/role.service.test.ts +++ b/packages/features/pbac/services/__tests__/role.service.test.ts @@ -39,7 +39,6 @@ describe("RoleService", () => { update: vi.fn(), roleBelongsToTeam: vi.fn(), getPermissions: vi.fn(), - reassignUsersToRole: vi.fn(), }; mockPermissionDiffService = { @@ -301,7 +300,7 @@ describe("RoleService", () => { }); describe("deleteRole", () => { - it("should delete a custom role and reassign users to member_role", async () => { + it("should delete a custom role", async () => { const roleId = "role-id"; const role: Role = { id: roleId, @@ -315,43 +314,9 @@ describe("RoleService", () => { }; mockRepository.findById.mockResolvedValueOnce(role); - mockRepository.reassignUsersToRole.mockResolvedValueOnce(void 0); mockRepository.delete.mockResolvedValueOnce(void 0); await service.deleteRole(roleId); - expect(mockRepository.reassignUsersToRole).toHaveBeenCalledWith(roleId, "member_role"); - expect(mockRepository.delete).toHaveBeenCalledWith(roleId); - }); - - it("should reassign users before deleting the role", async () => { - const roleId = "role-id"; - const role: Role = { - id: roleId, - name: "Test Role", - teamId: 1, - color: "#000000", - type: RoleType.CUSTOM, - permissions: [], - createdAt: new Date(), - updatedAt: new Date(), - }; - - const callOrder: string[] = []; - mockRepository.findById.mockResolvedValueOnce(role); - mockRepository.reassignUsersToRole.mockImplementation(() => { - callOrder.push("reassignUsersToRole"); - return Promise.resolve(); - }); - mockRepository.delete.mockImplementation(() => { - callOrder.push("delete"); - return Promise.resolve(); - }); - - await service.deleteRole(roleId); - - // Verify that reassignment happens before deletion - expect(callOrder).toEqual(["reassignUsersToRole", "delete"]); - expect(mockRepository.reassignUsersToRole).toHaveBeenCalledWith(roleId, "member_role"); expect(mockRepository.delete).toHaveBeenCalledWith(roleId); }); @@ -369,7 +334,6 @@ describe("RoleService", () => { }); await expect(service.deleteRole(roleId)).rejects.toThrow("Cannot delete default roles"); - expect(mockRepository.reassignUsersToRole).not.toHaveBeenCalled(); expect(mockRepository.delete).not.toHaveBeenCalled(); }); @@ -378,7 +342,6 @@ describe("RoleService", () => { mockRepository.findById.mockResolvedValueOnce(null); await expect(service.deleteRole(roleId)).rejects.toThrow("Role not found"); - expect(mockRepository.reassignUsersToRole).not.toHaveBeenCalled(); expect(mockRepository.delete).not.toHaveBeenCalled(); }); }); diff --git a/packages/features/pbac/services/permission-check.service.ts b/packages/features/pbac/services/permission-check.service.ts index 4e0d4986eac3a1..8b9c67d2e54409 100644 --- a/packages/features/pbac/services/permission-check.service.ts +++ b/packages/features/pbac/services/permission-check.service.ts @@ -71,8 +71,8 @@ export class PermissionCheckService { teamActions.forEach((action) => actions.add(action)); } - // Get org-level permissions (works even without team membership) - if (orgMembership?.customRoleId) { + // Get org-level permissions as fallback + if (membership?.team?.parentId && orgMembership?.customRoleId) { const orgActions = await this.repository.getResourcePermissionsByRoleId( orgMembership.customRoleId, resource @@ -108,23 +108,27 @@ export class PermissionCheckService { return false; } + const membership = await MembershipRepository.findUniqueByUserIdAndTeamId({ + userId, + teamId, + }); + + if (!membership) return false; + const isPBACEnabled = await this.featuresRepository.checkIfTeamHasFeature( teamId, this.PBAC_FEATURE_FLAG ); if (isPBACEnabled) { - // Check if user has permission through team or org membership - return this.hasPermission({ userId, teamId }, permission); - } + if (!membership.customRoleId) { + this.logger.info(`PBAC is enabled for ${teamId} but no custom role is set on membership relation`); + return false; + } - // Fallback to role-based check only if user has team membership - const membership = await MembershipRepository.findUniqueByUserIdAndTeamId({ - userId, - teamId, - }); + return this.hasPermission({ membershipId: membership.id }, permission); + } - if (!membership) return false; return this.checkFallbackRoles(membership.role, fallbackRoles); } catch (error) { this.logger.error(error); @@ -153,23 +157,27 @@ export class PermissionCheckService { return false; } + const membership = await MembershipRepository.findUniqueByUserIdAndTeamId({ + userId, + teamId, + }); + + if (!membership) return false; + const isPBACEnabled = await this.featuresRepository.checkIfTeamHasFeature( teamId, this.PBAC_FEATURE_FLAG ); if (isPBACEnabled) { - // Check if user has permissions through team or org membership - return this.hasPermissions({ userId, teamId }, permissions); - } + if (!membership.customRoleId) { + this.logger.info(`PBAC is enabled for ${teamId} but no custom role is set on membership relation`); + return false; + } - // Fallback to role-based check only if user has team membership - const membership = await MembershipRepository.findUniqueByUserIdAndTeamId({ - userId, - teamId, - }); + return this.hasPermissions({ membershipId: membership.id }, permissions); + } - if (!membership) return false; return this.checkFallbackRoles(membership.role, fallbackRoles); } catch (error) { this.logger.error(error); @@ -233,16 +241,8 @@ export class PermissionCheckService { membership = await this.repository.getMembershipByUserAndTeam(query.userId, query.teamId); } - // Get org membership either through the team membership or directly from teamId if (membership?.team.parentId) { - // User has team membership, check org through that orgMembership = await this.repository.getOrgMembership(membership.userId, membership.team.parentId); - } else if (query.userId && query.teamId) { - // No team membership, but check if team belongs to an org - const team = await this.repository.getTeamById(query.teamId); - if (team?.parentId) { - orgMembership = await this.repository.getOrgMembership(query.userId, team.parentId); - } } return { membership, orgMembership }; diff --git a/packages/features/pbac/services/role-management.factory.ts b/packages/features/pbac/services/role-management.factory.ts index fb5928b3c34fee..66a88f18c957c5 100644 --- a/packages/features/pbac/services/role-management.factory.ts +++ b/packages/features/pbac/services/role-management.factory.ts @@ -1,6 +1,6 @@ import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; diff --git a/packages/features/pbac/services/role.service.ts b/packages/features/pbac/services/role.service.ts index 2eecce7ab32bfa..dd7313c30673ff 100644 --- a/packages/features/pbac/services/role.service.ts +++ b/packages/features/pbac/services/role.service.ts @@ -5,7 +5,7 @@ import { RoleType as DomainRoleType } from "../domain/models/Role"; import type { CreateRoleData, UpdateRolePermissionsData } from "../domain/models/Role"; import type { IRoleRepository } from "../domain/repositories/IRoleRepository"; import { RoleRepository } from "../infrastructure/repositories/RoleRepository"; -import { DEFAULT_ROLE_IDS, DefaultPBACRole } from "../lib/constants"; +import { DEFAULT_ROLE_IDS } from "../lib/constants"; import { PermissionDiffService } from "./permission-diff.service"; import { PermissionService } from "./permission.service"; @@ -75,10 +75,6 @@ export class RoleService { if (role.type === DomainRoleType.SYSTEM) { throw new Error("Cannot delete default roles"); } - - // Reassign all users with this role to the members_role - await this.repository.reassignUsersToRole(roleId, DefaultPBACRole.MEMBER_ROLE); - await this.repository.delete(roleId); } diff --git a/packages/features/pbac/utils/permissionTraversal.ts b/packages/features/pbac/utils/permissionTraversal.ts index 70f08924372f70..e0acd23ea291b9 100644 --- a/packages/features/pbac/utils/permissionTraversal.ts +++ b/packages/features/pbac/utils/permissionTraversal.ts @@ -1,10 +1,4 @@ -import { - CrudAction, - type CustomAction, - PERMISSION_REGISTRY, - Scope, - getPermissionsForScope, -} from "../domain/types/permission-registry"; +import { CrudAction, type CustomAction, PERMISSION_REGISTRY } from "../domain/types/permission-registry"; /** * Helper function to split permission string into resource and action @@ -28,18 +22,15 @@ const splitPermission = (permission: string): { resource: string; action: string * Generic permission graph traversal function using BFS * @param startPermission The permission to start traversal from * @param direction Whether to find dependencies or dependents - * @param scope The scope to use for permission registry (defaults to Organization) * @returns Array of related permissions (excluding the start permission itself) */ export const traversePermissions = ( startPermission: string, - direction: "dependencies" | "dependents", - scope: Scope = Scope.Organization + direction: "dependencies" | "dependents" ): string[] => { const visited = new Set(); const result = new Set(); const queue: string[] = [startPermission]; - const registry = getPermissionsForScope(scope); while (queue.length > 0) { const currentPermission = queue.shift(); @@ -55,7 +46,7 @@ export const traversePermissions = ( if (direction === "dependencies") { // Find what the current permission depends on const { resource, action } = splitPermission(currentPermission); - const resourceConfig = registry[resource as keyof typeof registry]; + const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY]; if (resourceConfig && resourceConfig[action as CrudAction | CustomAction]) { const permissionDetails = resourceConfig[action as CrudAction | CustomAction]; @@ -79,7 +70,7 @@ export const traversePermissions = ( } } else { // Find what depends on the current permission - Object.entries(registry).forEach(([resource, config]) => { + Object.entries(PERMISSION_REGISTRY).forEach(([resource, config]) => { Object.entries(config).forEach(([action, details]) => { if (action.startsWith("_")) return; // Skip internal keys @@ -117,19 +108,17 @@ export const traversePermissions = ( /** * Get all permissions that the given permission transitively depends on * @param permission The permission to find dependencies for - * @param scope The scope to use for permission registry (defaults to Organization) * @returns Array of dependency permissions */ -export const getTransitiveDependencies = (permission: string, scope: Scope = Scope.Organization): string[] => { - return traversePermissions(permission, "dependencies", scope); +export const getTransitiveDependencies = (permission: string): string[] => { + return traversePermissions(permission, "dependencies"); }; /** * Get all permissions that transitively depend on the given permission * @param permission The permission to find dependents for - * @param scope The scope to use for permission registry (defaults to Organization) * @returns Array of dependent permissions */ -export const getTransitiveDependents = (permission: string, scope: Scope = Scope.Organization): string[] => { - return traversePermissions(permission, "dependents", scope); +export const getTransitiveDependents = (permission: string): string[] => { + return traversePermissions(permission, "dependents"); }; diff --git a/packages/features/schedules/lib/use-schedule/useSchedule.ts b/packages/features/schedules/lib/use-schedule/useSchedule.ts index 29cb456f2169bf..11263327fd4add 100644 --- a/packages/features/schedules/lib/use-schedule/useSchedule.ts +++ b/packages/features/schedules/lib/use-schedule/useSchedule.ts @@ -6,7 +6,7 @@ import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookin import { useTimesForSchedule } from "@calcom/features/schedules/lib/use-schedule/useTimesForSchedule"; import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams"; import { PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS } from "@calcom/lib/constants"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; import { trpc } from "@calcom/trpc/react"; import { useApiV2AvailableSlots } from "./useApiV2AvailableSlots"; diff --git a/packages/features/tasker/tasks/analytics/sendAnalyticsEvent.ts b/packages/features/tasker/tasks/analytics/sendAnalyticsEvent.ts index 17864c78ce3393..94410b7ecd31d2 100644 --- a/packages/features/tasker/tasks/analytics/sendAnalyticsEvent.ts +++ b/packages/features/tasker/tasks/analytics/sendAnalyticsEvent.ts @@ -1,8 +1,8 @@ +import AnalyticsManager from "@calcom/lib/analyticsManager/analyticsManager"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { CredentialRepository } from "@calcom/lib/server/repository/credential"; -import AnalyticsManager from "./analyticsManager"; import { sendAnalyticsEventSchema } from "./schema"; const log = logger.getSubLogger({ prefix: [`[[tasker] sendAnalyticsEvent]`] }); diff --git a/packages/features/troubleshooter/components/LargeCalendar.tsx b/packages/features/troubleshooter/components/LargeCalendar.tsx index 28ba9f2935e66a..b4d30ee5714f49 100644 --- a/packages/features/troubleshooter/components/LargeCalendar.tsx +++ b/packages/features/troubleshooter/components/LargeCalendar.tsx @@ -2,8 +2,8 @@ import { useSession } from "next-auth/react"; import { useMemo } from "react"; import dayjs from "@calcom/dayjs"; -import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots"; import { Calendar } from "@calcom/features/calendars/weeklyview"; +import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state"; import { BookingStatus } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc"; @@ -48,7 +48,22 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => { .add(extraDays - 1, "day") .toDate(); - const availableSlots = useAvailableTimeSlots({ schedule, eventDuration: event?.duration ?? 30 }); + const availableSlots = useMemo(() => { + const availableTimeslots: CalendarAvailableTimeslots = {}; + if (!schedule) return availableTimeslots; + if (!schedule?.slots) return availableTimeslots; + + for (const day in schedule.slots) { + availableTimeslots[day] = schedule.slots[day].map((slot) => ({ + start: dayjs(slot.time).toDate(), + end: dayjs(slot.time) + .add(event?.duration ?? 30, "minutes") + .toDate(), + })); + } + + return availableTimeslots; + }, [schedule, event]); const events = useMemo(() => { if (!busyEvents?.busy) return []; diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx index 4460d1c7c4a81e..6e3f80cddf78a6 100644 --- a/packages/features/users/components/UserTable/UserListTable.tsx +++ b/packages/features/users/components/UserTable/UserListTable.tsx @@ -386,6 +386,9 @@ function UserListTableContent({ meta: { filter: { type: ColumnFilterType.DATE_RANGE, + dateRangeOptions: { + endOfDay: true, + }, }, }, cell: ({ row }) =>
{row.original.lastActiveAt}
, @@ -399,6 +402,9 @@ function UserListTableContent({ meta: { filter: { type: ColumnFilterType.DATE_RANGE, + dateRangeOptions: { + endOfDay: true, + }, }, }, cell: ({ row }) =>
{row.original.createdAt || ""}
, @@ -412,6 +418,9 @@ function UserListTableContent({ meta: { filter: { type: ColumnFilterType.DATE_RANGE, + dateRangeOptions: { + endOfDay: true, + }, }, }, cell: ({ row }) =>
{row.original.updatedAt || ""}
, diff --git a/packages/features/webhooks/components/CreateNewWebhookButton.tsx b/packages/features/webhooks/components/CreateNewWebhookButton.tsx index f01f856c429621..db83c6ed98e36e 100644 --- a/packages/features/webhooks/components/CreateNewWebhookButton.tsx +++ b/packages/features/webhooks/components/CreateNewWebhookButton.tsx @@ -4,9 +4,8 @@ import { useRouter } from "next/navigation"; import { CreateButtonWithTeamsList } from "@calcom/features/ee/teams/components/createButton/CreateButtonWithTeamsList"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { MembershipRole } from "@calcom/prisma/enums"; -export const CreateNewWebhookButton = () => { +export const CreateNewWebhookButton = ({ isAdmin }: { isAdmin: boolean }) => { const router = useRouter(); const { t } = useLocale(); const createFunction = (teamId?: number, platform?: boolean) => { @@ -21,13 +20,10 @@ export const CreateNewWebhookButton = () => { ); }; diff --git a/packages/features/webhooks/components/WebhookListItem.tsx b/packages/features/webhooks/components/WebhookListItem.tsx index ba590b70ac95af..1d354da07174d2 100644 --- a/packages/features/webhooks/components/WebhookListItem.tsx +++ b/packages/features/webhooks/components/WebhookListItem.tsx @@ -36,14 +36,12 @@ export default function WebhookListItem(props: { canEditWebhook?: boolean; onEditWebhook: () => void; lastItem: boolean; - permissions: { - canEditWebhook?: boolean; - canDeleteWebhook?: boolean; - }; + readOnly?: boolean; }) { const { t } = useLocale(); const utils = trpc.useUtils(); const { webhook } = props; + const canEditWebhook = props.canEditWebhook ?? true; const deleteWebhook = trpc.viewer.webhook.delete.useMutation({ async onSuccess() { @@ -89,7 +87,7 @@ export default function WebhookListItem(props: { {webhook.subscriberUrl}

- {!props.permissions.canEditWebhook && ( + {!!props.readOnly && ( {t("readonly")} @@ -109,12 +107,12 @@ export default function WebhookListItem(props: {
- {(props.permissions.canEditWebhook || props.permissions.canDeleteWebhook) && ( + {!props.readOnly && (
toggleWebhook.mutate({ id: webhook.id, @@ -125,48 +123,39 @@ export default function WebhookListItem(props: { } /> - {props.permissions.canEditWebhook && ( - - )} + - {props.permissions.canDeleteWebhook && ( -
diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts index 02485d3f8196f4..f17c5d9f696dfa 100644 --- a/packages/features/webhooks/lib/scheduleTrigger.ts +++ b/packages/features/webhooks/lib/scheduleTrigger.ts @@ -4,7 +4,7 @@ import { selectOOOEntries } from "@calcom/app-store/zapier/api/subscriptions/lis import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import tasker from "@calcom/features/tasker"; -import { DailyLocationType, getHumanReadableLocationValue } from "@calcom/app-store/locations"; +import { DailyLocationType, getHumanReadableLocationValue } from "@calcom/lib/location"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { withReporting } from "@calcom/lib/sentryWrapper"; diff --git a/packages/features/webhooks/pages/webhooks-view.tsx b/packages/features/webhooks/pages/webhooks-view.tsx index edfa2350e58fa8..981470085c2723 100644 --- a/packages/features/webhooks/pages/webhooks-view.tsx +++ b/packages/features/webhooks/pages/webhooks-view.tsx @@ -7,27 +7,33 @@ import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { RouterOutputs } from "@calcom/trpc/react"; +import type { WebhooksByViewer } from "@calcom/trpc/server/routers/viewer/webhook/getByViewer.handler"; import classNames from "@calcom/ui/classNames"; import { Avatar } from "@calcom/ui/components/avatar"; import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import { WebhookListItem, CreateNewWebhookButton } from "../components"; -type WebhooksByViewer = RouterOutputs["viewer"]["webhook"]["getByViewer"]; - type Props = { - data: WebhooksByViewer; + data: RouterOutputs["viewer"]["webhook"]["getByViewer"]; + isAdmin: boolean; }; -const WebhooksView = ({ data }: Props) => { +const WebhooksView = ({ data, isAdmin }: Props) => { return (
- +
); }; -const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer }) => { +const WebhooksList = ({ + webhooksByViewer, + isAdmin, +}: { + webhooksByViewer: WebhooksByViewer; + isAdmin: boolean; +}) => { const { t } = useLocale(); const router = useRouter(); const { profiles, webhookGroups } = webhooksByViewer; @@ -39,7 +45,7 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer 0 ? : null} + CTA={webhooksByViewer.webhookGroups.length > 0 ? : null} borderInShellHeader={false}> {!!webhookGroups.length ? (
@@ -64,11 +70,8 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer router.push(`${WEBAPP_URL}/settings/developer/webhooks/${webhook.id}`) } @@ -85,7 +88,7 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer headline={t("create_your_first_webhook")} description={t("create_your_first_webhook_description", { appName: APP_NAME })} className="mt-6 rounded-b-lg" - buttonRaw={} + buttonRaw={} border={true} /> )} diff --git a/packages/features/calendars/lib/CalendarManager.test.ts b/packages/lib/CalendarManager.test.ts similarity index 100% rename from packages/features/calendars/lib/CalendarManager.test.ts rename to packages/lib/CalendarManager.test.ts diff --git a/packages/features/calendars/lib/CalendarManager.ts b/packages/lib/CalendarManager.ts similarity index 98% rename from packages/features/calendars/lib/CalendarManager.ts rename to packages/lib/CalendarManager.ts index ce07a064542c2e..a6c2a8418973d2 100644 --- a/packages/features/calendars/lib/CalendarManager.ts +++ b/packages/lib/CalendarManager.ts @@ -11,8 +11,6 @@ import { CalendarAppDelegationCredentialError } from "@calcom/lib/CalendarAppErr import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants"; import { buildNonDelegationCredentials } from "@calcom/lib/delegationCredential/clientAndServer"; import { formatCalEvent } from "@calcom/lib/formatCalendarEvent"; -import getCalendarsEvents from "@calcom/lib/getCalendarsEvents"; -import { getCalendarsEventsWithTimezones } from "@calcom/lib/getCalendarsEvents"; import logger from "@calcom/lib/logger"; import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -27,6 +25,9 @@ import type { import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential"; import type { EventResult } from "@calcom/types/EventManager"; +import getCalendarsEvents from "./getCalendarsEvents"; +import { getCalendarsEventsWithTimezones } from "./getCalendarsEvents"; + const log = logger.getSubLogger({ prefix: ["CalendarManager"] }); export const getCalendarCredentials = (credentials: Array) => { @@ -134,7 +135,7 @@ export const getConnectedCalendars = async ( errorMessage = error.message; } - log.error("getConnectedCalendars failed", errorMessage, safeStringify({ item })); + log.error("getConnectedCalendars failed", safeStringify(error), safeStringify({ item })); return { integration: cleanIntegrationKeys(item.integration), diff --git a/packages/features/bookings/lib/EventManager.test.ts b/packages/lib/EventManager.test.ts similarity index 100% rename from packages/features/bookings/lib/EventManager.test.ts rename to packages/lib/EventManager.test.ts diff --git a/packages/features/bookings/lib/EventManager.ts b/packages/lib/EventManager.ts similarity index 99% rename from packages/features/bookings/lib/EventManager.ts rename to packages/lib/EventManager.ts index c4e6e9662be641..e2a77d3979f7ea 100644 --- a/packages/features/bookings/lib/EventManager.ts +++ b/packages/lib/EventManager.ts @@ -34,10 +34,10 @@ import type { PartialReference, } from "@calcom/types/EventManager"; -import { createEvent, updateEvent, deleteEvent } from "@calcom/features/calendars/lib/CalendarManager"; -import CrmManager from "@calcom/lib/crmManager/crmManager"; -import { isDelegationCredential } from "@calcom/lib/delegationCredential/clientAndServer"; -import { createMeeting, updateMeeting, deleteMeeting } from "@calcom/app-store/videoClient"; +import { createEvent, updateEvent, deleteEvent } from "./CalendarManager"; +import CrmManager from "./crmManager/crmManager"; +import { isDelegationCredential } from "./delegationCredential/clientAndServer"; +import { createMeeting, updateMeeting, deleteMeeting } from "./videoClient"; const log = logger.getSubLogger({ prefix: ["EventManager"] }); const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || ""; diff --git a/packages/features/tasker/tasks/analytics/analyticsManager.ts b/packages/lib/analyticsManager/analyticsManager.ts similarity index 100% rename from packages/features/tasker/tasks/analytics/analyticsManager.ts rename to packages/lib/analyticsManager/analyticsManager.ts diff --git a/packages/features/tasker/tasks/analytics/handleAnalyticsEvents.ts b/packages/lib/analyticsManager/handleAnalyticsEvents.ts similarity index 95% rename from packages/features/tasker/tasks/analytics/handleAnalyticsEvents.ts rename to packages/lib/analyticsManager/handleAnalyticsEvents.ts index 1f4aaca8c9fa93..c068594c5eace6 100644 --- a/packages/features/tasker/tasks/analytics/handleAnalyticsEvents.ts +++ b/packages/lib/analyticsManager/handleAnalyticsEvents.ts @@ -1,7 +1,6 @@ +import tasker from "@calcom/features/tasker"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; -import { tasker } from "../../../tasker"; - interface HandleAnalyticsEventsProps { credentials: CredentialForCalendarService[]; rawBookingData: Record; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index defb3cff46f340..3fee151ca6eaea 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -249,8 +249,3 @@ export const RETELL_AI_TEST_EVENT_TYPE_MAP = (() => { return null; } })(); - -// Environment variable for configuring past booking reschedule behavior per team. A comma separated list of team IDs(e.g. '1,2,3') -/* This is an internal environment variable and is not meant to be used by the self-hosters. It is planned to be removed later by either having it as an option in Event Type or by some other customer configurable approaches*/ -export const ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS = - process.env._CAL_INTERNAL_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS; diff --git a/packages/features/eventtypes/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts similarity index 98% rename from packages/features/eventtypes/lib/defaultEvents.ts rename to packages/lib/defaultEvents.ts index b50efc84f1b6d3..511c1352884bae 100644 --- a/packages/features/eventtypes/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -1,10 +1,10 @@ import { DailyLocationType } from "@calcom/app-store/constants"; -import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; import slugify from "@calcom/lib/slugify"; import type { Prisma, SelectedCalendar } from "@calcom/prisma/client"; import { PeriodType, SchedulingType } from "@calcom/prisma/enums"; import type { userSelect } from "@calcom/prisma/selects"; import type { CustomInputSchema } from "@calcom/prisma/zod-utils"; +import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { CredentialPayload } from "@calcom/types/Credential"; @@ -92,7 +92,6 @@ const commons = { disableRescheduling: false, onlyShowFirstAvailableSlot: false, allowReschedulingPastBookings: false, - allowReschedulingCancelledBookings: false, hideOrganizerEmail: false, showOptimizedSlots: false, id: 0, @@ -149,8 +148,6 @@ const commons = { eventTypeColor: null, hostGroups: [], bookingRequiresAuthentication: false, - createdAt: null, - updatedAt: null, }; export const dynamicEvent = { diff --git a/packages/app-store/_utils/getBulkEventTypes.ts b/packages/lib/event-types/getBulkEventTypes.ts similarity index 95% rename from packages/app-store/_utils/getBulkEventTypes.ts rename to packages/lib/event-types/getBulkEventTypes.ts index d2c573ae1d884f..6117926ee616ac 100644 --- a/packages/app-store/_utils/getBulkEventTypes.ts +++ b/packages/lib/event-types/getBulkEventTypes.ts @@ -1,8 +1,7 @@ +import { getAppFromLocationValue } from "@calcom/app-store/utils"; import { prisma } from "@calcom/prisma"; import { eventTypeLocations as eventTypeLocationsSchema } from "@calcom/prisma/zod-utils"; -import { getAppFromLocationValue } from "../utils"; - /** * Process event types to add logo information * @param eventTypes - The event types to process diff --git a/packages/features/eventtypes/lib/getEventTypeById.test.ts b/packages/lib/event-types/getEventTypeById.test.ts similarity index 99% rename from packages/features/eventtypes/lib/getEventTypeById.test.ts rename to packages/lib/event-types/getEventTypeById.test.ts index 3b9f77ce2aac33..2ceebe28cb310a 100644 --- a/packages/features/eventtypes/lib/getEventTypeById.test.ts +++ b/packages/lib/event-types/getEventTypeById.test.ts @@ -1,4 +1,4 @@ -import prismock from "../../../../tests/libs/__mocks__/prisma"; +import prismock from "../../../tests/libs/__mocks__/prisma"; import { mockNoTranslations } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; diff --git a/packages/features/eventtypes/lib/getEventTypeById.ts b/packages/lib/event-types/getEventTypeById.ts similarity index 97% rename from packages/features/eventtypes/lib/getEventTypeById.ts rename to packages/lib/event-types/getEventTypeById.ts index c9b3fae9b6f86f..3cbd419eec7254 100644 --- a/packages/features/eventtypes/lib/getEventTypeById.ts +++ b/packages/lib/event-types/getEventTypeById.ts @@ -3,14 +3,12 @@ import { getLocationGroupedOptions } from "@calcom/app-store/server"; import { getEventTypeAppData } from "@calcom/app-store/utils"; import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; -import { WEBSITE_URL } from "@calcom/lib/constants"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; -import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits"; import { parseEventTypeColor } from "@calcom/lib/isEventTypeColor"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; -import type { LocationObject } from "@calcom/app-store/locations"; +import type { LocationObject } from "@calcom/lib/location"; import { getTranslation } from "@calcom/lib/server/i18n"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository"; import { UserRepository } from "@calcom/lib/server/repository/user"; @@ -21,6 +19,9 @@ import { customInputSchema } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; +import { WEBSITE_URL } from "../constants"; +import { getBookerBaseUrl } from "../getBookerUrl/server"; + interface getEventTypeByIdProps { eventTypeId: number; userId: number; diff --git a/packages/features/eventtypes/lib/getEventTypesByViewer.ts b/packages/lib/event-types/getEventTypesByViewer.ts similarity index 100% rename from packages/features/eventtypes/lib/getEventTypesByViewer.ts rename to packages/lib/event-types/getEventTypesByViewer.ts diff --git a/packages/features/eventtypes/lib/getEventTypesPublic.ts b/packages/lib/event-types/getEventTypesPublic.ts similarity index 98% rename from packages/features/eventtypes/lib/getEventTypesPublic.ts rename to packages/lib/event-types/getEventTypesPublic.ts index a64ff1fc1134fc..c889ad934e52c6 100644 --- a/packages/features/eventtypes/lib/getEventTypesPublic.ts +++ b/packages/lib/event-types/getEventTypesPublic.ts @@ -1,10 +1,11 @@ import logger from "@calcom/lib/logger"; -import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import type { baseEventTypeSelect } from "@calcom/prisma/selects"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { markdownToSafeHTML } from "../markdownToSafeHTML"; + const log = logger.getSubLogger({ prefix: ["getEventTypesPublic"] }); export type EventTypesPublic = Awaited>; diff --git a/packages/features/eventtypes/lib/checkForEmptyAssignment.ts b/packages/lib/event-types/utils/checkForEmptyAssignment.ts similarity index 87% rename from packages/features/eventtypes/lib/checkForEmptyAssignment.ts rename to packages/lib/event-types/utils/checkForEmptyAssignment.ts index a1c27825bd7bc9..d7b33fd096b1ba 100644 --- a/packages/features/eventtypes/lib/checkForEmptyAssignment.ts +++ b/packages/lib/event-types/utils/checkForEmptyAssignment.ts @@ -1,4 +1,7 @@ -import type { EventTypeAssignedUsers, EventTypeHosts } from "../components/EventType"; +import type { + EventTypeAssignedUsers, + EventTypeHosts, +} from "@calcom/features/eventtypes/components/EventType"; // This function checks if EventType requires assignment. // returns true: if EventType requires assignment but there is no assignment yet done by the user. diff --git a/packages/lib/event-types/utils/locationsResolver.ts b/packages/lib/event-types/utils/locationsResolver.ts new file mode 100644 index 00000000000000..119b00ebdc6bbb --- /dev/null +++ b/packages/lib/event-types/utils/locationsResolver.ts @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import type { TFunction } from "i18next"; +import { isValidPhoneNumber } from "libphonenumber-js"; +// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router +import { z } from "zod"; + +import { getEventLocationType } from "@calcom/app-store/locations"; + +export const locationsResolver = (t: TFunction) => { + return z + .array( + z + .object({ + type: z.string(), + address: z.string().optional(), + link: z.string().url().optional(), + phone: z + .string() + .refine((val) => isValidPhoneNumber(val)) + .optional(), + hostPhoneNumber: z + .string() + .refine((val) => isValidPhoneNumber(val)) + .optional(), + displayLocationPublicly: z.boolean().optional(), + credentialId: z.number().optional(), + teamName: z.string().optional(), + }) + .passthrough() + .superRefine((val, ctx) => { + if (val?.link) { + const link = val.link; + const eventLocationType = getEventLocationType(val.type); + if ( + eventLocationType && + !eventLocationType.default && + eventLocationType.linkType === "static" && + eventLocationType.urlRegExp + ) { + const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(link).success; + + if (!valid) { + const sampleUrl = eventLocationType.organizerInputPlaceholder; + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [eventLocationType?.defaultValueVariable ?? "link"], + message: t("invalid_url_error_message", { + label: eventLocationType.label, + sampleUrl: sampleUrl ?? "https://cal.com", + }), + }); + } + return; + } + + const valid = z.string().url().optional().safeParse(link).success; + + if (!valid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [eventLocationType?.defaultValueVariable ?? "link"], + message: `Invalid URL`, + }); + } + } + return; + }) + ) + .optional(); +}; diff --git a/packages/lib/formatCalendarEvent.ts b/packages/lib/formatCalendarEvent.ts index a9e8482efe0659..bfdd36301daca7 100644 --- a/packages/lib/formatCalendarEvent.ts +++ b/packages/lib/formatCalendarEvent.ts @@ -14,23 +14,14 @@ const formatClientIdFromEmails = (calEvent: CalendarEvent | ExtendedCalendarEven ...calEvent.organizer, email: calEvent.organizer.email.replace(`+${clientId}`, ""), }; - const team = calEvent.team - ? { - ...calEvent.team, - members: calEvent.team.members.map((member) => ({ - ...member, - email: member.email.replace(`+${clientId}`, ""), - })), - } - : undefined; - return [attendees, organizer, team]; + return [attendees, organizer]; }; export const formatCalEvent = (calEvent: CalendarEvent) => { const clonedEvent = cloneDeep(calEvent); if (clonedEvent.platformClientId) { - const [attendees, organizer, team] = formatClientIdFromEmails(clonedEvent, clonedEvent.platformClientId); - Object.assign(clonedEvent, { attendees, organizer, team }); + const [attendees, organizer] = formatClientIdFromEmails(clonedEvent, clonedEvent.platformClientId); + Object.assign(clonedEvent, { attendees, organizer }); } return clonedEvent; diff --git a/packages/lib/getBooking.tsx b/packages/lib/getBooking.tsx index feaa2c00f84e76..4a3594248887b0 100644 --- a/packages/lib/getBooking.tsx +++ b/packages/lib/getBooking.tsx @@ -1,3 +1,6 @@ +import type { z } from "zod"; + +import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import slugify from "@calcom/lib/slugify"; import type { PrismaClient } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; @@ -98,8 +101,8 @@ export const getBookingWithResponses = < ) => { return { ...booking, - responses: booking.responses || getResponsesFromOldBooking(booking), - } as Omit & { responses: Record }; + responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)), + } as Omit & { responses: z.infer }; }; export default getBooking; diff --git a/packages/lib/getBusyTimes.ts b/packages/lib/getBusyTimes.ts index 4932cc36c456b8..c9aeb86f8d7cad 100644 --- a/packages/lib/getBusyTimes.ts +++ b/packages/lib/getBusyTimes.ts @@ -1,5 +1,5 @@ import dayjs from "@calcom/dayjs"; -import { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager"; +import { getBusyCalendarTimes } from "@calcom/lib/CalendarManager"; import { subtract } from "@calcom/lib/date-ranges"; import { stringToDayjs } from "@calcom/lib/dayjs"; import { intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit"; diff --git a/packages/lib/getConnectedDestinationCalendars.ts b/packages/lib/getConnectedDestinationCalendars.ts index 1ab936685167a4..5e60d6999576ef 100644 --- a/packages/lib/getConnectedDestinationCalendars.ts +++ b/packages/lib/getConnectedDestinationCalendars.ts @@ -1,4 +1,4 @@ -import { getCalendarCredentials, getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager"; +import { getCalendarCredentials, getConnectedCalendars } from "@calcom/lib/CalendarManager"; import { isDelegationCredential } from "@calcom/lib/delegationCredential/clientAndServer"; import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server"; import logger from "@calcom/lib/logger"; diff --git a/packages/features/apps/hooks/useAppsData.ts b/packages/lib/hooks/useAppsData.ts similarity index 100% rename from packages/features/apps/hooks/useAppsData.ts rename to packages/lib/hooks/useAppsData.ts diff --git a/packages/lib/location.ts b/packages/lib/location.ts new file mode 100644 index 00000000000000..3c5493f967c480 --- /dev/null +++ b/packages/lib/location.ts @@ -0,0 +1 @@ +export * from "@calcom/app-store/locations"; diff --git a/packages/lib/price.ts b/packages/lib/price.ts new file mode 100644 index 00000000000000..4a6e92128ff16b --- /dev/null +++ b/packages/lib/price.ts @@ -0,0 +1,14 @@ +import { convertFromSmallestToPresentableCurrencyUnit } from "@calcom/app-store/_utils/payments/currencyConversions"; + +export const formatPrice = (price: number, currency: string | undefined, locale = "en") => { + switch (currency) { + case "BTC": + return `${price} sats`; + default: + currency = currency?.toUpperCase() || "USD"; + return `${Intl.NumberFormat(locale, { + style: "currency", + currency: currency, + }).format(convertFromSmallestToPresentableCurrencyUnit(price, currency))}`; + } +}; diff --git a/packages/lib/server/defaultHandler.test.ts b/packages/lib/server/defaultHandler.test.ts index 8c79d48a25cb65..b15d62bd426012 100644 --- a/packages/lib/server/defaultHandler.test.ts +++ b/packages/lib/server/defaultHandler.test.ts @@ -39,4 +39,23 @@ describe("defaultHandler Test Suite", () => { expect(getHandler).toHaveBeenCalledWith(req, res); }); + + it("should return 500 for errors thrown in handler", async () => { + const getHandler = vi.fn().mockRejectedValue(new Error("Test Error")); + const handlers = { + GET: { default: getHandler }, + }; + const handler = defaultHandler(handlers); + + const { req, res } = createMocks({ + method: "GET", + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(500); + expect(res._getJSONData()).toEqual({ + message: "Something went wrong", + }); + }); }); diff --git a/packages/lib/server/defaultHandler.ts b/packages/lib/server/defaultHandler.ts index 251dd646d4bb71..4f64be2d1f8a71 100644 --- a/packages/lib/server/defaultHandler.ts +++ b/packages/lib/server/defaultHandler.ts @@ -13,5 +13,12 @@ export const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest .status(405) .json({ message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})` }); } - return await handler(req, res); + + try { + await handler(req, res); + return; + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Something went wrong" }); + } }; diff --git a/packages/lib/server/getLuckyUser.ts b/packages/lib/server/getLuckyUser.ts index e1ec8cda1185ef..b55a85251e4a68 100644 --- a/packages/lib/server/getLuckyUser.ts +++ b/packages/lib/server/getLuckyUser.ts @@ -1,7 +1,7 @@ import type { FormResponse, Fields } from "@calcom/app-store/routing-forms/types/types"; import { zodRoutes } from "@calcom/app-store/routing-forms/zod"; import dayjs from "@calcom/dayjs"; -import { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager"; +import { getBusyCalendarTimes } from "@calcom/lib/CalendarManager"; import logger from "@calcom/lib/logger"; import { acrossQueryValueCompatiblity } from "@calcom/lib/raqb/raqbUtils"; import { raqbQueryValueSchema } from "@calcom/lib/raqb/zod"; diff --git a/packages/features/ee/teams/lib/getTeamMemberEmailFromCrm.ts b/packages/lib/server/getTeamMemberEmailFromCrm.ts similarity index 100% rename from packages/features/ee/teams/lib/getTeamMemberEmailFromCrm.ts rename to packages/lib/server/getTeamMemberEmailFromCrm.ts diff --git a/packages/features/ee/teams/lib/queries.ts b/packages/lib/server/queries/teams/index.ts similarity index 98% rename from packages/features/ee/teams/lib/queries.ts rename to packages/lib/server/queries/teams/index.ts index 0ccb517c95b939..d263d788165a3d 100644 --- a/packages/features/ee/teams/lib/queries.ts +++ b/packages/lib/server/queries/teams/index.ts @@ -2,24 +2,25 @@ import { z } from "zod"; import { getAppFromSlug } from "@calcom/app-store/utils"; import { DATABASE_CHUNK_SIZE } from "@calcom/lib/constants"; -import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; -import { getTeam, getOrg } from "@calcom/lib/server/repository/team"; -import { UserRepository } from "@calcom/lib/server/repository/user"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import type { Team } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { baseEventTypeSelect } from "@calcom/prisma/selects"; +import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema"; import { EventTypeMetaDataSchema, allManagedEventTypeProps, unlockedManagedEventTypeProps, eventTypeLocations, } from "@calcom/prisma/zod-utils"; -import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema"; + +import { getBookerBaseUrlSync } from "../../../getBookerUrl/client"; +import { getTeam, getOrg } from "../../repository/team"; +import { UserRepository } from "../../repository/user"; export type TeamWithMembers = Awaited>; diff --git a/packages/features/apps/repository/PrismaAppRepository.ts b/packages/lib/server/repository/PrismaAppRepository.ts similarity index 100% rename from packages/features/apps/repository/PrismaAppRepository.ts rename to packages/lib/server/repository/PrismaAppRepository.ts diff --git a/packages/lib/server/repository/webhook.ts b/packages/lib/server/repository/webhook.ts index 4b8c4d337e494b..a17cf3f86aa773 100644 --- a/packages/lib/server/repository/webhook.ts +++ b/packages/lib/server/repository/webhook.ts @@ -1,5 +1,5 @@ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { compareMembership } from "@calcom/lib/event-types/getEventTypesByViewer"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { prisma } from "@calcom/prisma"; import type { Webhook } from "@calcom/prisma/client"; @@ -14,8 +14,7 @@ type WebhookGroup = { image?: string; }; metadata?: { - canModify: boolean; - canDelete: boolean; + readOnly: boolean; }; webhooks: Webhook[]; }; @@ -31,28 +30,7 @@ const filterWebhooks = (webhook: Webhook) => { }; export class WebhookRepository { - static async findByWebhookId(webhookId?: string) { - return await prisma.webhook.findUniqueOrThrow({ - where: { - id: webhookId, - }, - select: { - id: true, - subscriberUrl: true, - payloadTemplate: true, - active: true, - eventTriggers: true, - secret: true, - teamId: true, - userId: true, - platform: true, - time: true, - timeUnit: true, - }, - }); - } - - static async getFilteredWebhooksForUser({ + static async getAllWebhooksByUserId({ userId, userRole, }: { @@ -60,12 +38,13 @@ export class WebhookRepository { userRole?: UserPermissionRole; }) { const user = await prisma.user.findUnique({ - where: { id: userId }, + where: { + id: userId, + }, select: { - id: true, username: true, - name: true, avatarUrl: true, + name: true, webhooks: true, teams: { where: { @@ -76,10 +55,18 @@ export class WebhookRepository { team: { select: { id: true, + isOrganization: true, name: true, slug: true, - logoUrl: true, + parentId: true, + metadata: true, + members: { + select: { + userId: true, + }, + }, webhooks: true, + logoUrl: true, }, }, }, @@ -91,82 +78,64 @@ export class WebhookRepository { throw new Error("User not found"); } - // Use permission service which handles both PBAC and role-based fallbacks - const permissionService = new PermissionCheckService(); - - // Build webhook groups with proper permissions - const webhookGroups: WebhookGroup[] = []; + let userWebhooks = user.webhooks; + userWebhooks = userWebhooks.filter(filterWebhooks); + let webhookGroups: WebhookGroup[] = []; - // Add user's personal webhooks webhookGroups.push({ teamId: null, profile: { slug: user.username, name: user.name, - image: getUserAvatarUrl({ avatarUrl: user.avatarUrl }), + image: getUserAvatarUrl({ + avatarUrl: user.avatarUrl, + }), }, - webhooks: user.webhooks.filter(filterWebhooks), + webhooks: userWebhooks, metadata: { - canModify: true, - canDelete: true, + readOnly: false, }, }); - // Check permissions for each team - // The permission service handles PBAC when enabled and falls back to role-based permissions - for (const membership of user.teams) { - const teamId = membership.team.id; - - // Check read permission (fallback: MEMBER, ADMIN, OWNER can read) - const canRead = await permissionService.checkPermission({ - userId, - teamId, - permission: "webhook.read", - fallbackRoles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!canRead) { - // User doesn't have permission to view this team's webhooks - continue; - } + const teamMemberships = user.teams.map((membership) => ({ + teamId: membership.team.id, + membershipRole: membership.role, + })); - // Check update/delete permissions in parallel (fallback: only ADMIN, OWNER can modify) - const [canUpdate, canDelete] = await Promise.all([ - permissionService.checkPermission({ - userId, - teamId, - permission: "webhook.update", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }), - permissionService.checkPermission({ - userId, - teamId, - permission: "webhook.delete", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }), - ]); - - webhookGroups.push({ + const teamWebhookGroups: WebhookGroup[] = user.teams.map((membership) => { + const orgMembership = teamMemberships.find( + (teamM) => teamM.teamId === membership.team.parentId + )?.membershipRole; + return { teamId: membership.team.id, profile: { name: membership.team.name, - slug: membership.team.slug || null, + slug: membership.team.slug + ? !membership.team.parentId + ? `/team` + : `${membership.team.slug}` + : null, image: getPlaceholderAvatar(membership.team.logoUrl, membership.team.name), }, - webhooks: membership.team.webhooks.filter(filterWebhooks), metadata: { - canModify: canUpdate, - canDelete, + readOnly: + membership.role === + (membership.team.parentId + ? orgMembership && compareMembership(orgMembership, membership.role) + ? orgMembership + : MembershipRole.MEMBER + : MembershipRole.MEMBER), }, - }); - } + webhooks: membership.team.webhooks.filter(filterWebhooks), + }; + }); + + webhookGroups = webhookGroups.concat(teamWebhookGroups); - // Add platform webhooks for admins if (userRole === "ADMIN") { const platformWebhooks = await prisma.webhook.findMany({ where: { platform: true }, }); - webhookGroups.push({ teamId: null, profile: { @@ -176,14 +145,13 @@ export class WebhookRepository { }, webhooks: platformWebhooks, metadata: { - canDelete: true, - canModify: true, + readOnly: false, }, }); } return { - webhookGroups: webhookGroups.filter((group) => group.webhooks.length > 0), + webhookGroups: webhookGroups.filter((groupBy) => !!groupBy.webhooks?.length), profiles: webhookGroups.map((group) => ({ teamId: group.teamId, ...group.profile, @@ -191,4 +159,25 @@ export class WebhookRepository { })), }; } + + static async findByWebhookId(webhookId?: string) { + return await prisma.webhook.findUniqueOrThrow({ + where: { + id: webhookId, + }, + select: { + id: true, + subscriberUrl: true, + payloadTemplate: true, + active: true, + eventTriggers: true, + secret: true, + teamId: true, + userId: true, + platform: true, + time: true, + timeUnit: true, + }, + }); + } } diff --git a/packages/lib/server/service/InsightsBookingBaseService.ts b/packages/lib/server/service/InsightsBookingBaseService.ts index 183f9af07adfb8..6a8f9ea045ce2d 100644 --- a/packages/lib/server/service/InsightsBookingBaseService.ts +++ b/packages/lib/server/service/InsightsBookingBaseService.ts @@ -4,21 +4,14 @@ import { z } from "zod"; import dayjs from "@calcom/dayjs"; import { makeSqlCondition } from "@calcom/features/data-table/lib/server"; import { ZColumnFilter } from "@calcom/features/data-table/lib/types"; -import { ColumnFilterType } from "@calcom/features/data-table/lib/types"; -import { type ColumnFilter } from "@calcom/features/data-table/lib/types"; +import type { ColumnFilter } from "@calcom/features/data-table/lib/types"; import { isSingleSelectFilterValue, isMultiSelectFilterValue, isTextFilterValue, isNumberFilterValue, - isDateRangeFilterValue, } from "@calcom/features/data-table/lib/utils"; -import { - extractDateRangeFromColumnFilters, - replaceDateRangeColumnFilter, -} from "@calcom/features/insights/lib/bookingUtils"; import type { DateRange } from "@calcom/features/insights/server/insightsDateUtils"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import type { PrismaClient } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -107,7 +100,7 @@ export const insightsBookingServiceOptionsSchema = z.discriminatedUnion("scope", z.object({ scope: z.literal("user"), userId: z.number(), - orgId: z.number().nullish().optional(), + orgId: z.number(), }), z.object({ scope: z.literal("org"), @@ -117,7 +110,7 @@ export const insightsBookingServiceOptionsSchema = z.discriminatedUnion("scope", z.object({ scope: z.literal("team"), userId: z.number(), - orgId: z.number().nullish().optional(), + orgId: z.number(), teamId: z.number(), }), ]); @@ -125,7 +118,7 @@ export const insightsBookingServiceOptionsSchema = z.discriminatedUnion("scope", export type InsightsBookingServicePublicOptions = { scope: "user" | "org" | "team"; userId: number; - orgId: number | null | undefined; + orgId: number; teamId?: number; }; @@ -134,6 +127,13 @@ export type InsightsBookingServiceOptions = z.infer; export const insightsBookingServiceFilterOptionsSchema = z.object({ + dateRange: z + .object({ + target: z.enum(["createdAt", "startTime"]), + startDate: z.string(), + endDate: z.string(), + }) + .optional(), columnFilters: z.array(ZColumnFilter).optional(), }); @@ -273,6 +273,23 @@ export class InsightsBookingBaseService { } } + // Use dateRange object for date filtering + if (this.filters.dateRange) { + const { target, startDate, endDate } = this.filters.dateRange; + if (startDate) { + if (isNaN(Date.parse(startDate))) { + throw new Error(`Invalid date format: ${startDate}`); + } + conditions.push(Prisma.sql`"${Prisma.raw(target)}" >= ${startDate}::timestamp`); + } + if (endDate) { + if (isNaN(Date.parse(endDate))) { + throw new Error(`Invalid date format: ${endDate}`); + } + conditions.push(Prisma.sql`"${Prisma.raw(target)}" <= ${endDate}::timestamp`); + } + } + if (conditions.length === 0) { return null; } @@ -338,39 +355,6 @@ export class InsightsBookingBaseService { } } - if ((id === "startTime" || id === "createdAt") && isDateRangeFilterValue(value)) { - const conditions: Prisma.Sql[] = []; - // if `startTime` filter -> x <= "startTime" AND "endTime" <= y - // if `createdAt` filter -> x <= "createdAt" AND "createdAt" <= y - if (value.data.startDate) { - if (isNaN(Date.parse(value.data.startDate))) { - throw new Error(`Invalid date format: ${value.data.startDate}`); - } - if (id === "startTime") { - conditions.push(Prisma.sql`${value.data.startDate}::timestamp <= "startTime"`); - } else { - conditions.push(Prisma.sql`${value.data.startDate}::timestamp <= "createdAt"`); - } - } - if (value.data.endDate) { - if (isNaN(Date.parse(value.data.endDate))) { - throw new Error(`Invalid date format: ${value.data.endDate}`); - } - if (id === "startTime") { - conditions.push(Prisma.sql`"endTime" <= ${value.data.endDate}::timestamp`); - } else { - conditions.push(Prisma.sql`"createdAt" <= ${value.data.endDate}::timestamp`); - } - } - if (conditions.length === 0) { - return null; - } - return conditions.reduce((acc, condition, index) => { - if (index === 0) return condition; - return Prisma.sql`(${acc}) AND (${condition})`; - }); - } - return null; } @@ -436,27 +420,13 @@ export class InsightsBookingBaseService { options: Extract ): Promise { const teamRepo = new TeamRepository(this.prisma); - - if (options.orgId) { - // team under org - const childTeamOfOrg = await teamRepo.findByIdAndParentId({ - id: options.teamId, - parentId: options.orgId, - select: { id: true }, - }); - if (!childTeamOfOrg) { - // teamId and its orgId does not match - return NOTHING_CONDITION; - } - } else { - // standalone team - const team = await teamRepo.findById({ - id: options.teamId, - }); - if (team?.parentId) { - // a team without orgId is not supposed to have parentId - return NOTHING_CONDITION; - } + const childTeamOfOrg = await teamRepo.findByIdAndParentId({ + id: options.teamId, + parentId: options.orgId, + select: { id: true }, + }); + if (options.orgId && !childTeamOfOrg) { + return NOTHING_CONDITION; } const usersFromTeam = await MembershipRepository.findAllByTeamIds({ @@ -479,17 +449,7 @@ export class InsightsBookingBaseService { }); } - async getCsvData({ - limit = 100, - offset = 0, - timeZone, - }: { - limit?: number; - offset?: number; - timeZone: string; - }) { - const DATE_FORMAT = "YYYY-MM-DD"; - const TIME_FORMAT = "HH:mm:ss"; + async getCsvData({ limit = 100, offset = 0 }: { limit?: number; offset?: number }) { const baseConditions = await this.getBaseConditions(); // Get total count first @@ -638,20 +598,8 @@ export class InsightsBookingBaseService { }) ); - // 6. Combine booking data with attendee data and add ISO timestamp columns + // 6. Combine booking data with attendee data const data = csvData.map((bookingTimeStatus) => { - const dateAndTime = { - createdAt: bookingTimeStatus.createdAt.toISOString(), - createdAt_date: dayjs(bookingTimeStatus.createdAt).tz(timeZone).format(DATE_FORMAT), - createdAt_time: dayjs(bookingTimeStatus.createdAt).tz(timeZone).format(TIME_FORMAT), - startTime: bookingTimeStatus.startTime.toISOString(), - startTime_date: dayjs(bookingTimeStatus.startTime).tz(timeZone).format(DATE_FORMAT), - startTime_time: dayjs(bookingTimeStatus.startTime).tz(timeZone).format(TIME_FORMAT), - endTime: bookingTimeStatus.endTime.toISOString(), - endTime_date: dayjs(bookingTimeStatus.endTime).tz(timeZone).format(DATE_FORMAT), - endTime_time: dayjs(bookingTimeStatus.endTime).tz(timeZone).format(TIME_FORMAT), - }; - if (!bookingTimeStatus.uid) { // should not be reached because we filtered above const nullAttendeeFields: Record = {}; @@ -661,7 +609,6 @@ export class InsightsBookingBaseService { return { ...bookingTimeStatus, - ...dateAndTime, noShowGuests: null, noShowGuestsCount: 0, ...nullAttendeeFields, @@ -678,7 +625,6 @@ export class InsightsBookingBaseService { return { ...bookingTimeStatus, - ...dateAndTime, noShowGuests: null, noShowGuestsCount: 0, ...nullAttendeeFields, @@ -687,7 +633,6 @@ export class InsightsBookingBaseService { return { ...bookingTimeStatus, - ...dateAndTime, noShowGuests: attendeeData.noShowGuests, noShowGuestsCount: attendeeData.noShowGuestsCount, ...Object.fromEntries(Object.entries(attendeeData).filter(([key]) => key.startsWith("attendee"))), @@ -918,42 +863,27 @@ export class InsightsBookingBaseService { return result; } - async getMembersStatsWithCount({ - type = "all", - sortOrder = "DESC", - completed, - }: { - type?: "all" | "accepted" | "cancelled" | "noShow"; - sortOrder?: "ASC" | "DESC"; - completed?: boolean; - } = {}): Promise { + async getMembersStatsWithCount( + type: "all" | "accepted" | "cancelled" | "noShow" = "all", + sortOrder: "ASC" | "DESC" = "DESC" + ): Promise { const baseConditions = await this.getBaseConditions(); - const conditions: Prisma.Sql[] = [Prisma.sql`"userId" IS NOT NULL`]; - + let additionalCondition = Prisma.sql``; if (type === "cancelled") { - conditions.push(Prisma.sql`status = 'cancelled'`); + additionalCondition = Prisma.sql`AND status = 'cancelled'`; } else if (type === "noShow") { - conditions.push(Prisma.sql`"noShowHost" = true`); + additionalCondition = Prisma.sql`AND "noShowHost" = true`; } else if (type === "accepted") { - conditions.push(Prisma.sql`status = 'accepted'`); - } - - if (completed) { - conditions.push(Prisma.sql`"endTime" <= NOW()`); + additionalCondition = Prisma.sql`AND status = 'accepted'`; } - const additionalCondition = conditions.reduce((acc, condition, index) => { - if (index === 0) return condition; - return Prisma.sql`(${acc}) AND (${condition})`; - }); - const query = Prisma.sql` SELECT "userId", COUNT(id)::int as count FROM "BookingTimeStatusDenormalized" - WHERE (${baseConditions}) AND (${additionalCondition}) + WHERE ${baseConditions} AND "userId" IS NOT NULL ${additionalCondition} GROUP BY "userId" ORDER BY count ${sortOrder === "ASC" ? Prisma.sql`ASC` : Prisma.sql`DESC`} LIMIT 10 @@ -1278,10 +1208,12 @@ export class InsightsBookingBaseService { } calculatePreviousPeriodDates() { - const result = extractDateRangeFromColumnFilters(this.filters?.columnFilters); - const startDate = dayjs(result.startDate); - const endDate = dayjs(result.endDate); + if (!this.filters?.dateRange) { + throw new Error("Date range is required for calculating previous period"); + } + const startDate = dayjs(this.filters.dateRange.startDate); + const endDate = dayjs(this.filters.dateRange.endDate); const startTimeEndTimeDiff = endDate.diff(startDate, "day"); const lastPeriodStartDate = startDate.subtract(startTimeEndTimeDiff, "day"); @@ -1296,12 +1228,13 @@ export class InsightsBookingBaseService { } private async isOwnerOrAdmin(userId: number, targetId: number): Promise { - const permissionCheckService = new PermissionCheckService(); - return await permissionCheckService.checkPermission({ - userId, - teamId: targetId, - permission: "insights.read", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); + // Check if the user is an owner or admin of the organization or team + const membership = await MembershipRepository.findUniqueByUserIdAndTeamId({ userId, teamId: targetId }); + return Boolean( + membership && + membership.accepted && + membership.role && + (membership.role === MembershipRole.OWNER || membership.role === MembershipRole.ADMIN) + ); } } diff --git a/packages/lib/server/service/InsightsRoutingBaseService.ts b/packages/lib/server/service/InsightsRoutingBaseService.ts index fa46d0b5887dc1..d410a1c2fe3b21 100644 --- a/packages/lib/server/service/InsightsRoutingBaseService.ts +++ b/packages/lib/server/service/InsightsRoutingBaseService.ts @@ -11,19 +11,19 @@ import { isSingleSelectFilterValue, } from "@calcom/features/data-table/lib/utils"; import type { DateRange } from "@calcom/features/insights/server/insightsDateUtils"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import type { PrismaClient } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; import type { BookingStatus } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums"; +import { MembershipRepository } from "../repository/membership"; import { TeamRepository } from "../repository/team"; export const insightsRoutingServiceOptionsSchema = z.discriminatedUnion("scope", [ z.object({ scope: z.literal("user"), userId: z.number(), - orgId: z.number().nullish().optional(), + orgId: z.number(), }), z.object({ scope: z.literal("org"), @@ -33,7 +33,7 @@ export const insightsRoutingServiceOptionsSchema = z.discriminatedUnion("scope", z.object({ scope: z.literal("team"), userId: z.number(), - orgId: z.number().nullish().optional(), + orgId: z.number(), teamId: z.number(), }), ]); @@ -41,8 +41,8 @@ export const insightsRoutingServiceOptionsSchema = z.discriminatedUnion("scope", export type InsightsRoutingServicePublicOptions = { scope: "user" | "org" | "team"; userId: number; - orgId: number | null | undefined; - teamId?: number; + orgId: number | null; + teamId: number | undefined; }; export type InsightsRoutingServiceOptions = z.infer; @@ -852,40 +852,27 @@ export class InsightsRoutingBaseService { options: Extract ): Promise { const teamRepo = new TeamRepository(this.prisma); - - if (options.orgId) { - // team under org - const childTeamOfOrg = await teamRepo.findByIdAndParentId({ - id: options.teamId, - parentId: options.orgId, - select: { id: true }, - }); - if (!childTeamOfOrg) { - // teamId and its orgId does not match - return NOTHING_CONDITION; - } - } else { - // standalone team - const team = await teamRepo.findById({ - id: options.teamId, - }); - if (team?.parentId) { - // a team without orgId is not supposed to have parentId - return NOTHING_CONDITION; - } + const childTeamOfOrg = await teamRepo.findByIdAndParentId({ + id: options.teamId, + parentId: options.orgId, + select: { id: true }, + }); + if (options.orgId && !childTeamOfOrg) { + return NOTHING_CONDITION; } return Prisma.sql`rfrd."formTeamId" = ${options.teamId}`; } private async isOwnerOrAdmin(userId: number, targetId: number): Promise { - const permissionCheckService = new PermissionCheckService(); - return await permissionCheckService.checkPermission({ - userId, - teamId: targetId, - permission: "insights.read", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); + // Check if the user is an owner or admin of the organization or team + const membership = await MembershipRepository.findUniqueByUserIdAndTeamId({ userId, teamId: targetId }); + return Boolean( + membership && + membership.accepted && + membership.role && + (membership.role === MembershipRole.OWNER || membership.role === MembershipRole.ADMIN) + ); } private buildFormFieldSqlCondition(fieldId: string, filterValue: FilterValue): Prisma.Sql | null { diff --git a/packages/lib/server/service/__tests__/InsightsBookingService.integration-test.ts b/packages/lib/server/service/__tests__/InsightsBookingService.integration-test.ts index e1059a47e16fb7..6c08f7ac837d07 100644 --- a/packages/lib/server/service/__tests__/InsightsBookingService.integration-test.ts +++ b/packages/lib/server/service/__tests__/InsightsBookingService.integration-test.ts @@ -6,10 +6,7 @@ import type { Team, User, Membership } from "@calcom/prisma/client"; import { Prisma } from "@calcom/prisma/client"; import { BookingStatus, MembershipRole } from "@calcom/prisma/enums"; -import { - InsightsBookingBaseService as InsightsBookingService, - type InsightsBookingServicePublicOptions, -} from "../InsightsBookingBaseService"; +import { InsightsBookingBaseService as InsightsBookingService } from "../InsightsBookingBaseService"; const NOTHING_CONDITION = Prisma.sql`1=0`; @@ -207,7 +204,7 @@ describe("InsightsBookingService Integration Tests", () => { it("should return NOTHING for invalid options", async () => { const service = new InsightsBookingService({ prisma, - options: null as unknown as InsightsBookingServicePublicOptions, + options: null as any, }); const conditions = await service.getAuthorizationConditions(); @@ -348,170 +345,6 @@ describe("InsightsBookingService Integration Tests", () => { await testData.cleanup(); }); - - it("should build user scope conditions with null orgId", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const service = new InsightsBookingService({ - prisma, - options: { - scope: "user", - userId: testData.user.id, - orgId: null, - }, - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual(Prisma.sql`("userId" = ${testData.user.id}) AND ("teamId" IS NULL)`); - - await testData.cleanup(); - }); - - it("should build user scope conditions with undefined orgId", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const service = new InsightsBookingService({ - prisma, - options: { - scope: "user", - userId: testData.user.id, - orgId: null, - }, - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual(Prisma.sql`("userId" = ${testData.user.id}) AND ("teamId" IS NULL)`); - - await testData.cleanup(); - }); - - it("should build team scope conditions with null orgId for standalone team", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const standaloneTeam = await prisma.team.create({ - data: { - name: "Standalone Team", - slug: `standalone-team-${Date.now()}-${Math.random().toString(36).substring(7)}`, - isOrganization: false, - parentId: null, - }, - }); - - await prisma.membership.create({ - data: { - userId: testData.user.id, - teamId: standaloneTeam.id, - role: MembershipRole.OWNER, - accepted: true, - }, - }); - - const service = new InsightsBookingService({ - prisma, - options: { - scope: "team", - userId: testData.user.id, - orgId: null, - teamId: standaloneTeam.id, - }, - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual( - Prisma.sql`(("teamId" = ${standaloneTeam.id}) AND ("isTeamBooking" = true)) OR (("userId" = ANY(${[ - testData.user.id, - ]})) AND ("isTeamBooking" = false))` - ); - - await prisma.membership.deleteMany({ - where: { teamId: standaloneTeam.id }, - }); - await prisma.team.delete({ - where: { id: standaloneTeam.id }, - }); - await testData.cleanup(); - }); - - it("should return NOTHING_CONDITION for team scope when team belongs to org but no orgId provided", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const service = new InsightsBookingService({ - prisma, - options: { - scope: "team", - userId: testData.user.id, - orgId: null, - teamId: testData.team.id, - }, - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual(NOTHING_CONDITION); - - await testData.cleanup(); - }); - - it("should build team scope conditions with undefined orgId for standalone team", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const standaloneTeam = await prisma.team.create({ - data: { - name: "Standalone Team 2", - slug: `standalone-team-2-${Date.now()}-${Math.random().toString(36).substring(7)}`, - isOrganization: false, - parentId: null, - }, - }); - - await prisma.membership.create({ - data: { - userId: testData.user.id, - teamId: standaloneTeam.id, - role: MembershipRole.OWNER, - accepted: true, - }, - }); - - const service = new InsightsBookingService({ - prisma, - options: { - scope: "team", - userId: testData.user.id, - orgId: null, - teamId: standaloneTeam.id, - }, - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual( - Prisma.sql`(("teamId" = ${standaloneTeam.id}) AND ("isTeamBooking" = true)) OR (("userId" = ANY(${[ - testData.user.id, - ]})) AND ("isTeamBooking" = false))` - ); - - await prisma.membership.deleteMany({ - where: { teamId: standaloneTeam.id }, - }); - await prisma.team.delete({ - where: { id: standaloneTeam.id }, - }); - await testData.cleanup(); - }); }); describe("Filter Conditions", () => { diff --git a/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts b/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts index a1f320307d8090..25b222b62868ef 100644 --- a/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts +++ b/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts @@ -8,10 +8,7 @@ import type { Team, User, Membership } from "@calcom/prisma/client"; import { Prisma } from "@calcom/prisma/client"; import { BookingStatus, MembershipRole } from "@calcom/prisma/enums"; -import { - InsightsRoutingBaseService as InsightsRoutingService, - type InsightsRoutingServicePublicOptions, -} from "../../service/InsightsRoutingBaseService"; +import { InsightsRoutingBaseService as InsightsRoutingService } from "../../service/InsightsRoutingBaseService"; // SQL condition constants for testing const NOTHING_CONDITION = Prisma.sql`1=0`; @@ -254,7 +251,7 @@ describe("InsightsRoutingService Integration Tests", () => { it("should return NOTHING for invalid options", async () => { const service = new InsightsRoutingService({ prisma, - options: null as unknown as InsightsRoutingServicePublicOptions, + options: null as any, filters: createDefaultFilters(), }); @@ -333,56 +330,6 @@ describe("InsightsRoutingService Integration Tests", () => { await testData.cleanup(); }); - it("should build user scope conditions with null orgId", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const service = new InsightsRoutingService({ - prisma, - options: { - scope: "user", - userId: testData.user.id, - orgId: null, - teamId: undefined, - }, - filters: createDefaultFilters(), - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual( - Prisma.sql`rfrd."formUserId" = ${testData.user.id} AND rfrd."formTeamId" IS NULL` - ); - - await testData.cleanup(); - }); - - it("should build user scope conditions with undefined orgId", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const service = new InsightsRoutingService({ - prisma, - options: { - scope: "user", - userId: testData.user.id, - orgId: null, - teamId: undefined, - }, - filters: createDefaultFilters(), - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual( - Prisma.sql`rfrd."formUserId" = ${testData.user.id} AND rfrd."formTeamId" IS NULL` - ); - - await testData.cleanup(); - }); - it("should build team scope conditions", async () => { const testData = await createTestData({ teamRole: MembershipRole.OWNER, @@ -479,123 +426,6 @@ describe("InsightsRoutingService Integration Tests", () => { }); await testData.cleanup(); }); - - it("should build team scope conditions with null orgId for standalone team", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const standaloneTeam = await prisma.team.create({ - data: { - name: "Standalone Team", - slug: `standalone-team-${randomUUID()}`, - isOrganization: false, - parentId: null, - }, - }); - - await prisma.membership.create({ - data: { - userId: testData.user.id, - teamId: standaloneTeam.id, - role: MembershipRole.OWNER, - accepted: true, - }, - }); - - const service = new InsightsRoutingService({ - prisma, - options: { - scope: "team", - userId: testData.user.id, - orgId: null, - teamId: standaloneTeam.id, - }, - filters: createDefaultFilters(), - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual(Prisma.sql`rfrd."formTeamId" = ${standaloneTeam.id}`); - - await prisma.membership.deleteMany({ - where: { teamId: standaloneTeam.id }, - }); - await prisma.team.delete({ - where: { id: standaloneTeam.id }, - }); - await testData.cleanup(); - }); - - it("should return NOTHING_CONDITION for team scope when team belongs to org but no orgId provided", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const service = new InsightsRoutingService({ - prisma, - options: { - scope: "team", - userId: testData.user.id, - orgId: null, - teamId: testData.team.id, - }, - filters: createDefaultFilters(), - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual(NOTHING_CONDITION); - - await testData.cleanup(); - }); - - it("should build team scope conditions with undefined orgId for standalone team", async () => { - const testData = await createTestData({ - teamRole: MembershipRole.OWNER, - orgRole: MembershipRole.OWNER, - }); - - const standaloneTeam = await prisma.team.create({ - data: { - name: "Standalone Team 2", - slug: `standalone-team-2-${randomUUID()}`, - isOrganization: false, - parentId: null, - }, - }); - - await prisma.membership.create({ - data: { - userId: testData.user.id, - teamId: standaloneTeam.id, - role: MembershipRole.OWNER, - accepted: true, - }, - }); - - const service = new InsightsRoutingService({ - prisma, - options: { - scope: "team", - userId: testData.user.id, - orgId: null, - teamId: standaloneTeam.id, - }, - filters: createDefaultFilters(), - }); - - const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual(Prisma.sql`rfrd."formTeamId" = ${standaloneTeam.id}`); - - await prisma.membership.deleteMany({ - where: { teamId: standaloneTeam.id }, - }); - await prisma.team.delete({ - where: { id: standaloneTeam.id }, - }); - await testData.cleanup(); - }); }); describe("Filter Conditions", () => { diff --git a/packages/lib/server/service/teamService.ts b/packages/lib/server/service/teamService.ts index 64c947a0584b3a..c33320ca4df601 100644 --- a/packages/lib/server/service/teamService.ts +++ b/packages/lib/server/service/teamService.ts @@ -218,7 +218,7 @@ export class TeamService { } // TODO: Needs to be moved to repository - static async fetchTeamOrThrow(teamId: number): Promise { + private static async fetchTeamOrThrow(teamId: number): Promise { const team = await prisma.team.findUnique({ where: { id: teamId }, select: { diff --git a/packages/lib/server/service/userCreationService.test.ts b/packages/lib/server/service/userCreationService.test.ts index 9f32c9c3e8550c..006af47701ae3a 100644 --- a/packages/lib/server/service/userCreationService.test.ts +++ b/packages/lib/server/service/userCreationService.test.ts @@ -2,8 +2,8 @@ import prismock from "../../../../tests/libs/__mocks__/prisma"; import { describe, test, expect, vi, beforeEach } from "vitest"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { CreationSource } from "@calcom/prisma/enums"; import { UserRepository } from "../repository/user"; @@ -20,7 +20,7 @@ vi.mock("@calcom/lib/server/i18n", () => { }; }); -vi.mock("@calcom/lib/auth/hashPassword", () => ({ +vi.mock("@calcom/features/auth/lib/hashPassword", () => ({ hashPassword: vi.fn().mockResolvedValue("hashed-password"), })); diff --git a/packages/lib/server/service/userCreationService.ts b/packages/lib/server/service/userCreationService.ts index 20d6ecd071ddb4..9d149951c0f692 100644 --- a/packages/lib/server/service/userCreationService.ts +++ b/packages/lib/server/service/userCreationService.ts @@ -1,5 +1,5 @@ +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { CreationSource, UserPermissionRole, IdentityProvider } from "@calcom/prisma/enums"; diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 119dff6bdb9549..4db4ff722c3645 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -162,8 +162,6 @@ export const buildEventType = (eventType?: Partial): EventType => { restrictionScheduleId: null, useBookerTimezone: false, bookingRequiresAuthentication: false, - createdAt: null, - updatedAt: null, ...eventType, }; }; diff --git a/packages/app-store/videoClient.ts b/packages/lib/videoClient.ts similarity index 100% rename from packages/app-store/videoClient.ts rename to packages/lib/videoClient.ts diff --git a/packages/platform/atoms/CHANGELOG.md b/packages/platform/atoms/CHANGELOG.md index ff2b9a157f868a..859f5cf5103815 100644 --- a/packages/platform/atoms/CHANGELOG.md +++ b/packages/platform/atoms/CHANGELOG.md @@ -1,15 +1,5 @@ ## 1.1.2 -## 1.9.0 - -### Minor Changes - -- [#23840](https://github.com/calcom/cal.com/pull/23840) [`63740c0`](https://github.com/calcom/cal.com/commit/63740c02c752f89a90d8373fb19c09e3f0da935f) Thanks [@Ryukemeister](https://github.com/Ryukemeister)! - This PR adds a new atom called `CalendarView` which is a read only calendar view component for a user. - -### Patch Changes - -- [#23891](https://github.com/calcom/cal.com/pull/23891) [`4f114ef`](https://github.com/calcom/cal.com/commit/4f114ef8d3b241394976ed930be70197eff6431d) Thanks [@supalarry](https://github.com/supalarry)! - fix: EventTypeSettings Checkbox booking field label - ## 1.8.0 ### Minor Changes diff --git a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx index 65a90e72093fff..16522116bf1009 100644 --- a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx +++ b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { useQueryClient } from "@tanstack/react-query"; // eslint-disable-next-line no-restricted-imports import debounce from "lodash/debounce"; @@ -16,12 +15,11 @@ import { import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout"; import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet"; -import { usePrefetch } from "@calcom/features/bookings/Booker/components/hooks/usePrefetch"; import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; import { useTimePreferences } from "@calcom/features/bookings/lib"; import { useTimesForSchedule } from "@calcom/features/schedules/lib/use-schedule/useTimesForSchedule"; import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; import type { ConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars"; import { localStorage } from "@calcom/lib/webstorage"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; @@ -94,7 +92,6 @@ const BookerPlatformWrapperComponent = ( ); const prevStateRef = useRef(null); const bookerStoreContext = useContext(BookerStoreContext); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const getStateValues = useCallback((state: any): BookerStoreValues => { return Object.fromEntries( Object.entries(state).filter(([_, value]) => typeof value !== "function") @@ -150,12 +147,10 @@ const BookerPlatformWrapperComponent = ( useEffect(() => { setSelectedDuration(props.duration ?? null); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.duration]); useEffect(() => { setOrg(props.entity?.orgSlug ?? null); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.entity?.orgSlug]); const isDynamic = useMemo(() => { @@ -228,7 +223,6 @@ const BookerPlatformWrapperComponent = ( name: prefillFormParamName, guests: defaultGuests ?? [], }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultName, defaultGuests]); const extraOptions = useMemo(() => { @@ -236,12 +230,23 @@ const BookerPlatformWrapperComponent = ( }, [restFormValues]); const date = dayjs(selectedDate).format("YYYY-MM-DD"); - const { prefetchNextMonth, monthCount } = usePrefetch({ - date, - month, - bookerLayout, - bookerState, - }); + const prefetchNextMonth = + (bookerLayout.layout === BookerLayouts.WEEK_VIEW && + !!bookerLayout.extraDays && + dayjs(date).month() !== dayjs(date).add(bookerLayout.extraDays, "day").month()) || + (bookerLayout.layout === BookerLayouts.COLUMN_VIEW && + dayjs(date).month() !== dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month()) || + ((bookerLayout.layout === BookerLayouts.MONTH_VIEW || bookerLayout.layout === "mobile") && + (!dayjs(date).isValid() || dayjs().isSame(dayjs(month), "month")) && + dayjs().isAfter(dayjs(month).startOf("month").add(2, "week"))); + + const monthCount = + ((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") || + bookerLayout.layout === BookerLayouts.COLUMN_VIEW) && + dayjs(date).add(1, "month").month() !== + dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month() + ? 2 + : undefined; const { timezone } = useTimePreferences(); const [calculatedStartTime, calculatedEndTime] = useTimesForSchedule({ @@ -465,7 +470,6 @@ const BookerPlatformWrapperComponent = ( ); useEffect(() => { setSelectedDate({ date: selectedDateProp, omitUpdatingParams: true }); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDateProp]); useEffect(() => { @@ -595,7 +599,7 @@ export const BookerPlatformWrapper = ( ); }; -export function formatUsername(username: string | string[]): string { +function formatUsername(username: string | string[]): string { if (typeof username === "string") { return username; } diff --git a/packages/platform/atoms/booker/BookerWebWrapper.tsx b/packages/platform/atoms/booker/BookerWebWrapper.tsx index 5770ca9dfb9bb2..19607722f77ae6 100644 --- a/packages/platform/atoms/booker/BookerWebWrapper.tsx +++ b/packages/platform/atoms/booker/BookerWebWrapper.tsx @@ -21,7 +21,6 @@ import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hoo import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm"; import { useBookings } from "@calcom/features/bookings/Booker/components/hooks/useBookings"; import { useCalendars } from "@calcom/features/bookings/Booker/components/hooks/useCalendars"; -import { usePrefetch } from "@calcom/features/bookings/Booker/components/hooks/usePrefetch"; import { useSlots } from "@calcom/features/bookings/Booker/components/hooks/useSlots"; import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode"; import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail"; @@ -32,6 +31,7 @@ import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEv import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { localStorage } from "@calcom/lib/webstorage"; +import { BookerLayouts } from "@calcom/prisma/zod-utils"; export type BookerWebWrapperAtomProps = BookerProps & { eventData?: NonNullable>>; @@ -139,12 +139,23 @@ const BookerPlatformWrapperComponent = (props: BookerWebWrapperAtomProps) => { const isEmbed = useIsEmbed(); - const { prefetchNextMonth, monthCount } = usePrefetch({ - date, - month, - bookerLayout, - bookerState, - }); + const prefetchNextMonth = + (bookerLayout.layout === BookerLayouts.WEEK_VIEW && + !!bookerLayout.extraDays && + dayjs(date).month() !== dayjs(date).add(bookerLayout.extraDays, "day").month()) || + (bookerLayout.layout === BookerLayouts.COLUMN_VIEW && + dayjs(date).month() !== dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month()) || + ((bookerLayout.layout === BookerLayouts.MONTH_VIEW || bookerLayout.layout === "mobile") && + (!dayjs(date).isValid() || dayjs().isSame(dayjs(month), "month")) && + dayjs().isAfter(dayjs(month).startOf("month").add(2, "week"))); + + const monthCount = + ((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") || + bookerLayout.layout === BookerLayouts.COLUMN_VIEW) && + dayjs(date).add(1, "month").month() !== + dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month() + ? 2 + : undefined; /** * Prioritize dateSchedule load * Component will render but use data already fetched from here, and no duplicate requests will be made @@ -216,7 +227,6 @@ const BookerPlatformWrapperComponent = (props: BookerWebWrapperAtomProps) => { useEffect(() => { if (hasSession) onOverlaySwitchStateChange(true); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasSession]); return ( diff --git a/packages/platform/atoms/calendar-view/index.ts b/packages/platform/atoms/calendar-view/index.ts deleted file mode 100644 index 57f223bbecee11..00000000000000 --- a/packages/platform/atoms/calendar-view/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CalendarViewPlatformWrapper } from "./wrappers/CalendarViewPlatformWrapper"; diff --git a/packages/platform/atoms/calendar-view/wrappers/CalendarViewPlatformWrapper.tsx b/packages/platform/atoms/calendar-view/wrappers/CalendarViewPlatformWrapper.tsx deleted file mode 100644 index a94855f0bc6b15..00000000000000 --- a/packages/platform/atoms/calendar-view/wrappers/CalendarViewPlatformWrapper.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { AtomsWrapper } from "@/components/atoms-wrapper"; -import { useMemo, useEffect, useState } from "react"; -import { shallow } from "zustand/shallow"; - -import dayjs from "@calcom/dayjs"; -import { - BookerStoreProvider, - useBookerStoreContext, - useInitializeBookerStoreContext, -} from "@calcom/features/bookings/Booker/BookerStoreProvider"; -import { Header } from "@calcom/features/bookings/Booker/components/Header"; -import { BookerSection } from "@calcom/features/bookings/Booker/components/Section"; -import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout"; -import { useTimePreferences } from "@calcom/features/bookings/lib"; -import { LargeCalendar } from "@calcom/features/calendar-view/LargeCalendar"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; -import { useTimesForSchedule } from "@calcom/features/schedules/lib/use-schedule/useTimesForSchedule"; -import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams"; -import { BookerLayouts } from "@calcom/prisma/zod-utils"; - -import { formatUsername } from "../../booker/BookerPlatformWrapper"; -import type { - BookerPlatformWrapperAtomPropsForIndividual, - BookerPlatformWrapperAtomPropsForTeam, -} from "../../booker/types"; -import { useGetBookingForReschedule } from "../../hooks/bookings/useGetBookingForReschedule"; -import { useAtomGetPublicEvent } from "../../hooks/event-types/public/useAtomGetPublicEvent"; -import { useEventType } from "../../hooks/event-types/public/useEventType"; -import { useTeamEventType } from "../../hooks/event-types/public/useTeamEventType"; -import { useAvailableSlots } from "../../hooks/useAvailableSlots"; - -const CalendarViewPlatformWrapperComponent = ( - props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam -) => { - const { - eventSlug, - isTeamEvent, - hostsLimit, - allowUpdatingUrlParams = false, - teamMemberEmail, - crmAppSlug, - crmOwnerRecordType, - view = "MONTH_VIEW", - } = props; - - const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined; - const username = useMemo(() => { - if (props.username) { - return formatUsername(props.username); - } - return ""; - }, [props.username]); - - const { isPending } = useEventType(username, eventSlug, isTeamEvent); - const { isPending: isTeamPending } = useTeamEventType(teamId, eventSlug, isTeamEvent, hostsLimit); - - const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration); - const selectedDuration = useBookerStoreContext((state) => state.selectedDuration); - - const event = useAtomGetPublicEvent({ - username, - eventSlug: props.eventSlug, - isTeamEvent: props.isTeamEvent, - teamId, - selectedDuration, - }); - - const bookerLayout = useBookerLayout(event.data?.profile?.bookerLayouts); - - const [bookerState, _setBookerState] = useBookerStoreContext( - (state) => [state.state, state.setState], - shallow - ); - const selectedDate = useBookerStoreContext((state) => state.selectedDate); - const date = dayjs(selectedDate).format("YYYY-MM-DD"); - const monthCount = - ((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") || - bookerLayout.layout === BookerLayouts.COLUMN_VIEW) && - dayjs(date).add(1, "month").month() !== - dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month() - ? 2 - : undefined; - const month = useBookerStoreContext((state) => state.month); - const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow); - - const prefetchNextMonth = - (bookerLayout.layout === BookerLayouts.WEEK_VIEW && - !!bookerLayout.extraDays && - dayjs(date).month() !== dayjs(date).add(bookerLayout.extraDays, "day").month()) || - (bookerLayout.layout === BookerLayouts.COLUMN_VIEW && - dayjs(date).month() !== dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month()); - - const [startTime, endTime] = useTimesForSchedule({ - month, - monthCount, - dayCount, - prefetchNextMonth, - selectedDate, - }); - - useEffect(() => { - setSelectedDuration(props.duration ?? null); - }, [props.duration, setSelectedDuration]); - - const { timezone } = useTimePreferences(); - const isDynamic = useMemo(() => { - return getUsernameList(username ?? "").length > 1; - }, [username]); - - const [routingParams, setRoutingParams] = useState<{ - routedTeamMemberIds?: number[]; - _shouldServeCache?: boolean; - skipContactOwner?: boolean; - isBookingDryRun?: boolean; - }>({}); - - useEffect(() => { - const searchParams = props.routingFormSearchParams - ? new URLSearchParams(props.routingFormSearchParams) - : new URLSearchParams(window.location.search); - - const routedTeamMemberIds = getRoutedTeamMemberIdsFromSearchParams(searchParams); - const skipContactOwner = searchParams.get("cal.skipContactOwner") === "true"; - - const _cacheParam = searchParams?.get("cal.cache"); - const _shouldServeCache = _cacheParam ? _cacheParam === "true" : undefined; - const isBookingDryRun = - searchParams?.get("cal.isBookingDryRun")?.toLowerCase() === "true" || - searchParams?.get("cal.sandbox")?.toLowerCase() === "true"; - setRoutingParams({ - ...(skipContactOwner ? { skipContactOwner } : {}), - ...(routedTeamMemberIds ? { routedTeamMemberIds } : {}), - ...(_shouldServeCache ? { _shouldServeCache } : {}), - ...(isBookingDryRun ? { isBookingDryRun } : {}), - }); - }, [props.routingFormSearchParams]); - const bookingData = useBookerStoreContext((state) => state.bookingData); - const setBookingData = useBookerStoreContext((state) => state.setBookingData); - const layout = BookerLayouts[view]; - - useGetBookingForReschedule({ - uid: props.rescheduleUid ?? props.bookingUid ?? "", - onSuccess: (data) => { - setBookingData(data); - }, - }); - - useInitializeBookerStoreContext({ - ...props, - teamMemberEmail, - crmAppSlug, - crmOwnerRecordType, - crmRecordId: props.crmRecordId, - eventId: event?.data?.id, - rescheduleUid: props.rescheduleUid ?? null, - bookingUid: props.bookingUid ?? null, - layout: layout, - org: props.entity?.orgSlug, - username, - bookingData, - isPlatform: true, - allowUpdatingUrlParams, - }); - - const schedule = useAvailableSlots({ - usernameList: getUsernameList(username), - eventTypeId: event?.data?.id ?? 0, - startTime, - endTime, - timeZone: timezone, - duration: selectedDuration ?? undefined, - rescheduleUid: props.rescheduleUid, - teamMemberEmail: props.teamMemberEmail ?? undefined, - ...(props.isTeamEvent - ? { - isTeamEvent: props.isTeamEvent, - teamId: teamId, - } - : {}), - enabled: - Boolean(teamId || username) && - Boolean(month) && - Boolean(timezone) && - (props.isTeamEvent ? !isTeamPending : !isPending) && - Boolean(event?.data?.id), - orgSlug: props.entity?.orgSlug ?? undefined, - eventTypeSlug: isDynamic ? "dynamic" : eventSlug || "", - ...routingParams, - }); - - return ( - - -
- - - - - - ); -}; - -export const CalendarViewPlatformWrapper = ( - props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam -) => { - return ( - - - - ); -}; diff --git a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts index 3ea864c0e08ca5..03b64c6b490b18 100644 --- a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts +++ b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts @@ -4,7 +4,6 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps"; -import { locationsResolver } from "@calcom/app-store/locations"; import { DEFAULT_PROMPT_VALUE, DEFAULT_BEGIN_MESSAGE } from "@calcom/features/calAIPhone/promptTemplates"; import type { TemplateType } from "@calcom/features/calAIPhone/zod-utils"; import { sortHosts } from "@calcom/features/eventtypes/components/HostEditDialogs"; @@ -14,6 +13,7 @@ import type { EventTypeSetupProps, EventTypeUpdateInput, } from "@calcom/features/eventtypes/lib/types"; +import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder"; import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts"; diff --git a/packages/platform/atoms/event-types/hooks/useHandleRouteChange.ts b/packages/platform/atoms/event-types/hooks/useHandleRouteChange.ts index ef85c7ed97e30d..b1256b48fca66d 100644 --- a/packages/platform/atoms/event-types/hooks/useHandleRouteChange.ts +++ b/packages/platform/atoms/event-types/hooks/useHandleRouteChange.ts @@ -5,7 +5,7 @@ import type { EventTypeAssignedUsers, EventTypeHosts, } from "@calcom/features/eventtypes/components/EventType"; -import { checkForEmptyAssignment } from "@calcom/features/eventtypes/lib/checkForEmptyAssignment"; +import { checkForEmptyAssignment } from "@calcom/lib/event-types/utils/checkForEmptyAssignment"; export const useHandleRouteChange = ({ isTeamEventTypeDeleted, diff --git a/packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx b/packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx index c5d27b56cdde2c..c36cbaaefc8b79 100644 --- a/packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx +++ b/packages/platform/atoms/event-types/hooks/useTabsNavigations.tsx @@ -24,7 +24,6 @@ type Props = { team: EventTypeSetupProps["team"]; eventTypeApps?: EventTypeApps; allActiveWorkflows?: Workflow[]; - canReadWorkflows?: boolean; }; export const useTabsNavigations = ({ formMethods, @@ -32,7 +31,6 @@ export const useTabsNavigations = ({ team, eventTypeApps, allActiveWorkflows, - canReadWorkflows = false, }: Props) => { const { t } = useLocale(); @@ -81,7 +79,6 @@ export const useTabsNavigations = ({ installedAppsNumber, enabledWorkflowsNumber, availability, - canReadWorkflows, }); if (!requirePayment) { @@ -169,7 +166,6 @@ export const useTabsNavigations = ({ watchSchedulingType, watchChildrenCount, activeWebhooksNumber, - canReadWorkflows, ]); return { tabsNavigation: EventTypeTabs }; @@ -184,7 +180,6 @@ type getNavigationProps = { enabledWorkflowsNumber: number; installedAppsNumber: number; availability: AvailabilityOption | undefined; - canReadWorkflows: boolean; }; function getNavigation({ @@ -195,11 +190,10 @@ function getNavigation({ enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, - canReadWorkflows, }: getNavigationProps) { const duration = multipleDuration?.map((duration) => ` ${duration}`) || length; - const baseNavigation: VerticalTabItemProps[] = [ + return [ { name: t("basics"), href: `/event-types/${id}?tabName=setup`, @@ -229,18 +223,12 @@ function getNavigation({ info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`, "data-testid": "apps", }, - ]; - - // Only add workflows tab if user has permission to read workflows - if (canReadWorkflows) { - baseNavigation.push({ + { name: t("workflows"), href: `/event-types/${id}?tabName=workflows`, icon: "zap", info: `${enabledWorkflowsNumber} ${t("active")}`, "data-testid": "workflows", - }); - } - - return baseNavigation; + }, + ] satisfies VerticalTabItemProps[]; } diff --git a/packages/platform/atoms/event-types/wrappers/EventPaymentsTabPlatformWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventPaymentsTabPlatformWrapper.tsx index 7447d8b665f057..c7e98cc0ecdafc 100644 --- a/packages/platform/atoms/event-types/wrappers/EventPaymentsTabPlatformWrapper.tsx +++ b/packages/platform/atoms/event-types/wrappers/EventPaymentsTabPlatformWrapper.tsx @@ -4,8 +4,8 @@ import type { EventTypeForAppCard, } from "@calcom/app-store/_components/EventTypeAppCardInterface"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; -import useAppsData from "@calcom/features/apps/hooks/useAppsData"; import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types"; +import useAppsData from "@calcom/lib/hooks/useAppsData"; import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import { StripeConnect } from "../../connect/stripe/StripeConnect"; diff --git a/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx index fc9fa6443833ce..81634ae83ddcc1 100644 --- a/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx +++ b/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx @@ -3,7 +3,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { useRef, useState, useEffect, forwardRef, useImperativeHandle, useCallback } from "react"; -import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider"; import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect"; import { EventType as EventTypeComponent } from "@calcom/features/eventtypes/components/EventType"; import ManagedEventTypeDialog from "@calcom/features/eventtypes/components/dialogs/ManagedEventDialog"; @@ -418,22 +417,20 @@ export const EventTypePlatformWrapper = forwardRef< if (!eventTypeQueryData) return null; return ( - - - + ); }); diff --git a/packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx index d5f3c870906971..2a391b1bcf541f 100644 --- a/packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx +++ b/packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx @@ -9,8 +9,6 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect"; import { EventType as EventTypeComponent } from "@calcom/features/eventtypes/components/EventType"; import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types"; -import { EventPermissionProvider } from "@calcom/features/pbac/client/context/EventPermissionContext"; -import { useWorkflowPermission } from "@calcom/features/pbac/client/hooks/useEventPermission"; import { WEBSITE_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useTelemetry } from "@calcom/lib/hooks/useTelemetry"; @@ -31,21 +29,6 @@ import { useEventTypeForm } from "../hooks/useEventTypeForm"; import { useHandleRouteChange } from "../hooks/useHandleRouteChange"; import { useTabsNavigations } from "../hooks/useTabsNavigations"; -type EventPermissions = { - eventTypes: { - canRead: boolean; - canCreate: boolean; - canUpdate: boolean; - canDelete: boolean; - }; - workflows: { - canRead: boolean; - canCreate: boolean; - canUpdate: boolean; - canDelete: boolean; - }; -}; - const ManagedEventTypeDialog = dynamic( () => import("@calcom/features/eventtypes/components/dialogs/ManagedEventDialog") ); @@ -108,47 +91,21 @@ const EventAITab = dynamic(() => export type EventTypeWebWrapperProps = { id: number; data: RouterOutputs["viewer"]["eventTypes"]["get"]; - permissions?: EventPermissions; }; -export const EventTypeWebWrapper = ({ - id, - data: serverFetchedData, - permissions = { - eventTypes: { - canRead: false, - canCreate: false, - canUpdate: false, - canDelete: false, - }, - workflows: { - canRead: false, - canCreate: false, - canUpdate: false, - canDelete: false, - }, - }, -}: EventTypeWebWrapperProps) => { +export const EventTypeWebWrapper = ({ id, data: serverFetchedData }: EventTypeWebWrapperProps) => { const { data: eventTypeQueryData } = trpc.viewer.eventTypes.get.useQuery( { id }, { enabled: !serverFetchedData } ); if (serverFetchedData) { - return ( - - - - ); + return ; } if (!eventTypeQueryData) return null; - return ( - - - - ); + return ; }; const EventTypeWeb = ({ @@ -174,9 +131,6 @@ const EventTypeWeb = ({ teamId: eventType.team?.id || eventType.parent?.teamId, onlyInstalled: true, }); - - // Check workflow permissions - const { hasPermission: canReadWorkflows } = useWorkflowPermission("canRead"); const updateMutation = trpc.viewer.eventTypes.heavy.update.useMutation({ onSuccess: async () => { const currentValues = form.getValues(); @@ -287,12 +241,11 @@ const EventTypeWeb = ({ instant: , recurring: , apps: , - workflows: - allActiveWorkflows && canReadWorkflows ? ( - - ) : ( - <> - ), + workflows: allActiveWorkflows ? ( + + ) : ( + <> + ), webhooks: , ai: , } as const; @@ -405,7 +358,6 @@ const EventTypeWeb = ({ team, eventTypeApps, allActiveWorkflows, - canReadWorkflows, }); return ( diff --git a/packages/platform/atoms/hooks/event-types/public/useAtomGetPublicEvent.tsx b/packages/platform/atoms/hooks/event-types/public/useAtomGetPublicEvent.tsx index 249af285d48768..5e5cd46e2908f4 100644 --- a/packages/platform/atoms/hooks/event-types/public/useAtomGetPublicEvent.tsx +++ b/packages/platform/atoms/hooks/event-types/public/useAtomGetPublicEvent.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; import { SUCCESS_STATUS, V2_ENDPOINTS } from "@calcom/platform-constants"; import type { PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent"; import type { ApiResponse } from "@calcom/platform-types"; diff --git a/packages/platform/atoms/hooks/event-types/public/useEventType.ts b/packages/platform/atoms/hooks/event-types/public/useEventType.ts index c2796bef53d74c..33dae8a8515480 100644 --- a/packages/platform/atoms/hooks/event-types/public/useEventType.ts +++ b/packages/platform/atoms/hooks/event-types/public/useEventType.ts @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; -import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; import { SUCCESS_STATUS, V2_ENDPOINTS } from "@calcom/platform-constants"; import type { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; import type { ApiResponse } from "@calcom/platform-types"; diff --git a/packages/platform/atoms/hooks/useOAuthFlow.ts b/packages/platform/atoms/hooks/useOAuthFlow.ts index 5c01471efa534d..d9ff7bed8b47a9 100644 --- a/packages/platform/atoms/hooks/useOAuthFlow.ts +++ b/packages/platform/atoms/hooks/useOAuthFlow.ts @@ -42,7 +42,7 @@ export const useOAuthFlow = ({ accessToken, refreshUrl, clientId, onError, onSuc setIsRefreshing(false); } - return Promise.reject(err); + return Promise.reject(err.response); }) : ""; diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index 7622eb877f3a52..9cad136808008d 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -41,4 +41,3 @@ export { useCreateTeamEventType } from "./hooks/event-types/private/useCreateTea export { useOrganizationBookings } from "./hooks/organizations/bookings/useOrganizationBookings"; export { useOrganizationUserBookings } from "./hooks/organizations/bookings/useOrganizationUserBookings"; -export { CalendarViewPlatformWrapper as CalendarView } from "./calendar-view/index"; diff --git a/packages/platform/atoms/package.json b/packages/platform/atoms/package.json index 6c05117534ed17..2cff48b1c82f2e 100644 --- a/packages/platform/atoms/package.json +++ b/packages/platform/atoms/package.json @@ -4,7 +4,7 @@ "type": "module", "description": "Customizable UI components to integrate scheduling into your product.", "authors": "Cal.com, Inc.", - "version": "1.9.0", + "version": "1.8.0", "scripts": { "dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch", "build": "NODE_OPTIONS='--max_old_space_size=12288' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../prisma/client/*.d.ts ./dist/packages/prisma-client", diff --git a/packages/platform/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper.tsx b/packages/platform/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper.tsx index bfa88f57896ebf..becd316df141c4 100644 --- a/packages/platform/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper.tsx +++ b/packages/platform/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper.tsx @@ -15,12 +15,12 @@ import { Dropdown, DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/c import { Switch } from "@calcom/ui/components/form"; import { List } from "@calcom/ui/components/list"; -import * as Connect from "../../connect"; import { AppleConnect } from "../../connect/apple/AppleConnect"; import { useAddSelectedCalendar } from "../../hooks/calendars/useAddSelectedCalendar"; import { useDeleteCalendarCredentials } from "../../hooks/calendars/useDeleteCalendarCredentials"; import { useRemoveSelectedCalendar } from "../../hooks/calendars/useRemoveSelectedCalendar"; import { useConnectedCalendars } from "../../hooks/useConnectedCalendars"; +import { Connect } from "../../index"; import { AtomsWrapper } from "../../src/components/atoms-wrapper"; import { useToast } from "../../src/components/ui/use-toast"; import { SelectedCalendarsSettings } from "../SelectedCalendarsSettings"; diff --git a/packages/platform/atoms/src/components/ui/dialog.tsx b/packages/platform/atoms/src/components/ui/dialog.tsx index 4259ae1a42c9c3..36753e97dfe11a 100644 --- a/packages/platform/atoms/src/components/ui/dialog.tsx +++ b/packages/platform/atoms/src/components/ui/dialog.tsx @@ -51,7 +51,7 @@ DialogOverlay.displayName = DialogPrimitives.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef + React.ComponentPropsWithoutRef & { enableOverflow?: boolean } >(({ className, children, ...props }, ref) => ( <> diff --git a/packages/platform/examples/base/package.json b/packages/platform/examples/base/package.json index d761c05f05c83b..3c7cc12cfbb711 100644 --- a/packages/platform/examples/base/package.json +++ b/packages/platform/examples/base/package.json @@ -26,7 +26,6 @@ "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", - "dotenv": "^17.2.1", "eslint": "^8.34.0", "eslint-config-next": "14.0.4", "postcss": "^8", diff --git a/packages/platform/examples/base/playwright.config.ts b/packages/platform/examples/base/playwright.config.ts index d77d9e71361d81..240045c3ebc533 100644 --- a/packages/platform/examples/base/playwright.config.ts +++ b/packages/platform/examples/base/playwright.config.ts @@ -1,10 +1,4 @@ import { defineConfig, devices } from "@playwright/test"; -import dotenv from "dotenv"; -import path from "path"; - -const envPath = process.env.CI ? path.resolve(__dirname, ".env") : path.resolve(__dirname, ".env.local"); - -dotenv.config({ path: envPath }); const DEFAULT_EXPECT_TIMEOUT = process.env.CI ? 30000 : 120000; const DEFAULT_TEST_TIMEOUT = process.env.CI ? 60000 : 240000; diff --git a/packages/platform/examples/base/src/components/Navbar/index.tsx b/packages/platform/examples/base/src/components/Navbar/index.tsx index 035de96cfb1d90..051ad00697595a 100644 --- a/packages/platform/examples/base/src/components/Navbar/index.tsx +++ b/packages/platform/examples/base/src/components/Navbar/index.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; const poppins = Poppins({ subsets: ["latin"], weight: ["400", "800"] }); -// eslint-disable-next-line @typescript-eslint/no-unused-vars export function Navbar({ username }: { username?: string }) { return (
+ {username &&
👤 {username}
}
    -
  • - Week View -
  • Calendar
  • diff --git a/packages/platform/examples/base/src/pages/calendar-view.tsx b/packages/platform/examples/base/src/pages/calendar-view.tsx deleted file mode 100644 index b80239632b648b..00000000000000 --- a/packages/platform/examples/base/src/pages/calendar-view.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Navbar } from "@/components/Navbar"; -import { Inter } from "next/font/google"; - -import { CalendarView } from "@calcom/atoms"; - -const inter = Inter({ subsets: ["latin"] }); - -export default function CalendarViewAtom(props: { calUsername: string; calEmail: string }) { - return ( -
    - -
    - -
    -
    - ); -} diff --git a/packages/platform/examples/base/src/pages/calendars.tsx b/packages/platform/examples/base/src/pages/calendars.tsx index d521ccf1c11d0f..2cd2e8d59a20cf 100644 --- a/packages/platform/examples/base/src/pages/calendars.tsx +++ b/packages/platform/examples/base/src/pages/calendars.tsx @@ -9,7 +9,7 @@ export default function Calendars(props: { calUsername: string; calEmail: string return (
    -
    +
    @@ -23,7 +19,7 @@ export default function Home(props: { calUsername: string; calEmail: string }) {

    To get started, connect your google calendar.

    -
    +
    { - router.push(`/calendars`); - }} isMultiCalendar={true} className="h-[40px] bg-gradient-to-r from-[#8A2387] via-[#E94057] to-[#F27121] text-center text-base font-semibold text-transparent text-white hover:bg-orange-700" /> diff --git a/packages/platform/examples/base/tests/connect-atoms/apple-connect.e2e.ts b/packages/platform/examples/base/tests/connect-atoms/apple-connect.e2e.ts deleted file mode 100644 index eed8380b0330ec..00000000000000 --- a/packages/platform/examples/base/tests/connect-atoms/apple-connect.e2e.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("connect calendar using the apple connect atom", async ({ page }) => { - const appleId = process.env.ATOMS_E2E_APPLE_ID; - const appSpecificPassword = process.env.ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE; - - await page.goto("/"); - - await expect(page.locator("body")).toBeVisible(); - - await expect(page.locator('[data-testid="connect-atoms"]')).toBeVisible(); - - await page.locator('[data-testid="connect-atoms"] button:has-text("Connect Apple Calendar")').click(); - - await expect(page.locator('[role="dialog"]')).toBeVisible(); - - await expect(page.locator('[role="dialog"] fieldset[data-testid="apple-calendar-form"]')).toBeVisible(); - - await expect(page.locator('[data-testid="apple-calendar-email"]')).toBeVisible(); - await page.locator('[data-testid="apple-calendar-email"]').fill(appleId ?? ""); - - await expect(page.locator('[data-testid="apple-calendar-password"]')).toBeVisible(); - await page.locator('[data-testid="apple-calendar-password"]').fill(appSpecificPassword ?? ""); - - await page.locator('[data-testid="apple-calendar-login-button"]').click(); - - await expect(page).toHaveURL("/calendars"); - - await expect(page.locator("body")).toBeVisible(); - - await expect(page.locator('[data-testid="calendars-settings-atom"]')).toBeVisible(); - - await expect(page.locator('h2:has-text("Add to calendar")')).toBeVisible(); - - await expect(page.locator('label:has-text("Add events to")')).toBeVisible(); - - await expect(page.locator('[data-testid="select-control"]')).toBeVisible(); - await page.locator('[data-testid="select-control"]').click(); - - await page.keyboard.press("ArrowDown"); - await page.keyboard.press("Enter"); - - await expect(page.locator('h4:has-text("Check for conflicts")')).toBeVisible(); - - await expect(page.locator('[data-testid="list"]')).toBeVisible(); - await page.locator('[data-testid="list"] button:has(svg[data-name="start-icon"])').click(); - await page.locator('[data-testid="dialog-rejection"]').click(); - - await expect(page.locator('[data-testid="list"] button[role="switch"]').first()).toBeVisible(); - await page.locator('[data-testid="list"] button[role="switch"]').first().click(); - - await expect(page.locator('[data-testid="list"] button[role="switch"]').last()).toBeVisible(); - await page.locator('[data-testid="list"] button[role="switch"]').last().click(); - - await expect(page.locator('[data-testid="list"] button[role="switch"]').first()).toBeVisible(); - await page.locator('[data-testid="list"] button[role="switch"]').first().click(); - - await page.locator('[data-testid="list"] button:has(svg[data-name="start-icon"])').click(); - await page.locator('[data-testid="dialog-confirmation"]').click(); -}); diff --git a/packages/platform/examples/base/tests/create-team-event-type-atom/create-team-event-type.e2e.ts b/packages/platform/examples/base/tests/create-team-event-type-atom/create-team-event-type.e2e.ts index 6253cbb963e1e0..85b4bc9cbe8341 100644 --- a/packages/platform/examples/base/tests/create-team-event-type-atom/create-team-event-type.e2e.ts +++ b/packages/platform/examples/base/tests/create-team-event-type-atom/create-team-event-type.e2e.ts @@ -2,8 +2,7 @@ import { test, expect } from "@playwright/test"; import { generateRandomText } from "../../src/lib/generateRandomText"; -// eslint-disable-next-line playwright/no-skipped-test -test.skip("create team event using CreateTeamEventTypeAtom", async ({ page }) => { +test("create team event using CreateTeamEventTypeAtom", async ({ page }) => { await page.goto("/"); await page.goto("/event-types"); diff --git a/packages/platform/libraries/conferencing.ts b/packages/platform/libraries/conferencing.ts index 2d1acd5c2a0429..b5bed7b4e8fcb2 100644 --- a/packages/platform/libraries/conferencing.ts +++ b/packages/platform/libraries/conferencing.ts @@ -2,4 +2,4 @@ export { getRecordingsOfCalVideoByRoomName, getDownloadLinkOfCalVideoByRecordingId, getAllTranscriptsAccessLinkFromRoomName, -} from "@calcom/app-store/videoClient"; +} from "@calcom/lib/videoClient"; diff --git a/packages/platform/libraries/event-types.ts b/packages/platform/libraries/event-types.ts index df62d149163606..2e71adcd9a30c1 100644 --- a/packages/platform/libraries/event-types.ts +++ b/packages/platform/libraries/event-types.ts @@ -1,15 +1,15 @@ -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; export { getPublicEvent, type PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent"; -export { getBulkUserEventTypes, getBulkTeamEventTypes } from "@calcom/app-store/_utils/getBulkEventTypes"; +export { getBulkUserEventTypes, getBulkTeamEventTypes } from "@calcom/lib/event-types/getBulkEventTypes"; export { createHandler as createEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/create.handler"; export { updateHandler as updateEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.handler"; export type { TUpdateInputSchema as TUpdateEventTypeInputSchema } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.schema"; -export type { EventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic"; -export { getEventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic"; +export type { EventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic"; +export { getEventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic"; export { parseEventTypeColor } from "@calcom/lib/isEventTypeColor"; export { @@ -20,12 +20,12 @@ export { export { validateCustomEventName } from "@calcom/features/eventtypes/lib/eventNaming"; export { EventManager }; -export { getEventTypeById } from "@calcom/features/eventtypes/lib/getEventTypeById"; -export { getEventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer"; -export type { EventType } from "@calcom/features/eventtypes/lib/getEventTypeById"; -export type { EventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer"; +export { getEventTypeById } from "@calcom/lib/event-types/getEventTypeById"; +export { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer"; +export type { EventType } from "@calcom/lib/event-types/getEventTypeById"; +export type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer"; export type { UpdateEventTypeReturn } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.handler"; -export { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; +export { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; export { bulkUpdateEventsToDefaultLocation } from "@calcom/app-store/_utils/bulkUpdateEventsToDefaultLocation"; export { bulkUpdateTeamEventsToDefaultLocation } from "@calcom/app-store/_utils/bulkUpdateTeamEventsToDefaultLocation"; diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 3c5e1ee85865f5..32c04f57eb01bf 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -4,7 +4,6 @@ import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo"; import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; import * as newBookingMethods from "@calcom/features/bookings/lib/handleNewBooking"; import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment"; -import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm"; import { verifyPhoneNumber, sendVerificationCode, @@ -15,6 +14,7 @@ import * as instantMeetingMethods from "@calcom/features/instant-meeting/handleI import { getRoutedUrl } from "@calcom/features/routing-forms/lib/getRoutedUrl"; import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings"; import { symmetricEncrypt, symmetricDecrypt } from "@calcom/lib/crypto"; +import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/lib/server/getTeamMemberEmailFromCrm"; import { getTranslation } from "@calcom/lib/server/i18n"; import type { Prisma } from "@calcom/prisma/client"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; @@ -39,7 +39,7 @@ export { WorkflowTemplates, } from "@calcom/prisma/enums"; -export { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; +export { getUsernameList } from "@calcom/lib/defaultEvents"; const handleNewBooking = newBookingMethods.default; export { handleNewBooking }; @@ -53,7 +53,7 @@ export { handleNewRecurringBooking } from "@calcom/features/bookings/lib/handleN export { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars"; -export { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager"; +export { getBusyCalendarTimes } from "@calcom/lib/CalendarManager"; export type { BookingCreateBody, BookingResponse } from "@calcom/features/bookings/types"; export { HttpError } from "@calcom/lib/http-error"; @@ -75,7 +75,7 @@ export { userMetadata, bookingMetadataSchema, teamMetadataSchema } from "@calcom export { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; export { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; -export { dynamicEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +export { dynamicEvent } from "@calcom/lib/defaultEvents"; export { symmetricEncrypt, symmetricDecrypt }; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts index 860bb6920e1b32..d6c9f3ff6c1554 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts @@ -478,7 +478,7 @@ class BaseCreateEventTypeInput { @DocsPropertyOptional({ default: false, description: - "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type.", + "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type.", }) bookingRequiresAuthentication?: boolean; } diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts index 62c0cb4a8bc3de..c91b85bc7d589d 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts @@ -425,7 +425,7 @@ class BaseUpdateEventTypeInput { @DocsPropertyOptional({ default: false, description: - "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type.", + "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type.", }) bookingRequiresAuthentication?: boolean; } diff --git a/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts index 4610cb29597982..09bf5fe1728186 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts @@ -449,7 +449,7 @@ class BaseEventTypeOutput_2024_06_14 { @IsBoolean() @DocsProperty({ description: - "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type.", + "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type.", }) bookingRequiresAuthentication?: boolean; } diff --git a/packages/prisma/extensions/event-type-timestamps.ts b/packages/prisma/extensions/event-type-timestamps.ts deleted file mode 100644 index 9f1f8d5388423b..00000000000000 --- a/packages/prisma/extensions/event-type-timestamps.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Prisma } from "@calcom/prisma/client"; - -export function eventTypeTimestampsExtension() { - return Prisma.defineExtension({ - query: { - eventType: { - async create({ args, query }) { - const now = new Date(); - args.data.createdAt = now; - return query(args); - }, - async createMany({ args, query }) { - const now = new Date(); - if (Array.isArray(args.data)) { - args.data = args.data.map((item) => ({ - ...item, - createdAt: now, - })); - } else { - args.data.createdAt = now; - } - return query(args); - }, - }, - }, - }); -} diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index 03a4456ab4020c..c014de6c7d4272 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -2,7 +2,6 @@ import { PrismaClient, type Prisma } from "@calcom/prisma/client"; import { bookingIdempotencyKeyExtension } from "./extensions/booking-idempotency-key"; import { disallowUndefinedDeleteUpdateManyExtension } from "./extensions/disallow-undefined-delete-update-many"; -import { eventTypeTimestampsExtension } from "./extensions/event-type-timestamps"; import { excludeLockedUsersExtension } from "./extensions/exclude-locked-users"; import { excludePendingPaymentsExtension } from "./extensions/exclude-pending-payment-teams"; import { usageTrackingExtention } from "./extensions/usage-tracking"; @@ -43,7 +42,6 @@ export const customPrisma = (options?: Prisma.PrismaClientOptions) => .$extends(excludeLockedUsersExtension()) .$extends(excludePendingPaymentsExtension()) .$extends(bookingIdempotencyKeyExtension()) - .$extends(eventTypeTimestampsExtension()) .$extends(disallowUndefinedDeleteUpdateManyExtension()) as unknown as PrismaClient; // If any changed on middleware server restart is required @@ -60,7 +58,6 @@ export const prisma: PrismaClient = baseClient .$extends(excludeLockedUsersExtension()) .$extends(excludePendingPaymentsExtension()) .$extends(bookingIdempotencyKeyExtension()) - .$extends(eventTypeTimestampsExtension()) .$extends(disallowUndefinedDeleteUpdateManyExtension()) as unknown as PrismaClient; // This prisma instance is meant to be used only for READ operations. diff --git a/packages/prisma/migrations/20250905115031_add_webhooks_permissions_default_roles/migration.sql b/packages/prisma/migrations/20250905115031_add_webhooks_permissions_default_roles/migration.sql deleted file mode 100644 index 5d09ca6bf88d8b..00000000000000 --- a/packages/prisma/migrations/20250905115031_add_webhooks_permissions_default_roles/migration.sql +++ /dev/null @@ -1,22 +0,0 @@ --- Add webhook permissions for admin role -INSERT INTO "RolePermission" (id, "roleId", resource, action, "createdAt") -SELECT - gen_random_uuid(), 'admin_role', resource, action, NOW() -FROM ( - VALUES - -- Attribute permissions - ('webhook', 'create'), - ('webhook', 'read'), - ('webhook', 'update'), - ('webhook', 'delete') -) AS permissions(resource, action); - --- Add read permission for member role -INSERT INTO "RolePermission" (id, "roleId", resource, action, "createdAt") -SELECT - gen_random_uuid(), 'member_role', resource, action, NOW() -FROM ( - VALUES - -- Attribute permissions - read only - ('webhook', 'read') -) AS permissions(resource, action); diff --git a/packages/prisma/migrations/20250911093331_pbac_team_billing/migration.sql b/packages/prisma/migrations/20250911093331_pbac_team_billing/migration.sql deleted file mode 100644 index 2b67281002d9d1..00000000000000 --- a/packages/prisma/migrations/20250911093331_pbac_team_billing/migration.sql +++ /dev/null @@ -1,7 +0,0 @@ -INSERT INTO "RolePermission" (id, "roleId", resource, action, "createdAt") -SELECT - gen_random_uuid(), 'admin_role', resource, action, NOW() -FROM ( - VALUES - ('team', 'manageBilling') -) AS permissions(resource, action); diff --git a/packages/prisma/migrations/20250919124540_booking_denormalized_starttime_endtime/migration.sql b/packages/prisma/migrations/20250919124540_booking_denormalized_starttime_endtime/migration.sql deleted file mode 100644 index b8d75b222902fa..00000000000000 --- a/packages/prisma/migrations/20250919124540_booking_denormalized_starttime_endtime/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- CreateIndex -CREATE INDEX "BookingDenormalized_startTime_endTime_idx" ON "BookingDenormalized"("startTime", "endTime"); diff --git a/packages/prisma/migrations/20250919174231_add_event_type_timestamps/migration.sql b/packages/prisma/migrations/20250919174231_add_event_type_timestamps/migration.sql deleted file mode 100644 index 46ad7995c00a86..00000000000000 --- a/packages/prisma/migrations/20250919174231_add_event_type_timestamps/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- AlterTable -ALTER TABLE "EventType" ADD COLUMN "createdAt" TIMESTAMP(3), -ADD COLUMN "updatedAt" TIMESTAMP(3); diff --git a/packages/prisma/migrations/20250923082416_add_spam_block/migration.sql b/packages/prisma/migrations/20250923082416_add_spam_block/migration.sql deleted file mode 100644 index c8814cae4ec6a9..00000000000000 --- a/packages/prisma/migrations/20250923082416_add_spam_block/migration.sql +++ /dev/null @@ -1,52 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[type,value,organizationId]` on the table `Watchlist` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateEnum -CREATE TYPE "WatchlistAction" AS ENUM ('REPORT', 'BLOCK'); - --- DropIndex -DROP INDEX "Watchlist_type_value_idx"; - --- DropIndex -DROP INDEX "Watchlist_type_value_key"; - --- AlterTable -ALTER TABLE "Watchlist" ADD COLUMN "action" "WatchlistAction" NOT NULL DEFAULT 'REPORT', -ADD COLUMN "organizationId" INTEGER; - --- CreateTable -CREATE TABLE "BlockedBookingLog" ( - "id" TEXT NOT NULL, - "email" TEXT NOT NULL, - "eventTypeId" INTEGER, - "organizationId" INTEGER, - "bookingData" JSONB, - "watchlistId" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "BlockedBookingLog_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "BlockedBookingLog_organizationId_createdAt_idx" ON "BlockedBookingLog"("organizationId", "createdAt"); - --- CreateIndex -CREATE INDEX "BlockedBookingLog_email_idx" ON "BlockedBookingLog"("email"); - --- CreateIndex -CREATE INDEX "BlockedBookingLog_watchlistId_idx" ON "BlockedBookingLog"("watchlistId"); - --- CreateIndex -CREATE INDEX "Watchlist_type_value_organizationId_action_idx" ON "Watchlist"("type", "value", "organizationId", "action"); - --- CreateIndex -CREATE UNIQUE INDEX "Watchlist_type_value_organizationId_key" ON "Watchlist"("type","value","organizationId"); - --- CreateIndex --- Enforce uniqueness for global entries (organizationId IS NULL) -CREATE UNIQUE INDEX "Watchlist_type_value_global_key" - ON "Watchlist"("type","value") - WHERE "organizationId" IS NULL; diff --git a/packages/prisma/migrations/20250924091435_update_watchlist_created_by_id_fkey/migration.sql b/packages/prisma/migrations/20250924091435_update_watchlist_created_by_id_fkey/migration.sql deleted file mode 100644 index 41c43862d40030..00000000000000 --- a/packages/prisma/migrations/20250924091435_update_watchlist_created_by_id_fkey/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- DropForeignKey -ALTER TABLE "Watchlist" DROP CONSTRAINT "Watchlist_createdById_fkey"; - --- AlterTable -ALTER TABLE "Watchlist" ALTER COLUMN "createdById" DROP NOT NULL; - --- AddForeignKey -ALTER TABLE "Watchlist" ADD CONSTRAINT "Watchlist_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma/package.json b/packages/prisma/package.json index dc9407f418f575..28b4116dbe6567 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -18,10 +18,11 @@ "post-install": "yarn generate-schemas", "seed-app-store": "ts-node --transpile-only ../../scripts/seed-app-store.ts", "delete-app": "ts-node --transpile-only ./delete-app.ts", - "seed-insights": "ts-node --transpile-only ../../scripts/seed-insights.ts", - "seed-pbac": "ts-node --transpile-only ../../scripts/seed-pbac-organization.ts" + "seed-insights": "ts-node --transpile-only ./seed-insights.ts", + "seed-pbac": "ts-node --transpile-only ./seed-pbac-organization.ts" }, "devDependencies": { + "@faker-js/faker": "9.2.0", "npm-run-all": "^4.1.5" }, "dependencies": { diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 4f08fa2c63adcb..0ea9c358a758bb 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -244,9 +244,6 @@ model EventType { bookingRequiresAuthentication Boolean @default(false) - createdAt DateTime? - updatedAt DateTime? @updatedAt - @@unique([userId, slug]) @@unique([teamId, slug]) @@unique([userId, parentId]) @@ -1746,11 +1743,10 @@ model BookingDenormalized { @@index([status]) @@index([teamId, isTeamBooking]) @@index([userId, isTeamBooking]) - @@index([startTime, endTime]) } view BookingTimeStatusDenormalized { - id Int @unique + id Int @id @unique uid String eventTypeId Int? title String @@ -2251,47 +2247,22 @@ enum WatchlistSeverity { CRITICAL } -enum WatchlistAction { - REPORT - BLOCK -} - model Watchlist { - id String @id @unique @default(cuid()) - type WatchlistType - + id String @id @unique @default(cuid()) + type WatchlistType + // The identifier of the Watchlisted entity (email or domain) value String description String? - - organizationId Int? - - action WatchlistAction @default(REPORT) - severity WatchlistSeverity @default(LOW) createdAt DateTime @default(now()) - createdBy User? @relation("CreatedWatchlists", onDelete: SetNull, fields: [createdById], references: [id]) - createdById Int? + createdBy User @relation("CreatedWatchlists", onDelete: Cascade, fields: [createdById], references: [id]) + createdById Int updatedAt DateTime @updatedAt updatedBy User? @relation("UpdatedWatchlists", onDelete: SetNull, fields: [updatedById], references: [id]) updatedById Int? + severity WatchlistSeverity @default(LOW) - @@unique([type, value, organizationId]) - @@index([type, value, organizationId, action]) -} - -model BlockedBookingLog { - id String @id @default(uuid()) - email String - eventTypeId Int? - - organizationId Int? - - bookingData Json? - createdAt DateTime @default(now()) - watchlistId String? - - @@index([organizationId, createdAt]) - @@index([email]) - @@index([watchlistId]) + @@unique([type, value]) + @@index([type, value]) } enum BillingPeriod { diff --git a/scripts/seed-huge-event-types.ts b/packages/prisma/seed-huge-event-types.ts similarity index 100% rename from scripts/seed-huge-event-types.ts rename to packages/prisma/seed-huge-event-types.ts diff --git a/scripts/seed-insights.ts b/packages/prisma/seed-insights.ts similarity index 98% rename from scripts/seed-insights.ts rename to packages/prisma/seed-insights.ts index 83a52370357a2e..d330b6455c3a9e 100644 --- a/scripts/seed-insights.ts +++ b/packages/prisma/seed-insights.ts @@ -2,11 +2,11 @@ import { faker } from "@faker-js/faker"; import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; -import type { Prisma } from "@calcom/prisma/client"; -import { PrismaClient } from "@calcom/prisma/client"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { BookingStatus, AssignmentReasonEnum } from "@calcom/prisma/enums"; +import type { Prisma } from "./client"; +import { PrismaClient } from "./client"; import { seedAttributes, seedRoutingFormResponses, seedRoutingForms } from "./seed-utils"; function getRandomRatingFeedback() { diff --git a/scripts/seed-pbac-organization.ts b/packages/prisma/seed-pbac-organization.ts similarity index 99% rename from scripts/seed-pbac-organization.ts rename to packages/prisma/seed-pbac-organization.ts index 01a527b9ba7e66..8cb68d958f01ee 100644 --- a/scripts/seed-pbac-organization.ts +++ b/packages/prisma/seed-pbac-organization.ts @@ -1,10 +1,11 @@ import { uuid } from "short-uuid"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; -import prisma from "@calcom/prisma"; import { MembershipRole, RoleType } from "@calcom/prisma/enums"; +import prisma from "."; + /** * Creates an organization with custom roles and PBAC (Permission-Based Access Control) enabled * This demonstrates how to set up fine-grained permissions for team members diff --git a/scripts/seed-utils.ts b/packages/prisma/seed-utils.ts similarity index 98% rename from scripts/seed-utils.ts rename to packages/prisma/seed-utils.ts index 97150269a85175..6cbe9b2fe2d709 100644 --- a/scripts/seed-utils.ts +++ b/packages/prisma/seed-utils.ts @@ -4,12 +4,13 @@ import { uuid } from "short-uuid"; import type z from "zod"; import dayjs from "@calcom/dayjs"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; -import prisma from "@calcom/prisma"; -import type { Prisma, UserPermissionRole } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; -import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; + +import prisma from "."; +import type { Prisma, UserPermissionRole } from "./client"; +import type { teamMetadataSchema } from "./zod-utils"; export async function createUserAndEventType({ user, diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index afddcb2146b3da..c0a2558ca9273e 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -11,9 +11,10 @@ import type { ZodTypeAny, } from "zod"; -import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid"; +import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid"; +import type { FieldType as FormBuilderFieldType } from "@calcom/features/form-builder/schema"; +import { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/schema"; import { emailSchema as emailRegexSchema, emailRegex } from "@calcom/lib/emailSchema"; -import { getValidRhfFieldName } from "@calcom/lib/getValidRhfFieldName"; import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; import { zodAttributesQueryValue } from "@calcom/lib/raqb/zod"; import { slugify } from "@calcom/lib/slugify"; @@ -134,6 +135,10 @@ export const eventTypeMetaDataSchemaWithoutApps = _eventTypeMetaDataSchemaWithou export type EventTypeMetadata = z.infer; +export const eventTypeBookingFields = formBuilderFieldsSchema; +export const BookingFieldTypeEnum = eventTypeBookingFields.element.shape.type.Enum; +export type BookingFieldType = FormBuilderFieldType; + // Validation of user added bookingFields' responses happen using `getBookingResponsesSchema` which requires `eventType`. // So it is a dynamic validation and thus entire validation can't exist here // Note that this validation runs to validate prefill params as well, so it should consider that partial values can be there. e.g. `name` might be empty string @@ -763,193 +768,3 @@ export const serviceAccountKeySchema = z export type TServiceAccountKeySchema = z.infer; export const rrSegmentQueryValueSchema = zodAttributesQueryValue.nullish(); - -// Routing Form Fields -export const fieldTypeEnum = z.enum([ - "name", - "text", - "textarea", - "number", - "email", - "phone", - "address", - "multiemail", - "select", - "multiselect", - "checkbox", - "radio", - "radioInput", - "boolean", - "url", -]); - -export type FieldType = z.infer; - -export const excludeOrRequireEmailSchema = z.string().superRefine((val, ctx) => { - const allDomains = val.split(",").map((dom) => dom.trim()); - - const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/; - - /* - Valid patterns - [ example, example.anything, anyone@example.anything ] - Invalid patterns - Patterns involving capital letter [ Example, Example.anything, Anyone@example.anything ] -*/ - - const isValid = !allDomains.some((domain) => !regex.test(domain)); - - if (!isValid) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Enter valid domain or email", - }); - } -}); - -export const EditableSchema = z.enum([ - "system", // Can't be deleted, can't be hidden, name can't be edited, can't be marked optional - "system-but-optional", // Can't be deleted. Name can't be edited. But can be hidden or be marked optional - "system-but-hidden", // Can't be deleted, name can't be edited, will be shown - "user", // Fully editable - "user-readonly", // All fields are readOnly. -]); - -export const baseFieldSchema = z.object({ - name: z.string().transform(getValidRhfFieldName), - type: fieldTypeEnum, - // TODO: We should make at least one of `defaultPlaceholder` and `placeholder` required. Do the same for label. - label: z.string().optional(), - labelAsSafeHtml: z.string().optional(), - - /** - * It is the default label that will be used when a new field is created. - * Note: It belongs in FieldsTypeConfig, so that changing defaultLabel in code can work for existing fields as well(for fields that are using the default label). - * Supports translation - */ - defaultLabel: z.string().optional(), - - placeholder: z.string().optional(), - /** - * It is the default placeholder that will be used when a new field is created. - * Note: Same as defaultLabel, it belongs in FieldsTypeConfig - * Supports translation - */ - defaultPlaceholder: z.string().optional(), - required: z.boolean().default(false).optional(), - /** - * It is the list of options that is valid for a certain type of fields. - * - */ - options: z - .array( - z.object({ - label: z.string(), - value: z.string(), - price: z.coerce.number().min(0).optional(), - }) - ) - .optional(), - /** - * This is an alternate way to specify options when the options are stored elsewhere. Form Builder expects options to be present at `dataStore[getOptionsAt]` - * This allows keeping a single source of truth in DB. - */ - getOptionsAt: z.string().optional(), - - /** - * For `radioInput` type of questions, it stores the input that is shown based on the user option selected. - * e.g. If user is given a list of locations and he selects "Phone", then he will be shown a phone input - */ - optionsInputs: z - .record( - z.object({ - // Support all types as needed - // Must be a subset of `fieldTypeEnum`.TODO: Enforce it in TypeScript - type: z.enum(["address", "phone", "text"]), - required: z.boolean().optional(), - placeholder: z.string().optional(), - }) - ) - .optional(), - - /** - * It is the minimum number of characters that can be entered in the field. - * It is used for types with `supportsLengthCheck= true`. - * @default 0 - * @requires supportsLengthCheck = true - */ - minLength: z.number().optional(), - - /** - * It is the maximum number of characters that can be entered in the field. - * It is used for types with `supportsLengthCheck= true`. - * @requires supportsLengthCheck = true - */ - maxLength: z.number().optional(), - - // Emails that needs to be excluded - excludeEmails: excludeOrRequireEmailSchema.optional(), - // Emails that need to be required - requireEmails: excludeOrRequireEmailSchema.optional(), - // Price associated with the field which works like addons which users can add to the booking - price: z.coerce.number().min(0).optional(), -}); - -export const variantsConfigSchema = z.object({ - variants: z.record( - z.object({ - /** - * Variant Fields schema for a variant of the main field. - * It doesn't support non text fields as of now - **/ - fields: baseFieldSchema - .omit({ - defaultLabel: true, - defaultPlaceholder: true, - options: true, - getOptionsAt: true, - optionsInputs: true, - }) - .array(), - }) - ), -}); - -export const fieldSchema = baseFieldSchema.merge( - z.object({ - variant: z.string().optional(), - variantsConfig: variantsConfigSchema.optional(), - - views: z - .object({ - label: z.string(), - id: z.string(), - description: z.string().optional(), - }) - .array() - .optional(), - - /** - * It is used to hide fields such as location when there are less than two options - */ - hideWhenJustOneOption: z.boolean().default(false).optional(), - - hidden: z.boolean().optional(), - editable: EditableSchema.default("user").optional(), - sources: z - .array( - z.object({ - // Unique ID for the `type`. If type is workflow, it's the workflow ID - id: z.string(), - type: z.union([z.literal("user"), z.literal("system"), z.string()]), - label: z.string(), - editUrl: z.string().optional(), - // Mark if a field is required by this source or not. This allows us to set `field.required` based on all the sources' fieldRequired value - fieldRequired: z.boolean().optional(), - }) - ) - .optional(), - disableOnPrefill: z.boolean().default(false).optional(), - }) -); - -export const eventTypeBookingFields = z.array(fieldSchema); -export const BookingFieldTypeEnum = eventTypeBookingFields.element.shape.type.Enum; diff --git a/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts b/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts index 9f55735945993a..e34cb83c2f608b 100644 --- a/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts @@ -112,6 +112,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx, input }: TeamsAndUserProf // Store permission results for teams that passed the filter hasPermissionForFiltered = permissionChecks.filter((hasPermission) => hasPermission); teamsData = teamsData.filter((_, index) => permissionChecks[index]); + } return [ diff --git a/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts b/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts index f0c16167744f06..27437f3a3c6b17 100644 --- a/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts @@ -1,6 +1,6 @@ +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { validPassword } from "@calcom/features/auth/lib/validPassword"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { prisma } from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; diff --git a/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts b/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts index b43960dfe6ddc4..253c390e5520ac 100644 --- a/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/calendarOverlay.handler.ts @@ -1,5 +1,5 @@ import dayjs from "@calcom/dayjs"; -import { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager"; +import { getBusyCalendarTimes } from "@calcom/lib/CalendarManager"; import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server"; import { prisma } from "@calcom/prisma"; import type { EventBusyDate } from "@calcom/types/Calendar"; diff --git a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts index 3262b70d4c8a4b..a2ce6f27a0e5d7 100644 --- a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts @@ -1,10 +1,10 @@ import dayjs from "@calcom/dayjs"; import { sendAddGuestsEmails } from "@calcom/emails"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { isTeamAdmin, isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { BookingResponses } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index 7838b91c5689d4..217e2435b53829 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -4,7 +4,7 @@ import { getEventLocationType, OrganizerDefaultConferencingAppType } from "@calc import { getAppFromSlug } from "@calcom/app-store/utils"; import { sendLocationChangeEmailsAndSMS } from "@calcom/emails"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; +import EventManager from "@calcom/lib/EventManager"; import { buildCalEventFromBooking } from "@calcom/lib/buildCalEventFromBooking"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 9aea7ee396099d..076a29fe666db1 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -578,6 +578,7 @@ export async function getBookings({ .selectFrom("Attendee") .select(["Attendee.email"]) .whereRef("BookingSeat.attendeeId", "=", "Attendee.id") + .where("Attendee.email", "=", user.email) ).as("attendee"), ]) .whereRef("BookingSeat.bookingId", "=", "Booking.id") diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 2afdbe1500e6a3..b7207190070e6e 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -22,7 +22,7 @@ import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/ import { getTranslation } from "@calcom/lib/server/i18n"; import { WorkflowRepository } from "@calcom/lib/server/repository/workflow"; import { BookingWebhookFactory } from "@calcom/lib/server/service/BookingWebhookFactory"; -import { deleteMeeting } from "@calcom/app-store/videoClient"; +import { deleteMeeting } from "@calcom/lib/videoClient"; import { prisma } from "@calcom/prisma"; import type { BookingReference, EventType } from "@calcom/prisma/client"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; diff --git a/packages/trpc/server/routers/viewer/calVideo/getCalVideoRecordings.handler.ts b/packages/trpc/server/routers/viewer/calVideo/getCalVideoRecordings.handler.ts index 402eadc7144e7c..1cb6c39a5c2b02 100644 --- a/packages/trpc/server/routers/viewer/calVideo/getCalVideoRecordings.handler.ts +++ b/packages/trpc/server/routers/viewer/calVideo/getCalVideoRecordings.handler.ts @@ -1,4 +1,4 @@ -import { getRecordingsOfCalVideoByRoomName } from "@calcom/app-store/videoClient"; +import { getRecordingsOfCalVideoByRoomName } from "@calcom/lib/videoClient"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; diff --git a/packages/trpc/server/routers/viewer/calVideo/getDownloadLinkOfCalVideoRecordings.handler.ts b/packages/trpc/server/routers/viewer/calVideo/getDownloadLinkOfCalVideoRecordings.handler.ts index a5b71daec05026..b836c9899b9e7a 100644 --- a/packages/trpc/server/routers/viewer/calVideo/getDownloadLinkOfCalVideoRecordings.handler.ts +++ b/packages/trpc/server/routers/viewer/calVideo/getDownloadLinkOfCalVideoRecordings.handler.ts @@ -1,6 +1,6 @@ /// import { IS_SELF_HOSTED } from "@calcom/lib/constants"; -import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient"; +import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient"; import { TRPCError } from "@trpc/server"; diff --git a/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.test.ts b/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.test.ts index 96905b8d320967..3ee56c837de288 100644 --- a/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.test.ts +++ b/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.test.ts @@ -12,7 +12,7 @@ import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAn import { describe, it, vi, expect } from "vitest"; -import { getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager"; +import { getConnectedCalendars } from "@calcom/lib/CalendarManager"; import { SchedulingType, MembershipRole } from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; @@ -20,7 +20,7 @@ import { TRPCError } from "@trpc/server"; import type { TrpcSessionUser } from "../../../types"; import { setDestinationCalendarHandler } from "./setDestinationCalendar.handler"; -vi.mock("@calcom/features/calendars/lib/CalendarManager", () => ({ +vi.mock("@calcom/lib/CalendarManager", () => ({ getConnectedCalendars: vi.fn(), getCalendarCredentials: vi.fn().mockImplementation((creds) => creds), })); diff --git a/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.ts b/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.ts index d7cbe3a7a1dde2..a1190f61c685aa 100644 --- a/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.ts +++ b/packages/trpc/server/routers/viewer/calendars/setDestinationCalendar.handler.ts @@ -1,4 +1,4 @@ -import { getCalendarCredentials, getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager"; +import { getCalendarCredentials, getConnectedCalendars } from "@calcom/lib/CalendarManager"; import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials"; import { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar"; import { prisma } from "@calcom/prisma"; diff --git a/packages/trpc/server/routers/viewer/credits/buyCredits.handler.ts b/packages/trpc/server/routers/viewer/credits/buyCredits.handler.ts index 2478a07b2d9044..321e45557a02fd 100644 --- a/packages/trpc/server/routers/viewer/credits/buyCredits.handler.ts +++ b/packages/trpc/server/routers/viewer/credits/buyCredits.handler.ts @@ -1,11 +1,8 @@ import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billling-service"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { MembershipRepository } from "@calcom/lib/server/repository/membership"; import { TeamRepository } from "@calcom/lib/server/repository/team"; -import { TeamService } from "@calcom/lib/server/service/teamService"; import prisma from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/client"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -30,17 +27,9 @@ export const buyCreditsHandler = async ({ ctx, input }: BuyCreditsOptions) => { const { quantity, teamId } = input; if (teamId) { - const team = await TeamService.fetchTeamOrThrow(teamId); + const adminMembership = await MembershipRepository.getAdminOrOwnerMembership(ctx.user.id, teamId); - const permissionService = new PermissionCheckService(); - const hasManageBillingPermission = await permissionService.checkPermission({ - userId: ctx.user.id, - teamId, - permission: team.isOrganization ? "organization.manageBilling" : "team.manageBilling", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasManageBillingPermission) { + if (!adminMembership) { throw new TRPCError({ code: "UNAUTHORIZED", }); diff --git a/packages/trpc/server/routers/viewer/credits/downloadExpenseLog.handler.ts b/packages/trpc/server/routers/viewer/credits/downloadExpenseLog.handler.ts index c88e62e6f46159..3637b7e6e0a399 100644 --- a/packages/trpc/server/routers/viewer/credits/downloadExpenseLog.handler.ts +++ b/packages/trpc/server/routers/viewer/credits/downloadExpenseLog.handler.ts @@ -1,7 +1,5 @@ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { CreditsRepository } from "@calcom/lib/server/repository/credits"; -import { TeamService } from "@calcom/lib/server/service/teamService"; -import { MembershipRole } from "@calcom/prisma/enums"; +import { MembershipRepository } from "@calcom/lib/server/repository/membership"; import { TRPCError } from "@trpc/server"; @@ -30,17 +28,9 @@ export const downloadExpenseLogHandler = async ({ ctx, input }: DownloadExpenseL const { teamId, startDate, endDate } = input; if (teamId) { - const team = await TeamService.fetchTeamOrThrow(teamId); + const adminMembership = await MembershipRepository.getAdminOrOwnerMembership(ctx.user.id, teamId); - const permissionService = new PermissionCheckService(); - const hasManageBillingPermission = await permissionService.checkPermission({ - userId: ctx.user.id, - teamId, - permission: team.isOrganization ? "organization.manageBilling" : "team.manageBilling", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasManageBillingPermission) { + if (!adminMembership) { throw new TRPCError({ code: "UNAUTHORIZED", }); diff --git a/packages/trpc/server/routers/viewer/credits/getAllCredits.handler.ts b/packages/trpc/server/routers/viewer/credits/getAllCredits.handler.ts index f3149f25f8a837..061d534dcd6de8 100644 --- a/packages/trpc/server/routers/viewer/credits/getAllCredits.handler.ts +++ b/packages/trpc/server/routers/viewer/credits/getAllCredits.handler.ts @@ -1,7 +1,4 @@ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { MembershipRepository } from "@calcom/lib/server/repository/membership"; -import { TeamService } from "@calcom/lib/server/service/teamService"; -import { MembershipRole } from "@calcom/prisma/client"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -19,17 +16,9 @@ export const getAllCreditsHandler = async ({ ctx, input }: GetAllCreditsOptions) const { teamId } = input; if (teamId) { - const team = await TeamService.fetchTeamOrThrow(teamId); + const adminMembership = await MembershipRepository.getAdminOrOwnerMembership(ctx.user.id, teamId); - const permissionService = new PermissionCheckService(); - const hasManageBillingPermission = await permissionService.checkPermission({ - userId: ctx.user.id, - teamId, - permission: team.isOrganization ? "organization.manageBilling" : "team.manageBilling", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasManageBillingPermission) { + if (!adminMembership) { throw new TRPCError({ code: "UNAUTHORIZED", }); diff --git a/packages/trpc/server/routers/viewer/eventTypes/__tests__/getUserEventGroups.test.ts b/packages/trpc/server/routers/viewer/eventTypes/__tests__/getUserEventGroups.test.ts deleted file mode 100644 index 375e304f920d3f..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/__tests__/getUserEventGroups.test.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -import type { PrismaClient } from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { TRPCError } from "@trpc/server"; - -import { getUserEventGroups } from "../getUserEventGroups.handler"; - -// Mock dependencies -vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({ - checkRateLimitAndThrowError: vi.fn(), -})); - -vi.mock("@calcom/lib/server/repository/membership", () => ({ - MembershipRepository: { - findAllByUpIdIncludeTeam: vi.fn(), - }, -})); - -vi.mock("@calcom/lib/server/repository/profile", () => ({ - ProfileRepository: { - findByUpId: vi.fn(), - }, -})); - -const mockFilterTeamsByEventTypeReadPermission = vi.fn(); - -vi.mock("../teamAccessUseCase", () => ({ - TeamAccessUseCase: vi.fn().mockImplementation(() => ({ - filterTeamsByEventTypeReadPermission: mockFilterTeamsByEventTypeReadPermission, - })), -})); - -vi.mock("@calcom/lib/getBookerUrl/server", () => ({ - getBookerBaseUrl: vi.fn().mockResolvedValue("https://cal.com"), -})); - -vi.mock("@calcom/lib/getBookerUrl/client", () => ({ - getBookerBaseUrlSync: vi.fn().mockReturnValue("https://cal.com"), -})); - -vi.mock("@calcom/lib/getAvatarUrl", () => ({ - getUserAvatarUrl: vi.fn().mockReturnValue("https://avatar.com/user.jpg"), -})); - -vi.mock("@calcom/lib/defaultAvatarImage", () => ({ - getPlaceholderAvatar: vi.fn().mockReturnValue("https://avatar.com/placeholder.jpg"), -})); - -vi.mock("@calcom/features/pbac/lib/resource-permissions", () => ({ - getResourcePermissions: vi.fn(), -})); - -describe("getUserEventGroups", () => { - const mockUser = { - id: 1, - profile: { - upId: "user-123", - }, - } as unknown as NonNullable[0]["ctx"]["user"]>; - - const mockProfile = { - id: 1, - username: "testuser", - name: "Test User", - avatarUrl: null, - organizationId: null, - organization: null, - } as unknown as NonNullable< - Awaited> - >; - - const mockCtx = { - user: mockUser, - prisma: {} as PrismaClient, - }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("Basic functionality", () => { - it("should throw TRPCError when profile is not found", async () => { - const { ProfileRepository } = await import("@calcom/lib/server/repository/profile"); - vi.mocked(ProfileRepository.findByUpId).mockResolvedValue(null); - - await expect( - getUserEventGroups({ - ctx: mockCtx, - input: null, - }) - ).rejects.toThrow(TRPCError); - }); - - it("should return user event groups when no filters are applied", async () => { - const { ProfileRepository } = await import("@calcom/lib/server/repository/profile"); - const { MembershipRepository } = await import("@calcom/lib/server/repository/membership"); - - vi.mocked(ProfileRepository.findByUpId).mockResolvedValue(mockProfile); - vi.mocked(MembershipRepository.findAllByUpIdIncludeTeam).mockResolvedValue([]); - mockFilterTeamsByEventTypeReadPermission.mockResolvedValue([]); - - const result = await getUserEventGroups({ - ctx: mockCtx, - input: null, - }); - - expect(result).toHaveProperty("eventTypeGroups"); - expect(result).toHaveProperty("profiles"); - expect(result.eventTypeGroups).toHaveLength(1); - expect(result.eventTypeGroups[0]).toMatchObject({ - teamId: null, - membershipRole: null, - profile: { - slug: "testuser", - name: "Test User", - }, - }); - }); - }); - - describe("Team memberships", () => { - it("should include team events when team memberships exist", async () => { - const { ProfileRepository } = await import("@calcom/lib/server/repository/profile"); - const { MembershipRepository } = await import("@calcom/lib/server/repository/membership"); - const { getResourcePermissions } = await import("@calcom/features/pbac/lib/resource-permissions"); - - const mockTeamMembership = { - id: 1, - teamId: 100, - userId: 1, - accepted: true, - role: MembershipRole.MEMBER, - customRoleId: null, - disableImpersonation: false, - createdAt: new Date(), - updatedAt: new Date(), - team: { - id: 100, - name: "Test Team", - slug: "test-team", - logoUrl: null, - parentId: null, - parent: null, - metadata: {}, - }, - } as unknown as NonNullable< - Awaited< - ReturnType< - typeof import("@calcom/lib/server/repository/membership").MembershipRepository.findAllByUpIdIncludeTeam - > - > - >[0]; - - vi.mocked(ProfileRepository.findByUpId).mockResolvedValue(mockProfile); - vi.mocked(MembershipRepository.findAllByUpIdIncludeTeam).mockResolvedValue([mockTeamMembership]); - mockFilterTeamsByEventTypeReadPermission.mockResolvedValue([mockTeamMembership]); - - vi.mocked(getResourcePermissions).mockResolvedValue({ - canCreate: false, - canEdit: true, - canDelete: false, - canRead: true, - }); - - const result = await getUserEventGroups({ - ctx: mockCtx, - input: null, - }); - - expect(result.eventTypeGroups).toHaveLength(2); // User + Team - expect(result.eventTypeGroups[1]).toMatchObject({ - teamId: 100, - profile: { - name: "Test Team", - slug: "team/test-team", - }, - }); - }); - }); - - describe("Permissions", () => { - it("should handle PBAC permissions correctly", async () => { - const { ProfileRepository } = await import("@calcom/lib/server/repository/profile"); - const { MembershipRepository } = await import("@calcom/lib/server/repository/membership"); - const { getResourcePermissions } = await import("@calcom/features/pbac/lib/resource-permissions"); - - const mockTeamMembership = { - id: 1, - teamId: 100, - userId: 1, - accepted: true, - role: MembershipRole.ADMIN, - customRoleId: null, - disableImpersonation: false, - createdAt: new Date(), - updatedAt: new Date(), - team: { - id: 100, - name: "Test Team", - slug: "test-team", - logoUrl: null, - parentId: null, - parent: null, - metadata: {}, - }, - } as unknown as NonNullable< - Awaited< - ReturnType< - typeof import("@calcom/lib/server/repository/membership").MembershipRepository.findAllByUpIdIncludeTeam - > - > - >[0]; - - vi.mocked(ProfileRepository.findByUpId).mockResolvedValue(mockProfile); - vi.mocked(MembershipRepository.findAllByUpIdIncludeTeam).mockResolvedValue([mockTeamMembership]); - mockFilterTeamsByEventTypeReadPermission.mockResolvedValue([mockTeamMembership]); - - vi.mocked(getResourcePermissions).mockResolvedValue({ - canCreate: true, - canEdit: true, - canDelete: true, - canRead: true, - }); - - const result = await getUserEventGroups({ - ctx: mockCtx, - input: null, - }); - - expect(result.profiles[1]).toMatchObject({ - canCreateEventTypes: true, - canUpdateEventTypes: true, - }); - }); - - it("should fallback to role-based permissions when PBAC fails", async () => { - const { ProfileRepository } = await import("@calcom/lib/server/repository/profile"); - const { MembershipRepository } = await import("@calcom/lib/server/repository/membership"); - const { getResourcePermissions } = await import("@calcom/features/pbac/lib/resource-permissions"); - - const mockTeamMembership = { - id: 1, - teamId: 100, - userId: 1, - accepted: true, - role: MembershipRole.MEMBER, - customRoleId: null, - disableImpersonation: false, - createdAt: new Date(), - updatedAt: new Date(), - team: { - id: 100, - name: "Test Team", - slug: "test-team", - logoUrl: null, - parentId: null, - parent: null, - metadata: {}, - }, - } as unknown as NonNullable< - Awaited< - ReturnType< - typeof import("@calcom/lib/server/repository/membership").MembershipRepository.findAllByUpIdIncludeTeam - > - > - >[0]; - - vi.mocked(ProfileRepository.findByUpId).mockResolvedValue(mockProfile); - vi.mocked(MembershipRepository.findAllByUpIdIncludeTeam).mockResolvedValue([mockTeamMembership]); - mockFilterTeamsByEventTypeReadPermission.mockResolvedValue([mockTeamMembership]); - - vi.mocked(getResourcePermissions).mockRejectedValue(new Error("PBAC failed")); - - const result = await getUserEventGroups({ - ctx: mockCtx, - input: null, - }); - - // Member role should not have create/update/delete permissions - expect(result.profiles[1]).toMatchObject({ - canCreateEventTypes: false, - canUpdateEventTypes: false, - }); - }); - }); - - describe("Organization handling", () => { - it("should handle organization locked event types", async () => { - const { ProfileRepository } = await import("@calcom/lib/server/repository/profile"); - const { MembershipRepository } = await import("@calcom/lib/server/repository/membership"); - - const mockProfileWithOrg = { - ...mockProfile, - organization: { - organizationSettings: { - lockEventTypeCreationForUsers: true, - }, - }, - } as unknown as NonNullable< - Awaited< - ReturnType - > - >; - - vi.mocked(ProfileRepository.findByUpId).mockResolvedValue(mockProfileWithOrg); - vi.mocked(MembershipRepository.findAllByUpIdIncludeTeam).mockResolvedValue([]); - mockFilterTeamsByEventTypeReadPermission.mockResolvedValue([]); - - const result = await getUserEventGroups({ - ctx: mockCtx, - input: null, - }); - - expect(result.eventTypeGroups[0].profile.eventTypesLockedByOrg).toBe(true); - }); - }); - - describe("Routing forms", () => { - it("should handle routing forms slug format", async () => { - const { ProfileRepository } = await import("@calcom/lib/server/repository/profile"); - const { MembershipRepository } = await import("@calcom/lib/server/repository/membership"); - const { getResourcePermissions } = await import("@calcom/features/pbac/lib/resource-permissions"); - - const mockTeamMembership = { - id: 1, - teamId: 100, - userId: 1, - accepted: true, - role: MembershipRole.MEMBER, - customRoleId: null, - disableImpersonation: false, - createdAt: new Date(), - updatedAt: new Date(), - team: { - id: 100, - name: "Test Team", - slug: "test-team", - logoUrl: null, - parentId: null, - parent: null, - metadata: {}, - }, - } as unknown as NonNullable< - Awaited< - ReturnType< - typeof import("@calcom/lib/server/repository/membership").MembershipRepository.findAllByUpIdIncludeTeam - > - > - >[0]; - - vi.mocked(ProfileRepository.findByUpId).mockResolvedValue(mockProfile); - vi.mocked(MembershipRepository.findAllByUpIdIncludeTeam).mockResolvedValue([mockTeamMembership]); - mockFilterTeamsByEventTypeReadPermission.mockResolvedValue([mockTeamMembership]); - - vi.mocked(getResourcePermissions).mockResolvedValue({ - canCreate: false, - canEdit: true, - canDelete: false, - canRead: true, - }); - - const result = await getUserEventGroups({ - ctx: mockCtx, - input: { - forRoutingForms: true, - }, - }); - - expect(result.eventTypeGroups[1].profile.slug).toBe("team/test-team"); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/eventTypes/_router.ts b/packages/trpc/server/routers/viewer/eventTypes/_router.ts index d9e12aca1cb6f6..fda63764f3ca2d 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/_router.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/_router.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { logP } from "@calcom/lib/perf"; -import { MembershipRole } from "@calcom/prisma/enums"; import authedProcedure from "../../../procedures/authedProcedure"; import { router } from "../../../trpc"; @@ -11,7 +10,23 @@ import { ZGetHashedLinkInputSchema } from "./getHashedLink.schema"; import { ZGetHashedLinksInputSchema } from "./getHashedLinks.schema"; import { ZGetTeamAndEventTypeOptionsSchema } from "./getTeamAndEventTypeOptions.schema"; import { get } from "./procedures/get"; -import { createEventPbacProcedure } from "./util"; +import { eventOwnerProcedure } from "./util"; + +type BookingsRouterHandlerCache = { + getByViewer?: typeof import("./getByViewer.handler").getByViewerHandler; + getUserEventGroups?: typeof import("./getUserEventGroups.handler").getUserEventGroups; + getEventTypesFromGroup?: typeof import("./getEventTypesFromGroup.handler").getEventTypesFromGroup; + getTeamAndEventTypeOptions?: typeof import("./getTeamAndEventTypeOptions.handler").getTeamAndEventTypeOptions; + list?: typeof import("./list.handler").listHandler; + listWithTeam?: typeof import("./listWithTeam.handler").listWithTeamHandler; + get?: typeof import("./get.handler").getHandler; + delete?: typeof import("./delete.handler").deleteHandler; + bulkEventFetch?: typeof import("./bulkEventFetch.handler").bulkEventFetchHandler; + bulkUpdateToDefaultLocation?: typeof import("./bulkUpdateToDefaultLocation.handler").bulkUpdateToDefaultLocationHandler; +}; + +// Init the handler cache +const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {}; export const eventTypesRouter = router({ // REVIEW: What should we name this procedure? @@ -96,16 +111,14 @@ export const eventTypesRouter = router({ get, - delete: createEventPbacProcedure("eventType.delete", [MembershipRole.ADMIN, MembershipRole.OWNER]) - .input(ZDeleteInputSchema) - .mutation(async ({ ctx, input }) => { - const { deleteHandler } = await import("./delete.handler"); + delete: eventOwnerProcedure.input(ZDeleteInputSchema).mutation(async ({ ctx, input }) => { + const { deleteHandler } = await import("./delete.handler"); - return deleteHandler({ - ctx, - input, - }); - }), + return deleteHandler({ + ctx, + input, + }); + }), bulkEventFetch: authedProcedure.query(async ({ ctx }) => { const { bulkEventFetchHandler } = await import("./bulkEventFetch.handler"); diff --git a/packages/trpc/server/routers/viewer/eventTypes/bulkEventFetch.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/bulkEventFetch.handler.ts index 62d923d4056675..6d21df0afef611 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/bulkEventFetch.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/bulkEventFetch.handler.ts @@ -1,4 +1,4 @@ -import { getBulkUserEventTypes } from "@calcom/app-store/_utils/getBulkEventTypes"; +import { getBulkUserEventTypes } from "@calcom/lib/event-types/getBulkEventTypes"; import type { TrpcSessionUser } from "../../../types"; diff --git a/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts index 859787936380f9..638bd93ed90cf4 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts @@ -1,4 +1,4 @@ -import getEventTypeById from "@calcom/features/eventtypes/lib/getEventTypeById"; +import getEventTypeById from "@calcom/lib/event-types/getEventTypeById"; import type { PrismaClient } from "@calcom/prisma"; import type { TrpcSessionUser } from "../../../types"; diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts index 58ccfd8cb459ae..f72d39d86a5d9c 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts @@ -1,5 +1,5 @@ -import { getEventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer"; import type { PrismaClient } from "@calcom/prisma"; import type { TrpcSessionUser } from "../../../types"; diff --git a/packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts index a4cadf2b204978..f2d8584834fd2e 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getUserEventGroups.handler.ts @@ -1,16 +1,20 @@ +import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; +import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import { MembershipRepository } from "@calcom/lib/server/repository/membership"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; +// import { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer"; import type { PrismaClient } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; +import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; import type { TrpcSessionUser } from "../../../types"; import type { TEventTypeInputSchema } from "./getByViewer.schema"; -import { TeamAccessUseCase } from "./teamAccessUseCase"; -import { EventGroupBuilder } from "./usecases/EventGroupBuilder"; -import { ProfilePermissionProcessor } from "./usecases/ProfilePermissionProcessor"; -import { EventTypeGroupFilter } from "./utils/EventTypeGroupFilter"; type GetByViewerOptions = { ctx: { @@ -25,45 +29,174 @@ export const getUserEventGroups = async ({ ctx, input }: GetByViewerOptions) => identifier: `eventTypes:getUserProfiles:${ctx.user.id}`, rateLimitingType: "common", }); - const user = ctx.user; - const userProfile = user.profile; + const filters = input?.filters; + const forRoutingForms = input?.forRoutingForms; - // Validate profile exists + const userProfile = user.profile; const profile = await ProfileRepository.findByUpId(userProfile.upId); + const parentOrgHasLockedEventTypes = + profile?.organization?.organizationSettings?.lockEventTypeCreationForUsers; + const isFilterSet = filters && hasFilter(filters); + const isUpIdInFilter = filters?.upIds?.includes(userProfile.upId); + + let shouldListUserEvents = !isFilterSet || isUpIdInFilter; + + if (isFilterSet && filters?.upIds && !isUpIdInFilter) { + shouldListUserEvents = true; + } + + const profileMemberships = await MembershipRepository.findAllByUpIdIncludeTeam( + { + upId: userProfile.upId, + }, + { + where: { + accepted: true, + }, + } + ); + if (!profile) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } - // Initialize dependencies - const dependencies = { - membershipRepository: MembershipRepository, - profileRepository: ProfileRepository, - teamAccessUseCase: new TeamAccessUseCase(), + const memberships = profileMemberships.map((membership) => ({ + ...membership, + team: { + ...membership.team, + metadata: teamMetadataSchema.parse(membership.team.metadata), + }, + })); + + const teamMemberships = profileMemberships.map((membership) => ({ + teamId: membership.team.id, + membershipRole: membership.role, + })); + + type EventTypeGroup = { + teamId?: number | null; + parentId?: number | null; + bookerUrl: string; + membershipRole?: MembershipRole | null; + profile: { + slug: (typeof profile)["username"] | null; + name: (typeof profile)["name"]; + image: string; + eventTypesLockedByOrg?: boolean; + }; + metadata: { + membershipCount: number; + readOnly: boolean; + }; }; - // Build event groups - const eventGroupBuilder = new EventGroupBuilder(dependencies); - const { eventTypeGroups, teamPermissionsMap } = await eventGroupBuilder.buildEventGroups({ - userId: user.id, - userUpId: userProfile.upId, - filters: input?.filters, - forRoutingForms: input?.forRoutingForms, - }); + let eventTypeGroups: EventTypeGroup[] = []; + + if (shouldListUserEvents) { + const bookerUrl = await getBookerBaseUrl(profile.organizationId ?? null); + eventTypeGroups.push({ + teamId: null, + bookerUrl, + membershipRole: null, + profile: { + slug: profile.username, + name: profile.name, + image: getUserAvatarUrl({ + avatarUrl: profile.avatarUrl, + }), + eventTypesLockedByOrg: parentOrgHasLockedEventTypes, + }, + metadata: { + membershipCount: 1, + readOnly: false, + }, + }); + } - const filteredEventTypeGroups = new EventTypeGroupFilter(eventTypeGroups, teamPermissionsMap) - .has("eventType.read") - .get(); + eventTypeGroups = ([] as EventTypeGroup[]).concat( + eventTypeGroups, + await Promise.all( + memberships + .filter((mmship) => { + if (mmship.team.isOrganization) { + return false; + } else { + if (!filters || !hasFilter(filters)) { + return true; + } + return filters?.teamIds?.includes(mmship?.team?.id || 0) ?? false; + } + }) + .map(async (membership) => { + const orgMembership = teamMemberships.find( + (teamM) => teamM.teamId === membership.team.parentId + )?.membershipRole; - // Process profiles with permissions - const profileProcessor = new ProfilePermissionProcessor(); - const profiles = profileProcessor.processProfiles(eventTypeGroups, teamPermissionsMap); + const team = { + ...membership.team, + metadata: teamMetadataSchema.parse(membership.team.metadata), + }; - return { - eventTypeGroups: filteredEventTypeGroups, - profiles, + let slug; + + if (forRoutingForms) { + // For Routing form we want to ensure that after migration of team to an org, the URL remains same for the team + // Once we solve this https://github.com/calcom/cal.com/issues/12399, we can remove this conditional change in slug + slug = `team/${team.slug}`; + } else { + // In an Org, a team can be accessed without /team prefix as well as with /team prefix + slug = team.slug ? (!team.parentId ? `team/${team.slug}` : `${team.slug}`) : null; + } + + // const eventTypes = await Promise.all(team.eventTypes.map(mapEventType)); + const teamParentMetadata = team.parent ? teamMetadataSchema.parse(team.parent.metadata) : null; + return { + teamId: team.id, + parentId: team.parentId, + bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? teamParentMetadata?.requestedSlug ?? null), + membershipRole: + orgMembership && compareMembership(orgMembership, membership.role) + ? orgMembership + : membership.role, + profile: { + image: team.parent + ? getPlaceholderAvatar(team.parent.logoUrl, team.parent.name) + : getPlaceholderAvatar(team.logoUrl, team.name), + name: team.name, + slug, + }, + metadata: { + membershipCount: 0, + readOnly: + membership.role === + (team.parentId + ? orgMembership && compareMembership(orgMembership, membership.role) + ? orgMembership + : MembershipRole.MEMBER + : MembershipRole.MEMBER), + }, + }; + }) + ) + ); + + const denormalizedPayload = { + eventTypeGroups, + // so we can show a dropdown when the user has teams + profiles: eventTypeGroups.map((group) => ({ + ...group.profile, + ...group.metadata, + teamId: group.teamId, + membershipRole: group.membershipRole, + })), }; + + return denormalizedPayload; }; -// Re-export the compareMembership function for backward compatibility -export { compareMembership } from "@calcom/features/eventtypes/lib/getEventTypesByViewer"; +export function compareMembership(mship1: MembershipRole, mship2: MembershipRole) { + const mshipToNumber = (mship: MembershipRole) => + Object.keys(MembershipRole).findIndex((mmship) => mmship === mship); + return mshipToNumber(mship1) > mshipToNumber(mship2); +} diff --git a/packages/trpc/server/routers/viewer/eventTypes/heavy/_router.ts b/packages/trpc/server/routers/viewer/eventTypes/heavy/_router.ts index 14b2bfdd6090f0..5bc5353dccb877 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/heavy/_router.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/heavy/_router.ts @@ -1,12 +1,22 @@ -import { MembershipRole } from "@calcom/prisma/enums"; +import { z } from "zod"; + +import { logP } from "@calcom/lib/perf"; import authedProcedure from "../../../../procedures/authedProcedure"; import { router } from "../../../../trpc"; -import { createEventPbacProcedure } from "../util"; import { ZCreateInputSchema } from "./create.schema"; import { ZDuplicateInputSchema } from "./duplicate.schema"; +import { eventOwnerProcedure } from "../util"; import { ZUpdateInputSchema } from "./update.schema"; +type BookingsRouterHandlerCache = { + create?: typeof import("./create.handler").createHandler; + duplicate?: typeof import("./duplicate.handler").duplicateHandler; + update?: typeof import("./update.handler").updateHandler; +}; + +const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {}; + export const eventTypesRouter = router({ create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => { const { createHandler } = await import("./create.handler"); @@ -16,24 +26,20 @@ export const eventTypesRouter = router({ input, }); }), - duplicate: createEventPbacProcedure("eventType.create", [MembershipRole.ADMIN, MembershipRole.OWNER]) - .input(ZDuplicateInputSchema) - .mutation(async ({ ctx, input }) => { - const { duplicateHandler } = await import("./duplicate.handler"); - - return duplicateHandler({ - ctx, - input, - }); - }), - update: createEventPbacProcedure("eventType.update", [MembershipRole.ADMIN, MembershipRole.OWNER]) - .input(ZUpdateInputSchema) - .mutation(async ({ ctx, input }) => { - const { updateHandler } = await import("./update.handler"); - - return updateHandler({ - ctx, - input, - }); - }), + duplicate: eventOwnerProcedure.input(ZDuplicateInputSchema).mutation(async ({ ctx, input }) => { + const { duplicateHandler } = await import("./duplicate.handler"); + + return duplicateHandler({ + ctx, + input, + }); + }), + update: eventOwnerProcedure.input(ZUpdateInputSchema).mutation(async ({ ctx, input }) => { + const { updateHandler } = await import("./update.handler"); + + return updateHandler({ + ctx, + input, + }); + }) }); diff --git a/packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.ts index d1ab682fc2a23a..7dfe8cbe4f8824 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.ts @@ -2,11 +2,10 @@ import type { z } from "zod"; import { getDefaultLocations } from "@calcom/app-store/_utils/getDefaultLocations"; import { DailyLocationType } from "@calcom/app-store/constants"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository"; import type { PrismaClient } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; -import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; +import { SchedulingType } from "@calcom/prisma/enums"; import type { eventTypeLocations } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; @@ -54,19 +53,6 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { const isManagedEventType = schedulingType === SchedulingType.MANAGED; const isOrgAdmin = !!ctx.user?.organization?.isOrgAdmin; - const permissionService = new PermissionCheckService(); - // Check if user has organization-level eventType.create permission (equivalent to org admin for event types) - let hasOrgEventTypeCreatePermission = isOrgAdmin; // Default fallback - - if (ctx.user.organizationId) { - hasOrgEventTypeCreatePermission = await permissionService.checkPermission({ - userId, - teamId: ctx.user.organizationId, - permission: "eventType.create", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - } - const locations: EventTypeLocation[] = inputLocations && inputLocations.length !== 0 ? inputLocations : await getDefaultLocations(ctx.user); @@ -97,20 +83,22 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { } if (teamId && schedulingType) { - const isSystemAdmin = ctx.user.role === "ADMIN"; - - // Only check for team-level permissions - this will also check for membership - const hasCreatePermission = await permissionService.checkPermission({ - userId, - teamId, - permission: "eventType.create", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], + const hasMembership = await ctx.prisma.membership.findFirst({ + where: { + userId, + teamId: teamId, + accepted: true, + }, }); - if (!isSystemAdmin && !hasOrgEventTypeCreatePermission && !hasCreatePermission) { - // If none of the above conditions are met, the user is unauthorized. - // which means the user is not admin of the team nor the org. - console.warn(`User ${userId} does not have eventType.create permission for team ${teamId}`); + const isSystemAdmin = ctx.user.role === "ADMIN"; + + if ( + !isSystemAdmin && + !isOrgAdmin && + (!hasMembership?.role || !["ADMIN", "OWNER"].includes(hasMembership.role)) + ) { + console.warn(`User ${userId} does not have permission to create this new event type`); throw new TRPCError({ code: "UNAUTHORIZED" }); } @@ -122,9 +110,9 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { data.schedulingType = schedulingType; } - // If we are in an organization & they don't have org-level eventType.create permission & they are not creating an event on a teamID + // If we are in an organization & they are not admin & they are not creating an event on a teamID // Check if evenTypes are locked. - if (ctx.user.organizationId && !hasOrgEventTypeCreatePermission && !teamId) { + if (ctx.user.organizationId && !ctx.user?.organization?.isOrgAdmin && !teamId) { const orgSettings = await ctx.prisma.organizationSettings.findUnique({ where: { organizationId: ctx.user.organizationId, diff --git a/packages/trpc/server/routers/viewer/eventTypes/teamAccessUseCase.test.ts b/packages/trpc/server/routers/viewer/eventTypes/teamAccessUseCase.test.ts deleted file mode 100644 index 3c8132c4a348d2..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/teamAccessUseCase.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -import { MembershipRole } from "@calcom/prisma/enums"; - -import { TeamAccessUseCase } from "./teamAccessUseCase"; - -// Mock the PermissionCheckService -vi.mock("@calcom/features/pbac/services/permission-check.service", () => ({ - PermissionCheckService: vi.fn().mockImplementation(() => ({ - checkPermission: vi.fn(), - })), -})); - -describe("TeamAccessUseCase", () => { - let teamAccessUseCase: TeamAccessUseCase; - let mockCheckPermission: any; - - beforeEach(() => { - vi.clearAllMocks(); - teamAccessUseCase = new TeamAccessUseCase(); - mockCheckPermission = (teamAccessUseCase as any).permissionCheckService.checkPermission; - }); - - describe("filterTeamsByEventTypeReadPermission", () => { - const createMockMembership = (teamId: number, isOrganization = false) => ({ - id: teamId * 100, - teamId, - userId: 1, - accepted: true, - role: MembershipRole.MEMBER, - disableImpersonation: false, - createdAt: new Date(), - updatedAt: new Date(), - customRoleId: null, - team: { - id: teamId, - name: `Team ${teamId}`, - slug: `team-${teamId}`, - logoUrl: null, - isOrganization, - parentId: isOrganization ? null : 1, - metadata: {}, - theme: null, - brandColor: null, - darkBrandColor: null, - bio: null, - hideBranding: false, - hideBookATeamMember: false, - isPrivate: false, - parent: null, - calVideoLogo: null, - appLogo: null, - appIconLogo: null, - bannerUrl: null, - timeFormat: null, - timeZone: "UTC", - weekStart: "Monday", - createdAt: new Date(), - pendingPayment: false, - isPlatform: false, - smsLockState: "UNLOCKED" as const, - smsLockReviewedByAdmin: false, - includeManagedEventsInLimits: false, - bookingLimits: null, - rrResetInterval: "MONTH" as const, - rrTimestampBasis: "CREATED_AT" as const, - createdByOAuthClientId: null, - hideTeamProfileLink: false, - }, - }); - - it("should filter out organization memberships", async () => { - const memberships = [ - createMockMembership(1, false), - createMockMembership(2, true), // Organization - createMockMembership(3, false), - ]; - - mockCheckPermission.mockResolvedValue(true); - - const result = await teamAccessUseCase.filterTeamsByEventTypeReadPermission(memberships as any, 1); - - expect(result).toHaveLength(2); - expect(result.map((m) => m.teamId)).toEqual([1, 3]); - }); - - it("should filter out teams without eventType.read permission when PBAC is enabled", async () => { - const memberships = [ - createMockMembership(1, false), - createMockMembership(2, false), - createMockMembership(3, false), - ]; - - mockCheckPermission - .mockResolvedValueOnce(true) // Team 1 has permission - .mockResolvedValueOnce(false) // Team 2 does not have permission - .mockResolvedValueOnce(true); // Team 3 has permission - - const result = await teamAccessUseCase.filterTeamsByEventTypeReadPermission(memberships as any, 1); - - expect(result).toHaveLength(2); - expect(result.map((m) => m.teamId)).toEqual([1, 3]); - expect(mockCheckPermission).toHaveBeenCalledTimes(3); - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 1, - permission: "eventType.read", - fallbackRoles: ["ADMIN", "OWNER", "MEMBER"], - }); - }); - - it("should return all non-organization teams when PBAC returns true for all", async () => { - const memberships = [createMockMembership(1, false), createMockMembership(2, false)]; - - mockCheckPermission.mockResolvedValue(true); - - const result = await teamAccessUseCase.filterTeamsByEventTypeReadPermission(memberships as any, 1); - - expect(result).toHaveLength(2); - expect(result).toEqual(memberships); - }); - - it("should return empty array when no teams have permission", async () => { - const memberships = [createMockMembership(1, false), createMockMembership(2, false)]; - - mockCheckPermission.mockResolvedValue(false); - - const result = await teamAccessUseCase.filterTeamsByEventTypeReadPermission(memberships as any, 1); - - expect(result).toHaveLength(0); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/eventTypes/teamAccessUseCase.ts b/packages/trpc/server/routers/viewer/eventTypes/teamAccessUseCase.ts deleted file mode 100644 index 6c160027c1732c..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/teamAccessUseCase.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import type { Membership, Team } from "@calcom/prisma/client"; - -type TeamMembershipWithTeam = Membership & { - team: Team & { - parent?: { - id: number; - name: string; - slug: string | null; - logoUrl: string | null; - parentId: number | null; - metadata: any; - } | null; - }; -}; - -export class TeamAccessUseCase { - constructor(private permissionCheckService: PermissionCheckService = new PermissionCheckService()) {} - - async filterTeamsByEventTypeReadPermission( - memberships: TeamMembershipWithTeam[], - userId: number - ): Promise { - const filteredMemberships = await Promise.all( - memberships.map(async (membership) => { - // Organization memberships are excluded from this logic - if (membership.team.isOrganization) { - return null; - } - - // Check if user has eventType.read permission for this team - const hasPermission = await this.permissionCheckService.checkPermission({ - userId, - teamId: membership.team.id, - permission: "eventType.read", - fallbackRoles: ["ADMIN", "OWNER", "MEMBER"], // All roles can read event types by default - }); - - return hasPermission ? membership : null; - }) - ); - - return filteredMemberships.filter( - (membership): membership is TeamMembershipWithTeam => membership !== null - ); - } -} diff --git a/packages/trpc/server/routers/viewer/eventTypes/usecases/EventGroupBuilder.ts b/packages/trpc/server/routers/viewer/eventTypes/usecases/EventGroupBuilder.ts deleted file mode 100644 index 0f8c47a3d5cf94..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/usecases/EventGroupBuilder.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { MembershipRepository } from "@calcom/lib/server/repository/membership"; -import type { ProfileRepository } from "@calcom/lib/server/repository/profile"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -import type { TeamAccessUseCase } from "../teamAccessUseCase"; -import { - shouldListUserEvents, - shouldIncludeTeamMembership, - createTeamSlug, - type FilterContext, -} from "../utils/filterUtils"; -import { buildTeamPermissionsMap, getEffectiveRole, type TeamPermissions } from "../utils/permissionUtils"; -import { createUserEventGroup, createTeamEventGroup, type EventTypeGroup } from "../utils/transformUtils"; - -export interface EventGroupBuilderDependencies { - membershipRepository: typeof MembershipRepository; - profileRepository: typeof ProfileRepository; - teamAccessUseCase: TeamAccessUseCase; -} - -export interface EventGroupBuilderInput { - userId: number; - userUpId: string; - filters?: FilterContext["filters"]; - forRoutingForms?: boolean; -} - -export class EventGroupBuilder { - constructor(private dependencies: EventGroupBuilderDependencies) {} - - async buildEventGroups(input: EventGroupBuilderInput): Promise<{ - eventTypeGroups: EventTypeGroup[]; - teamPermissionsMap: Map; - }> { - const { userId, userUpId, filters, forRoutingForms } = input; - - // Get user profile - const profile = await this.dependencies.profileRepository.findByUpId(userUpId); - if (!profile) { - throw new Error("Profile not found"); - } - - const parentOrgHasLockedEventTypes = - profile.organization?.organizationSettings?.lockEventTypeCreationForUsers; - - // Get memberships - const profileMemberships = await this.dependencies.membershipRepository.findAllByUpIdIncludeTeam( - { upId: userUpId }, - { where: { accepted: true } } - ); - - // Filter memberships based on PBAC permissions - const accessibleMemberships = - await this.dependencies.teamAccessUseCase.filterTeamsByEventTypeReadPermission( - profileMemberships, - userId - ); - - if (!accessibleMemberships) { - throw new Error("Failed to filter team memberships"); - } - - const memberships = accessibleMemberships.map((membership) => ({ - ...membership, - team: { - ...membership.team, - metadata: teamMetadataSchema.parse(membership.team.metadata), - }, - })); - - const teamMemberships = accessibleMemberships.map((membership) => ({ - teamId: membership.team.id, - membershipRole: membership.role, - })); - - // Build permissions map - const teamPermissionsMap = await buildTeamPermissionsMap(memberships, teamMemberships, userId); - - // Build event type groups - const eventTypeGroups: EventTypeGroup[] = []; - - // Add user events if needed - const filterContext: FilterContext = { filters, userUpId }; - if (shouldListUserEvents(filterContext)) { - const userGroup = await createUserEventGroup(profile, parentOrgHasLockedEventTypes ?? false); - eventTypeGroups.push(userGroup); - } - - // Add team events - const teamGroupResults = await Promise.allSettled( - memberships - .filter((membership) => shouldIncludeTeamMembership(membership, filters)) - .map(async (membership) => { - const orgMembership = teamMemberships.find( - (teamM) => teamM.teamId === membership.team.parentId - )?.membershipRole; - - const effectiveRole = getEffectiveRole(orgMembership, membership.role); - const permissions = teamPermissionsMap.get(membership.team.id); - - if (!permissions) { - throw new Error(`Permissions not found for team ${membership.team.id}`); - } - - const teamSlug = createTeamSlug( - membership.team.slug, - !!membership.team.parentId, - forRoutingForms ?? false - ); - - return createTeamEventGroup(membership, effectiveRole, teamSlug, permissions); - }) - ); - - const teamGroups = teamGroupResults - .filter((result): result is PromiseFulfilledResult => result.status === "fulfilled") - .map((result) => result.value); - - const failedTeams = teamGroupResults.filter( - (result): result is PromiseRejectedResult => result.status === "rejected" - ); - - if (failedTeams.length > 0) { - console.warn(`Failed to process ${failedTeams.length} teams:`, failedTeams); - } - - eventTypeGroups.push(...teamGroups); - - return { - eventTypeGroups, - teamPermissionsMap, - }; - } -} diff --git a/packages/trpc/server/routers/viewer/eventTypes/usecases/ProfilePermissionProcessor.ts b/packages/trpc/server/routers/viewer/eventTypes/usecases/ProfilePermissionProcessor.ts deleted file mode 100644 index 345f4b9079a187..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/usecases/ProfilePermissionProcessor.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { TeamPermissions } from "../utils/permissionUtils"; -import { - createProfilesWithPermissions, - type EventTypeGroup, - type ProfileWithPermissions, -} from "../utils/transformUtils"; - -export class ProfilePermissionProcessor { - processProfiles( - eventTypeGroups: EventTypeGroup[], - teamPermissionsMap: Map - ): ProfileWithPermissions[] { - return createProfilesWithPermissions(eventTypeGroups, teamPermissionsMap); - } -} diff --git a/packages/trpc/server/routers/viewer/eventTypes/util.ts b/packages/trpc/server/routers/viewer/eventTypes/util.ts index 93e1a27590a12a..5f32253a33d4c0 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/util.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/util.ts @@ -1,12 +1,10 @@ import { z } from "zod"; -import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; +import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import type { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository"; import { UserRepository } from "@calcom/lib/server/repository/user"; import prisma from "@calcom/prisma"; -import type { MembershipRole } from "@calcom/prisma/enums"; import { PeriodType } from "@calcom/prisma/enums"; import type { CustomInputSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; @@ -61,7 +59,13 @@ export const eventOwnerProcedure = authedProcedure const isAuthorized = (function () { if (event.team) { - return event.team.members.map((member) => member.userId).includes(ctx.user.id); + const isOrgAdmin = !!ctx.user?.organization?.isOrgAdmin; + return ( + event.team.members + .filter((member) => checkAdminOrOwner(member.role)) + .map((member) => member.userId) + .includes(ctx.user.id) || isOrgAdmin + ); } return event.userId === ctx.user.id || event.users.find((user) => user.id === ctx.user.id); })(); @@ -88,62 +92,6 @@ export const eventOwnerProcedure = authedProcedure return next(); }); -/** - * Creates an event admin procedure with configurable permissions - * @param permission - The specific permission required (e.g., "eventType.manage", "eventType.update") - * @param fallbackRoles - Roles to check when PBAC is disabled (defaults to ["ADMIN", "OWNER"]) - * @returns A procedure that checks the specified permission - */ -export const createEventPbacProcedure = ( - permission: PermissionString, - fallbackRoles: MembershipRole[] = ["ADMIN", "OWNER"] -) => { - return eventOwnerProcedure.use(async ({ ctx, input, next }) => { - const id = input.eventTypeId ?? input.id; - - const event = await ctx.prisma.eventType.findUnique({ - where: { id }, - select: { - id: true, - userId: true, - teamId: true, - }, - }); - - if (!event) { - throw new TRPCError({ code: "NOT_FOUND" }); - } - - // For team events, check PBAC permissions - if (event.teamId) { - const permissionCheckService = new PermissionCheckService(); - const hasPermission = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId: event.teamId, - permission, - fallbackRoles, - }); - - if (!hasPermission) { - throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, - }); - } - } else { - // For personal events, only the owner can manage - if (event.userId !== ctx.user.id) { - throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, - }); - } - } - - return next(); - }); -}; - export function isPeriodType(keyInput: string): keyInput is PeriodType { return Object.keys(PeriodType).includes(keyInput); } diff --git a/packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts b/packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts deleted file mode 100644 index 31694a9605bbfe..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { MembershipRole } from "@calcom/prisma/enums"; - -import { filterEvents } from "./EventTypeGroupFilter"; -import type { TeamPermissions } from "./permissionUtils"; -import type { EventTypeGroup } from "./transformUtils"; - -describe("EventTypeGroupFilter", () => { - const mockUserGroup: EventTypeGroup = { - teamId: null, - bookerUrl: "https://cal.com/user", - membershipRole: null, - profile: { - slug: "user", - name: "User", - image: "avatar.jpg", - }, - metadata: { - membershipCount: 1, - readOnly: false, - }, - }; - - const mockTeamGroup1: EventTypeGroup = { - teamId: 1, - bookerUrl: "https://cal.com/team1", - membershipRole: MembershipRole.ADMIN, - profile: { - slug: "team1", - name: "Team 1", - image: "team1.jpg", - }, - metadata: { - membershipCount: 5, - readOnly: false, - }, - }; - - const mockTeamGroup2: EventTypeGroup = { - teamId: 2, - bookerUrl: "https://cal.com/team2", - membershipRole: MembershipRole.MEMBER, - profile: { - slug: "team2", - name: "Team 2", - image: "team2.jpg", - }, - metadata: { - membershipCount: 3, - readOnly: true, - }, - }; - - const mockPermissionsMap = new Map([ - [1, { canCreate: true, canEdit: true, canDelete: true, canRead: true }], - [2, { canCreate: false, canEdit: true, canDelete: false, canRead: true }], - ]); - - const allGroups = [mockUserGroup, mockTeamGroup1, mockTeamGroup2]; - - describe("has() method", () => { - it("should filter groups with eventType.create permission", () => { - const result = filterEvents(allGroups, mockPermissionsMap).has("eventType.create").get(); - - expect(result).toHaveLength(2); - expect(result).toContain(mockUserGroup); // User groups always have permissions - expect(result).toContain(mockTeamGroup1); // Team 1 has create permission - expect(result).not.toContain(mockTeamGroup2); // Team 2 doesn't have create permission - }); - - it("should filter groups with eventType.read permission", () => { - const result = filterEvents(allGroups, mockPermissionsMap).has("eventType.read").get(); - - expect(result).toHaveLength(3); // All groups can read - expect(result).toContain(mockUserGroup); - expect(result).toContain(mockTeamGroup1); - expect(result).toContain(mockTeamGroup2); - }); - - it("should filter groups with eventType.update permission", () => { - const result = filterEvents(allGroups, mockPermissionsMap).has("eventType.update").get(); - - expect(result).toHaveLength(3); // All groups can edit in this test - expect(result).toContain(mockUserGroup); - expect(result).toContain(mockTeamGroup1); - expect(result).toContain(mockTeamGroup2); - }); - - it("should filter groups with eventType.delete permission", () => { - const result = filterEvents(allGroups, mockPermissionsMap).has("eventType.delete").get(); - - expect(result).toHaveLength(2); - expect(result).toContain(mockUserGroup); - expect(result).toContain(mockTeamGroup1); - expect(result).not.toContain(mockTeamGroup2); - }); - - it("should return empty array for unknown permission", () => { - const teamOnlyGroups = [mockTeamGroup1, mockTeamGroup2]; - const result = filterEvents(teamOnlyGroups, mockPermissionsMap) - .has("unknown.permission" as "eventType.read") - .get(); - - expect(result).toHaveLength(0); - }); - }); - - describe("byTeam() method", () => { - it("should filter groups by team ID", () => { - const result = filterEvents(allGroups, mockPermissionsMap).byTeam(1).get(); - - expect(result).toHaveLength(1); - expect(result).toContain(mockTeamGroup1); - }); - - it("should return empty array for non-existent team", () => { - const result = filterEvents(allGroups, mockPermissionsMap).byTeam(999).get(); - - expect(result).toHaveLength(0); - }); - }); - - describe("userOnly() method", () => { - it("should return only user groups", () => { - const result = filterEvents(allGroups, mockPermissionsMap).userOnly().get(); - - expect(result).toHaveLength(1); - expect(result).toContain(mockUserGroup); - }); - }); - - describe("teamsOnly() method", () => { - it("should return only team groups", () => { - const result = filterEvents(allGroups, mockPermissionsMap).teamsOnly().get(); - - expect(result).toHaveLength(2); - expect(result).toContain(mockTeamGroup1); - expect(result).toContain(mockTeamGroup2); - expect(result).not.toContain(mockUserGroup); - }); - }); - - describe("readOnly() method", () => { - it("should filter by read-only status", () => { - const readOnlyResult = filterEvents(allGroups, mockPermissionsMap).readOnly(true).get(); - const writableResult = filterEvents(allGroups, mockPermissionsMap).readOnly(false).get(); - - expect(readOnlyResult).toHaveLength(1); - expect(readOnlyResult).toContain(mockTeamGroup2); - - expect(writableResult).toHaveLength(2); - expect(writableResult).toContain(mockUserGroup); - expect(writableResult).toContain(mockTeamGroup1); - }); - }); - - describe("chaining methods", () => { - it("should support method chaining", () => { - const result = filterEvents(allGroups, mockPermissionsMap).teamsOnly().has("eventType.create").get(); - - expect(result).toHaveLength(1); - expect(result).toContain(mockTeamGroup1); - }); - - it("should support complex chaining", () => { - const result = filterEvents(allGroups, mockPermissionsMap) - .has("eventType.update") - .readOnly(false) - .get(); - - expect(result).toHaveLength(2); - expect(result).toContain(mockUserGroup); - expect(result).toContain(mockTeamGroup1); - }); - }); - - describe("utility methods", () => { - it("should return correct count", () => { - const count = filterEvents(allGroups, mockPermissionsMap).teamsOnly().count(); - - expect(count).toBe(2); - }); - - it("should check existence correctly", () => { - const exists = filterEvents(allGroups, mockPermissionsMap).byTeam(1).exists(); - const notExists = filterEvents(allGroups, mockPermissionsMap).byTeam(999).exists(); - - expect(exists).toBe(true); - expect(notExists).toBe(false); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts b/packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts deleted file mode 100644 index 410bee7acfbef4..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry"; - -import type { TeamPermissions } from "./permissionUtils"; -import type { EventTypeGroup } from "./transformUtils"; - -export class EventTypeGroupFilter { - private groups: EventTypeGroup[]; - private permissionsMap: Map; - - constructor(groups: EventTypeGroup[], permissionsMap: Map) { - this.groups = groups; - this.permissionsMap = permissionsMap; - } - - /** - * Filter event type groups by permission - * @param permission - Permission to check (e.g., "eventType.read", "eventType.create", "eventType.update", "eventType.delete") - * @returns New EventTypeGroupFilter instance with filtered groups - */ - has(permission: PermissionString): EventTypeGroupFilter { - const filteredGroups = this.groups.filter((group) => { - // For user groups (no teamId), they have all permissions - if (!group.teamId) { - return true; - } - - const permissions = this.permissionsMap.get(group.teamId); - if (!permissions) { - return false; - } - - switch (permission) { - case "eventType.read": - // All groups with permissions can read - return permissions.canRead; - case "eventType.create": - return permissions.canCreate; - case "eventType.update": - return permissions.canEdit; - case "eventType.delete": - return permissions.canDelete; - default: - return false; - } - }); - - return new EventTypeGroupFilter(filteredGroups, this.permissionsMap); - } - - /** - * Filter by team ID - * @param teamId - Team ID to filter by - * @returns New EventTypeGroupFilter instance with filtered groups - */ - byTeam(teamId: number): EventTypeGroupFilter { - const filteredGroups = this.groups.filter((group) => group.teamId === teamId); - return new EventTypeGroupFilter(filteredGroups, this.permissionsMap); - } - - /** - * Filter by user groups only (no team) - * @returns New EventTypeGroupFilter instance with user groups only - */ - userOnly(): EventTypeGroupFilter { - const filteredGroups = this.groups.filter((group) => !group.teamId); - return new EventTypeGroupFilter(filteredGroups, this.permissionsMap); - } - - /** - * Filter by team groups only (exclude user groups) - * @returns New EventTypeGroupFilter instance with team groups only - */ - teamsOnly(): EventTypeGroupFilter { - const filteredGroups = this.groups.filter((group) => group.teamId); - return new EventTypeGroupFilter(filteredGroups, this.permissionsMap); - } - - /** - * Filter by read-only status - * @param readOnly - Whether to include only read-only or writable groups - * @returns New EventTypeGroupFilter instance with filtered groups - */ - readOnly(readOnly: boolean): EventTypeGroupFilter { - const filteredGroups = this.groups.filter((group) => group.metadata.readOnly === readOnly); - return new EventTypeGroupFilter(filteredGroups, this.permissionsMap); - } - - /** - * Get the filtered results - * @returns Array of filtered EventTypeGroup objects - */ - get(): EventTypeGroup[] { - return this.groups; - } - - /** - * Get the count of filtered groups - * @returns Number of groups after filtering - */ - count(): number { - return this.groups.length; - } - - /** - * Check if any groups match the current filter - * @returns Boolean indicating if any groups exist - */ - exists(): boolean { - return this.groups.length > 0; - } -} - -/** - * Create a new EventTypeGroupFilter instance - * @param groups - Array of EventTypeGroup objects - * @param permissionsMap - Map of team permissions - * @returns EventTypeGroupFilter instance for chaining - */ -export function filterEvents( - groups: EventTypeGroup[], - permissionsMap: Map -): EventTypeGroupFilter { - return new EventTypeGroupFilter(groups, permissionsMap); -} diff --git a/packages/trpc/server/routers/viewer/eventTypes/utils/filterUtils.ts b/packages/trpc/server/routers/viewer/eventTypes/utils/filterUtils.ts deleted file mode 100644 index bae68cfc7d3715..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/utils/filterUtils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; - -import type { TEventTypeInputSchema } from "../getByViewer.schema"; - -type FiltersType = NonNullable["filters"]; - -export interface FilterContext { - filters?: FiltersType; - userUpId: string; -} - -export function shouldListUserEvents(context: FilterContext): boolean { - const { filters, userUpId } = context; - const isFilterSet = filters && hasFilter(filters); - const isUpIdInFilter = filters?.upIds?.includes(userUpId) ?? false; - - let shouldList = !isFilterSet || isUpIdInFilter; - - if (isFilterSet && filters?.upIds && !isUpIdInFilter) { - shouldList = true; - } - - return shouldList; -} - -export function shouldIncludeTeamMembership( - membership: { team: { id: number } }, - filters?: FiltersType -): boolean { - if (!filters || !hasFilter(filters)) { - return true; - } - - return filters?.teamIds?.includes(membership.team.id) ?? false; -} - -export function createTeamSlug( - teamSlug: string | null, - hasParent: boolean, - forRoutingForms: boolean -): string | null { - if (!teamSlug) return null; - - if (forRoutingForms) { - return `team/${teamSlug}`; - } - - return hasParent ? teamSlug : `team/${teamSlug}`; -} diff --git a/packages/trpc/server/routers/viewer/eventTypes/utils/permissionUtils.ts b/packages/trpc/server/routers/viewer/eventTypes/utils/permissionUtils.ts deleted file mode 100644 index 041d06fa06d7ea..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/utils/permissionUtils.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; -import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; -import { MembershipRole } from "@calcom/prisma/enums"; - -export interface TeamPermissions { - canCreate: boolean; - canEdit: boolean; - canDelete: boolean; - canRead: boolean; -} - -export interface MembershipWithRole { - teamId: number; - membershipRole: MembershipRole; -} - -const MEMBERSHIP_HIERARCHY: Record = { - [MembershipRole.MEMBER]: 1, - [MembershipRole.ADMIN]: 2, - [MembershipRole.OWNER]: 3, -}; - -export function hasHigherPrivilege(role1: MembershipRole, role2: MembershipRole): boolean { - return MEMBERSHIP_HIERARCHY[role1] > MEMBERSHIP_HIERARCHY[role2]; -} - -export function getEffectiveRole( - orgMembership: MembershipRole | undefined, - membershipRole: MembershipRole -): MembershipRole { - return orgMembership && hasHigherPrivilege(orgMembership, membershipRole) ? orgMembership : membershipRole; -} - -export async function getTeamPermissions( - userId: number, - teamId: number, - effectiveRole: MembershipRole -): Promise { - try { - const permissions = await getResourcePermissions({ - userId, - teamId, - resource: Resource.EventType, - userRole: effectiveRole, - fallbackRoles: { - read: { - roles: [MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER], - }, - create: { - roles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }, - update: { - roles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }, - delete: { - roles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }, - }, - }); - - return { - canCreate: permissions.canCreate, - canEdit: permissions.canEdit, - canDelete: permissions.canDelete, - canRead: permissions.canRead, - }; - } catch (error) { - console.warn( - `PBAC check failed for user ${userId} on team ${teamId}, falling back to role check:`, - error - ); - - return getFallbackPermissions(effectiveRole); - } -} - -function getFallbackPermissions(role: MembershipRole): TeamPermissions { - const isAdminOrOwner = role === MembershipRole.ADMIN || role === MembershipRole.OWNER; - const isMember = role === MembershipRole.MEMBER; - - return { - canRead: isAdminOrOwner || isMember, - canCreate: isAdminOrOwner, - canEdit: isAdminOrOwner, - canDelete: isAdminOrOwner, - }; -} - -export async function buildTeamPermissionsMap( - memberships: Array<{ team: { id: number; parentId?: number | null }; role: MembershipRole }>, - teamMemberships: MembershipWithRole[], - userId: number -): Promise> { - const permissionPromises = memberships.map(async (membership) => { - const orgMembership = teamMemberships.find( - (teamM) => teamM.teamId === membership.team.parentId - )?.membershipRole; - - const effectiveRole = getEffectiveRole(orgMembership, membership.role); - const permissions = await getTeamPermissions(userId, membership.team.id, effectiveRole); - - return [membership.team.id, permissions] as const; - }); - - const results = await Promise.all(permissionPromises); - return new Map(results); -} diff --git a/packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts b/packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts deleted file mode 100644 index f4dc09403740c0..00000000000000 --- a/packages/trpc/server/routers/viewer/eventTypes/utils/transformUtils.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; -import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; -import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; -import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; -import type { MembershipRole } from "@calcom/prisma/enums"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -import type { TeamPermissions } from "./permissionUtils"; - -export interface EventTypeGroup { - teamId?: number | null; - parentId?: number | null; - bookerUrl: string; - membershipRole?: MembershipRole | null; - profile: { - slug: string | null; - name: string | null; - image: string; - eventTypesLockedByOrg?: boolean; - }; - metadata: { - membershipCount: number; - readOnly: boolean; - }; -} - -export interface ProfileWithPermissions { - slug: string | null; - name: string | null; - image: string; - eventTypesLockedByOrg?: boolean; - membershipCount: number; - readOnly: boolean; - teamId?: number | null; - membershipRole?: MembershipRole | null; - canCreateEventTypes?: boolean; - canUpdateEventTypes?: boolean; - canDeleteEventTypes?: boolean; -} - -export async function createUserEventGroup( - profile: { - username: string | null; - name: string | null; - avatarUrl: string | null; - organizationId: number | null; - }, - parentOrgHasLockedEventTypes: boolean -): Promise { - const bookerUrl = await getBookerBaseUrl(profile.organizationId); - - return { - teamId: null, - bookerUrl, - membershipRole: null, - profile: { - slug: profile.username, - name: profile.name, - image: getUserAvatarUrl({ - avatarUrl: profile.avatarUrl, - }), - eventTypesLockedByOrg: parentOrgHasLockedEventTypes, - }, - metadata: { - membershipCount: 1, - readOnly: false, - }, - }; -} - -export async function createTeamEventGroup( - membership: { - team: { - id: number; - name: string; - slug: string | null; - logoUrl: string | null; - parentId: number | null; - parent?: { - name: string; - slug: string | null; - logoUrl: string | null; - metadata: unknown; - } | null; - metadata: unknown; - }; - role: MembershipRole; - }, - effectiveRole: MembershipRole, - teamSlug: string | null, - permissions: TeamPermissions -): Promise { - const team = { - ...membership.team, - metadata: teamMetadataSchema.parse(membership.team.metadata), - }; - - const teamParentMetadata = team.parent ? teamMetadataSchema.parse(team.parent.metadata) : null; - - return { - teamId: team.id, - parentId: team.parentId, - bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? teamParentMetadata?.requestedSlug ?? null), - membershipRole: effectiveRole, - profile: { - image: team.parent - ? getPlaceholderAvatar(team.parent.logoUrl, team.parent.name) - : getPlaceholderAvatar(team.logoUrl, team.name), - name: team.name, - slug: teamSlug, - }, - metadata: { - membershipCount: 0, - readOnly: !permissions.canEdit, - }, - }; -} - -export function createProfilesWithPermissions( - eventTypeGroups: EventTypeGroup[], - teamPermissionsMap: Map -): ProfileWithPermissions[] { - return eventTypeGroups.map((group) => { - let canCreateEventTypes: boolean | undefined = undefined; - let canUpdateEventTypes: boolean | undefined = undefined; - let canDeleteEventTypes: boolean | undefined = undefined; - - if (group.teamId) { - const permissions = teamPermissionsMap.get(group.teamId); - canCreateEventTypes = permissions?.canCreate; - canUpdateEventTypes = permissions?.canEdit; - canDeleteEventTypes = permissions?.canDelete; - } - - return { - ...group.profile, - ...group.metadata, - teamId: group.teamId, - membershipRole: group.membershipRole, - canCreateEventTypes, - canUpdateEventTypes, - canDeleteEventTypes, - }; - }); -} diff --git a/packages/trpc/server/routers/viewer/me/getUserTopBanners.handler.ts b/packages/trpc/server/routers/viewer/me/getUserTopBanners.handler.ts index 16510e3790447f..c301a6a2c8e9d6 100644 --- a/packages/trpc/server/routers/viewer/me/getUserTopBanners.handler.ts +++ b/packages/trpc/server/routers/viewer/me/getUserTopBanners.handler.ts @@ -1,4 +1,4 @@ -import { getCalendarCredentials, getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager"; +import { getCalendarCredentials, getConnectedCalendars } from "@calcom/lib/CalendarManager"; import { buildNonDelegationCredentials } from "@calcom/lib/delegationCredential/server"; import { prisma } from "@calcom/prisma"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; diff --git a/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts b/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts index b6ba852f8fc8b0..14688c6e938067 100644 --- a/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts +++ b/packages/trpc/server/routers/viewer/me/updateProfile.handler.ts @@ -12,7 +12,7 @@ import logger from "@calcom/lib/logger"; import { uploadAvatar } from "@calcom/lib/server/avatar"; import { checkUsername } from "@calcom/lib/server/checkUsername"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; +import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import slugify from "@calcom/lib/slugify"; import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts"; diff --git a/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts b/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts index e2faafb94e010a..7b63ca4ba32334 100644 --- a/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts @@ -1,7 +1,7 @@ import { createHash } from "crypto"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { prisma } from "@calcom/prisma"; import { TRPCError } from "@trpc/server"; diff --git a/packages/trpc/server/routers/viewer/organizations/utils.ts b/packages/trpc/server/routers/viewer/organizations/utils.ts index 260bcbc7fdeccf..313c305ac31eda 100644 --- a/packages/trpc/server/routers/viewer/organizations/utils.ts +++ b/packages/trpc/server/routers/viewer/organizations/utils.ts @@ -1,5 +1,5 @@ import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; +import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; diff --git a/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts b/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts index 2145d2e2192af7..966c8e94e0c2f4 100644 --- a/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts +++ b/packages/trpc/server/routers/viewer/routing-forms/findTeamMembersMatchingAttributeLogicOfRoute.handler.ts @@ -70,7 +70,7 @@ export const findTeamMembersMatchingAttributeLogicOfRouteHandler = async ({ }: FindTeamMembersMatchingAttributeLogicOfRouteHandlerOptions) => { const { prisma, user } = ctx; const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import( - "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm" + "@calcom/lib/server/getTeamMemberEmailFromCrm" ); const { formId, response, route, isPreview, _enablePerf, _concurrency } = input; diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index ea976e7d770e86..71ff80d08a912c 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -15,7 +15,7 @@ import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils"; import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants"; import { buildDateRanges } from "@calcom/lib/date-ranges"; import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs"; -import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents"; +import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import type { getBusyTimesService } from "@calcom/lib/di/containers/BusyTimes"; import { getAggregatedAvailability } from "@calcom/lib/getAggregatedAvailability"; import type { BusyTimesService } from "@calcom/lib/getBusyTimes"; diff --git a/packages/trpc/server/routers/viewer/teams/acceptOrLeave.handler.ts b/packages/trpc/server/routers/viewer/teams/acceptOrLeave.handler.ts index 9bcd76b1acc5fb..0e4a6b67b5876c 100644 --- a/packages/trpc/server/routers/viewer/teams/acceptOrLeave.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/acceptOrLeave.handler.ts @@ -1,5 +1,5 @@ import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser"; -import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; +import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; diff --git a/packages/trpc/server/routers/viewer/teams/addMembersToEventTypes.handler.ts b/packages/trpc/server/routers/viewer/teams/addMembersToEventTypes.handler.ts index 501fd0d7baccaa..b308a16f6ee197 100644 --- a/packages/trpc/server/routers/viewer/teams/addMembersToEventTypes.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/addMembersToEventTypes.handler.ts @@ -1,4 +1,4 @@ -import { isTeamAdmin, isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; diff --git a/packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts b/packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts index 47a7351e4aa752..5fdc873aa961cd 100644 --- a/packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts @@ -1,5 +1,5 @@ import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory"; -import { isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamOwner } from "@calcom/lib/server/queries/teams"; import { TeamRepository } from "@calcom/lib/server/repository/team"; import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; diff --git a/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts b/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts index d066f867af7d52..c17e356dda97e2 100644 --- a/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/createInvite.handler.ts @@ -1,7 +1,7 @@ import { randomBytes } from "crypto"; import { WEBAPP_URL } from "@calcom/lib/constants"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; diff --git a/packages/trpc/server/routers/viewer/teams/delete.handler.ts b/packages/trpc/server/routers/viewer/teams/delete.handler.ts index b5f278686a5bed..ad050fd77cc4e5 100644 --- a/packages/trpc/server/routers/viewer/teams/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/delete.handler.ts @@ -1,4 +1,4 @@ -import { isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamOwner } from "@calcom/lib/server/queries/teams"; import { TeamService } from "@calcom/lib/server/service/teamService"; import { TRPCError } from "@trpc/server"; diff --git a/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts b/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts index 670ed1ddd61a1d..a68dec79a92796 100644 --- a/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/deleteInvite.handler.ts @@ -1,4 +1,4 @@ -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; diff --git a/packages/trpc/server/routers/viewer/teams/get.handler.test.ts b/packages/trpc/server/routers/viewer/teams/get.handler.test.ts index 76ff6b87a58258..492bff6ac6b55a 100644 --- a/packages/trpc/server/routers/viewer/teams/get.handler.test.ts +++ b/packages/trpc/server/routers/viewer/teams/get.handler.test.ts @@ -1,6 +1,6 @@ import { describe, it, beforeEach, vi, expect } from "vitest"; -import { getTeamWithoutMembers } from "@calcom/features/ee/teams/lib/queries"; +import { getTeamWithoutMembers } from "@calcom/lib/server/queries/teams"; import { MembershipRepository } from "@calcom/lib/server/repository/membership"; import type { TrpcSessionUser } from "../../../types"; @@ -12,7 +12,7 @@ vi.mock("@calcom/lib/server/repository/membership", () => ({ }, })); -vi.mock("@calcom/features/ee/teams/lib/queries", () => ({ +vi.mock("@calcom/lib/server/queries/teams", () => ({ getTeamWithoutMembers: vi.fn(), })); diff --git a/packages/trpc/server/routers/viewer/teams/get.handler.ts b/packages/trpc/server/routers/viewer/teams/get.handler.ts index 9590d107efffa2..aa78a898507e14 100644 --- a/packages/trpc/server/routers/viewer/teams/get.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/get.handler.ts @@ -1,4 +1,4 @@ -import { getTeamWithoutMembers } from "@calcom/features/ee/teams/lib/queries"; +import { getTeamWithoutMembers } from "@calcom/lib/server/queries/teams"; import { MembershipRepository } from "@calcom/lib/server/repository/membership"; import { TRPCError } from "@trpc/server"; diff --git a/packages/trpc/server/routers/viewer/teams/getInternalNotesPresets.handler.ts b/packages/trpc/server/routers/viewer/teams/getInternalNotesPresets.handler.ts index 389697de15f8eb..a994ae238511b3 100644 --- a/packages/trpc/server/routers/viewer/teams/getInternalNotesPresets.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/getInternalNotesPresets.handler.ts @@ -1,4 +1,4 @@ -import { isTeamMember } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamMember } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; diff --git a/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts b/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts index 4c42c41b89fb05..45472dbe2e7708 100644 --- a/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/getMemberAvailability.handler.ts @@ -1,6 +1,6 @@ import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server"; import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability"; -import { isTeamMember } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamMember } from "@calcom/lib/server/queries/teams"; import { MembershipRepository } from "@calcom/lib/server/repository/membership"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts index 386325a91d7094..adb66744992291 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMemberUtils.test.ts @@ -1,7 +1,7 @@ import { describe, it, vi, expect } from "vitest"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { MembershipRole } from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; @@ -25,7 +25,7 @@ vi.mock("@calcom/prisma", () => { }; }); -vi.mock("@calcom/features/ee/teams/lib/queries", () => { +vi.mock("@calcom/lib/server/queries/teams", () => { return { isTeamAdmin: vi.fn(), }; diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index b27dbe949614e1..9eef0f33e8d474 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -11,8 +11,8 @@ import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries/teams"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; import { getParsedTeam } from "@calcom/lib/server/repository/teamUtils"; import { UserRepository } from "@calcom/lib/server/repository/user"; diff --git a/packages/trpc/server/routers/viewer/teams/listSimpleMembers.handler.ts b/packages/trpc/server/routers/viewer/teams/listSimpleMembers.handler.ts index 6c22550984d76e..b4d0cf0cbbdf46 100644 --- a/packages/trpc/server/routers/viewer/teams/listSimpleMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/listSimpleMembers.handler.ts @@ -2,8 +2,8 @@ * Simplified version of legacyListMembers.handler.ts that returns basic member info. * Used for filtering people on /bookings. */ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import type { PrismaClient } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; type ListSimpleMembersOptions = { @@ -22,8 +22,24 @@ export const listSimpleMembers = async ({ ctx }: ListSimpleMembersOptions) => { return []; } - const permissionCheckService = new PermissionCheckService(); - const teamsToQuery = await permissionCheckService.getTeamIdsWithPermission(ctx.user.id, "team.listMembers"); + // query all teams the user is a member of and the team is not private + const teamsToQuery = ( + await prisma.membership.findMany({ + where: { + userId: ctx.user.id, + accepted: true, + NOT: [ + { + role: MembershipRole.MEMBER, + team: { + isPrivate: true, + }, + }, + ], + }, + select: { teamId: true }, + }) + ).map((membership) => membership.teamId); if (!teamsToQuery.length) { return []; diff --git a/packages/trpc/server/routers/viewer/teams/publish.handler.ts b/packages/trpc/server/routers/viewer/teams/publish.handler.ts index 4706fe0ebb824d..6e8d762f2121cb 100644 --- a/packages/trpc/server/routers/viewer/teams/publish.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/publish.handler.ts @@ -2,7 +2,7 @@ import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/pay import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { Redirect } from "@calcom/lib/redirect"; import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { TeamService } from "@calcom/lib/server/service/teamService"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; diff --git a/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts b/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts index d0b3efb4af0d69..97a21711ab2923 100644 --- a/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/removeHostsFromEventTypes.handler.ts @@ -1,4 +1,4 @@ -import { isTeamAdmin, isTeamOwner } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams"; import prisma from "@calcom/prisma"; import { TRPCError } from "@trpc/server"; diff --git a/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts b/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts index 41556a153fba16..82323bfa91cc2a 100644 --- a/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts @@ -1,72 +1,121 @@ +import { FeaturesRepository } from "@calcom/features/flags/features.repository"; +import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import { isTeamOwner } from "@calcom/lib/server/queries/teams"; +import { TeamService } from "@calcom/lib/server/service/teamService"; +import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; +import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; import type { TRemoveMemberInputSchema } from "./removeMember.schema"; -import { RemoveMemberServiceFactory } from "./removeMember/RemoveMemberServiceFactory"; type RemoveMemberOptions = { ctx: { - user: { - id: number; - organization?: { - isOrgAdmin: boolean; - }; - }; + user: NonNullable; + sourceIp?: string; }; input: TRemoveMemberInputSchema; }; -export const removeMemberHandler = async ({ - ctx: { - user: { id: userId, organization }, - }, - input, -}: RemoveMemberOptions) => { +export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) => { await checkRateLimitAndThrowError({ - identifier: `removeMember.${userId}`, + identifier: `removeMember.${ctx.user.id}`, }); const { memberIds, teamIds, isOrg } = input; - const isOrgAdmin = organization?.isOrgAdmin ?? false; - - // Note: This assumes that all teams in the request have the same PBAC setting 9999% chance they do. - const primaryTeamId = teamIds[0]; - if (!primaryTeamId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "At least one team ID must be provided", - }); - } - // Get the appropriate service based on feature flag - const service = await RemoveMemberServiceFactory.create(primaryTeamId); + // Check PBAC permissions for each team + const hasRemovePermission = await Promise.all( + teamIds.map(async (teamId) => { + // Get user's membership role in this team + const membership = await prisma.membership.findFirst({ + where: { + userId: ctx.user.id, + teamId: teamId, + }, + select: { + role: true, + }, + }); - const { hasPermission } = await service.checkRemovePermissions({ - userId, - isOrgAdmin, - memberIds, - teamIds, - isOrg, - }); + if (!membership) return false; + + // Check PBAC permissions for removing team members + const permissions = await getSpecificPermissions({ + userId: ctx.user.id, + teamId: teamId, + resource: isOrg ? Resource.Organization : Resource.Team, + userRole: membership.role, + actions: [CustomAction.Remove], + fallbackRoles: { + [CustomAction.Remove]: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + + return permissions[CustomAction.Remove]; + }) + ).then((results) => results.every((result) => result)); + + // Check if user is trying to remove themselves (allowed for non-owners) + const isRemovingSelf = memberIds.length === 1 && memberIds[0] === ctx.user.id; - if (!hasPermission) { + // Allow if user has remove permission OR if they're removing themselves + if (!hasRemovePermission && !isRemovingSelf) { throw new TRPCError({ code: "UNAUTHORIZED" }); } - await service.validateRemoval( - { - userId, - isOrgAdmin, - memberIds, - teamIds, - isOrg, - }, - hasPermission + // TODO(SEAN): Remove this after PBAC is rolled out. + // Check if any team has PBAC enabled + const featuresRepository = new FeaturesRepository(prisma); + const pbacEnabledForTeams = await Promise.all( + teamIds.map(async (teamId) => await featuresRepository.checkIfTeamHasFeature(teamId, "pbac")) ); + const isAnyTeamPBACEnabled = pbacEnabledForTeams.some((enabled) => enabled); + + // Only apply traditional owner-based logic if PBAC is not enabled for any teams + if (!isAnyTeamPBACEnabled) { + // Only a team owner can remove another team owner. + const isAnyMemberOwnerAndCurrentUserNotOwner = await Promise.all( + memberIds.map(async (memberId) => { + const isAnyTeamOwnerAndCurrentUserNotOwner = await Promise.all( + teamIds.map(async (teamId) => { + return (await isTeamOwner(memberId, teamId)) && !(await isTeamOwner(ctx.user.id, teamId)); + }) + ).then((results) => results.some((result) => result)); + + return isAnyTeamOwnerAndCurrentUserNotOwner; + }) + ).then((results) => results.some((result) => result)); + + if (isAnyMemberOwnerAndCurrentUserNotOwner) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Only a team owner can remove another team owner.", + }); + } + + // Check if user is trying to remove themselves from a team they own (prevent this) + if (isRemovingSelf && hasRemovePermission) { + // Additional check: ensure they're not an owner trying to remove themselves + const isOwnerOfAnyTeam = await Promise.all( + teamIds.map(async (teamId) => await isTeamOwner(ctx.user.id, teamId)) + ).then((results) => results.some((result) => result)); + + if (isOwnerOfAnyTeam) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You can not remove yourself from a team you own.", + }); + } + } + } - // Perform the removal - await service.removeMembers(memberIds, teamIds, isOrg); + await TeamService.removeMembers({ teamIds, userIds: memberIds, isOrg }); }; export default removeMemberHandler; diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/BaseRemoveMemberService.ts b/packages/trpc/server/routers/viewer/teams/removeMember/BaseRemoveMemberService.ts deleted file mode 100644 index a814d01e1b9c28..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/BaseRemoveMemberService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TeamService } from "@calcom/lib/server/service/teamService"; - -import type { - IRemoveMemberService, - RemoveMemberContext, - RemoveMemberPermissionResult, -} from "./IRemoveMemberService"; - -/** - * Base abstract class for remove member services - * Provides common functionality and defines abstract methods for specific implementations - */ -export abstract class BaseRemoveMemberService implements IRemoveMemberService { - abstract checkRemovePermissions(context: RemoveMemberContext): Promise; - - abstract validateRemoval(context: RemoveMemberContext, hasPermission: boolean): Promise; - - async removeMembers(memberIds: number[], teamIds: number[], isOrg: boolean): Promise { - await TeamService.removeMembers({ teamIds, userIds: memberIds, isOrg }); - } -} diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/IRemoveMemberService.ts b/packages/trpc/server/routers/viewer/teams/removeMember/IRemoveMemberService.ts deleted file mode 100644 index ca1305b485bc1e..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/IRemoveMemberService.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { MembershipRole } from "@calcom/prisma/enums"; - -export interface RemoveMemberContext { - userId: number; - isOrgAdmin: boolean; - memberIds: number[]; - teamIds: number[]; - isOrg: boolean; -} - -export interface RemoveMemberPermissionResult { - hasPermission: boolean; - userRoles?: Map; -} - -export interface IRemoveMemberService { - /** - * Checks if the user has permission to remove members from teams - */ - checkRemovePermissions(context: RemoveMemberContext): Promise; - - /** - * Validates that the removal can proceed (e.g., owner checks) - */ - validateRemoval(context: RemoveMemberContext, hasPermission: boolean): Promise; - - /** - * Performs the actual removal of members from teams - */ - removeMembers(memberIds: number[], teamIds: number[], isOrg: boolean): Promise; -} diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/LegacyRemoveMemberService.ts b/packages/trpc/server/routers/viewer/teams/removeMember/LegacyRemoveMemberService.ts deleted file mode 100644 index 4b5266d1a66f89..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/LegacyRemoveMemberService.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as teamQueries from "@calcom/features/ee/teams/lib/queries"; -import { prisma } from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { TRPCError } from "@trpc/server"; - -import { BaseRemoveMemberService } from "./BaseRemoveMemberService"; -import type { RemoveMemberContext, RemoveMemberPermissionResult } from "./IRemoveMemberService"; - -export class LegacyRemoveMemberService extends BaseRemoveMemberService { - async checkRemovePermissions(context: RemoveMemberContext): Promise { - const { userId, isOrgAdmin, teamIds } = context; - - // Org admins have full permission - if (isOrgAdmin) { - // Return admin role for all teams - const userRoles = new Map(); - teamIds.forEach((teamId) => { - userRoles.set(teamId, MembershipRole.ADMIN); - }); - return { - hasPermission: true, - userRoles, - }; - } - - // Get all memberships in a single query - const membershipRoles = await prisma.membership.findMany({ - where: { - userId, - teamId: { - in: teamIds, - }, - }, - select: { - role: true, - teamId: true, - }, - }); - - // Create a map for O(1) lookup - const userRoles = new Map(); - membershipRoles.forEach((m) => { - userRoles.set(m.teamId, m.role); - }); - - // Check if user has admin or owner role in all teams - const allowedRoles: MembershipRole[] = [MembershipRole.ADMIN, MembershipRole.OWNER]; - let hasPermission = true; - - for (const teamId of teamIds) { - const userRole = userRoles.get(teamId); - if (!userRole || !allowedRoles.includes(userRole)) { - hasPermission = false; - break; - } - } - - return { - hasPermission, - userRoles, - }; - } - - async validateRemoval(context: RemoveMemberContext, hasPermission: boolean): Promise { - const { userId, memberIds, teamIds, isOrgAdmin } = context; - const isRemovingSelf = memberIds.length === 1 && memberIds[0] === userId; - - // Only a team owner can remove another team owner (org admins are exempt) - if (!isOrgAdmin) { - const isAnyMemberOwnerAndCurrentUserNotOwner = await Promise.all( - memberIds.map(async (memberId) => { - const isAnyTeamOwnerAndCurrentUserNotOwner = await Promise.all( - teamIds.map(async (teamId) => { - return ( - (await teamQueries.isTeamOwner(memberId, teamId)) && - !(await teamQueries.isTeamOwner(userId, teamId)) - ); - }) - ).then((results) => results.some((result) => result)); - - return isAnyTeamOwnerAndCurrentUserNotOwner; - }) - ).then((results) => results.some((result) => result)); - - if (isAnyMemberOwnerAndCurrentUserNotOwner) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Only a team owner can remove another team owner.", - }); - } - } - - // Check if user is trying to remove themselves from a team they own (prevent this) - if (isRemovingSelf && hasPermission) { - const isOwnerOfAnyTeam = await Promise.all( - teamIds.map(async (teamId) => await teamQueries.isTeamOwner(userId, teamId)) - ).then((results) => results.some((result) => result)); - - if (isOwnerOfAnyTeam) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You can not remove yourself from a team you own.", - }); - } - } - } -} diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/PBACRemoveMemberService.ts b/packages/trpc/server/routers/viewer/teams/removeMember/PBACRemoveMemberService.ts deleted file mode 100644 index 9f4683b3b7e6bc..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/PBACRemoveMemberService.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as teamQueries from "@calcom/features/ee/teams/lib/queries"; -import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper"; -import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; - -import { TRPCError } from "@trpc/server"; - -import { BaseRemoveMemberService } from "./BaseRemoveMemberService"; -import type { RemoveMemberContext, RemoveMemberPermissionResult } from "./IRemoveMemberService"; - -export class PBACRemoveMemberService extends BaseRemoveMemberService { - private permissionService = new PermissionCheckService(); - - async checkRemovePermissions(context: RemoveMemberContext): Promise { - const { userId, teamIds, isOrg } = context; - - const resource = isOrg ? Resource.Organization : Resource.Team; - const removePermission = PermissionMapper.toPermissionString({ - resource, - action: CustomAction.Remove, - }); - - const teamsWithPermission = await this.permissionService.getTeamIdsWithPermission( - userId, - removePermission - ); - - // Convert to Set for O(1) lookup - const teamsWithPermissionSet = new Set(teamsWithPermission); - - // Check if user has permission for ALL requested teams - const hasPermission = teamIds.every((teamId) => teamsWithPermissionSet.has(teamId)); - - return { - hasPermission, - }; - } - - async validateRemoval(context: RemoveMemberContext, hasPermission: boolean): Promise { - const { userId, memberIds, teamIds } = context; - const isRemovingSelf = memberIds.length === 1 && memberIds[0] === userId; - - /** - * TODO: Figure out the best way to prevent someone bricking a team - * by removing all people with updateRole permissions - */ - if (isRemovingSelf && hasPermission) { - const isOwnerOfAnyTeam = await Promise.all( - teamIds.map(async (teamId) => await teamQueries.isTeamOwner(userId, teamId)) - ).then((results) => results.some((result) => result)); - - if (isOwnerOfAnyTeam) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You can not remove yourself from a team you own.", - }); - } - } - } -} diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/RemoveMemberServiceFactory.ts b/packages/trpc/server/routers/viewer/teams/removeMember/RemoveMemberServiceFactory.ts deleted file mode 100644 index 50ffd2a94d836f..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/RemoveMemberServiceFactory.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; -import { prisma } from "@calcom/prisma"; - -import type { IRemoveMemberService } from "./IRemoveMemberService"; -import { LegacyRemoveMemberService } from "./LegacyRemoveMemberService"; -import { PBACRemoveMemberService } from "./PBACRemoveMemberService"; - -export class RemoveMemberServiceFactory { - /** - * Creates the appropriate RemoveMemberService based on whether PBAC is enabled - * Caches the service per team/org to avoid repeated feature flag checks - */ - static async create(teamId: number): Promise { - const featuresRepository = new FeaturesRepository(prisma); - const isPBACEnabled = await featuresRepository.checkIfTeamHasFeature(teamId, "pbac"); - - const service = isPBACEnabled ? new PBACRemoveMemberService() : new LegacyRemoveMemberService(); - - return service; - } -} diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/LegacyRemoveMemberService.test.ts b/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/LegacyRemoveMemberService.test.ts deleted file mode 100644 index e22f4b2b509c72..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/LegacyRemoveMemberService.test.ts +++ /dev/null @@ -1,501 +0,0 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; - -import * as teamQueries from "@calcom/features/ee/teams/lib/queries"; -import { TeamService } from "@calcom/lib/server/service/teamService"; -import { prisma } from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { TRPCError } from "@trpc/server"; - -import { LegacyRemoveMemberService } from "../LegacyRemoveMemberService"; - -vi.mock("@calcom/prisma", () => ({ - prisma: { - membership: { - findMany: vi.fn(), - }, - }, -})); - -vi.mock("@calcom/lib/server/service/teamService"); -vi.mock("@calcom/features/ee/teams/lib/queries"); - -describe("LegacyRemoveMemberService", () => { - let service: LegacyRemoveMemberService; - - beforeEach(() => { - vi.clearAllMocks(); - service = new LegacyRemoveMemberService(); - }); - - describe("checkRemovePermissions", () => { - describe("Org Admin Scenarios", () => { - it("should allow org admin to remove members from teams they are NOT part of", async () => { - const userId = 1; - const isOrgAdmin = true; - const teamIds = [100, 200]; // Teams the org admin is not part of - const memberIds = [2, 3]; - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin, - memberIds, - teamIds, - isOrg: true, - }); - - expect(result.hasPermission).toBe(true); - // Should not query database for org admin - expect(prisma.membership.findMany).not.toHaveBeenCalled(); - }); - - it("should bypass membership checks for org admins", async () => { - const userId = 1; - const isOrgAdmin = true; - const teamIds = [1, 2, 3]; - const memberIds = [4, 5, 6]; - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin, - memberIds, - teamIds, - isOrg: true, - }); - - expect(result.hasPermission).toBe(true); - expect(result.userRoles).toBeInstanceOf(Map); - - // Org admin should have ADMIN role for all teams - teamIds.forEach((teamId) => { - expect(result.userRoles?.get(teamId)).toBe(MembershipRole.ADMIN); - }); - - // Should not query database - expect(prisma.membership.findMany).not.toHaveBeenCalled(); - }); - - it("should allow org admin to remove from multiple teams at once", async () => { - const userId = 1; - const isOrgAdmin = true; - const teamIds = [1, 2, 3, 4, 5]; - const memberIds = [10, 20]; - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin, - memberIds, - teamIds, - isOrg: true, - }); - - expect(result.hasPermission).toBe(true); - expect(prisma.membership.findMany).not.toHaveBeenCalled(); - }); - - it("should work for org admin even with isOrg=false", async () => { - const userId = 1; - const isOrgAdmin = true; - const teamIds = [1]; - const memberIds = [2]; - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin, - memberIds, - teamIds, - isOrg: false, // Note: isOrg is false - }); - - expect(result.hasPermission).toBe(true); - expect(prisma.membership.findMany).not.toHaveBeenCalled(); - }); - }); - - describe("Regular User Scenarios", () => { - it("should allow ADMIN to remove members", async () => { - const userId = 1; - const teamIds = [1, 2]; - const memberIds = [3]; - - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.ADMIN } as any, - { id: 2, userId, teamId: 2, role: MembershipRole.ADMIN } as any, - ]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - expect(result.hasPermission).toBe(true); - expect(result.userRoles?.get(1)).toBe(MembershipRole.ADMIN); - expect(result.userRoles?.get(2)).toBe(MembershipRole.ADMIN); - }); - - it("should allow OWNER to remove members", async () => { - const userId = 1; - const teamIds = [1]; - const memberIds = [2]; - - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.OWNER } as any, - ]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - expect(result.hasPermission).toBe(true); - expect(result.userRoles?.get(1)).toBe(MembershipRole.OWNER); - }); - - it("should deny MEMBER from removing members", async () => { - const userId = 1; - const teamIds = [1]; - const memberIds = [2]; - - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.MEMBER } as any, - ]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - expect(result.hasPermission).toBe(false); - }); - - it("should deny non-member from removing members", async () => { - const userId = 1; - const teamIds = [1]; - const memberIds = [2]; - - vi.mocked(prisma.membership.findMany).mockResolvedValue([]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - expect(result.hasPermission).toBe(false); - }); - - it("should require ADMIN/OWNER role in ALL teams for multi-team removal", async () => { - const userId = 1; - const teamIds = [1, 2, 3]; - const memberIds = [4]; - - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.ADMIN } as any, - { id: 2, userId, teamId: 2, role: MembershipRole.MEMBER } as any, // Not admin/owner - { id: 3, userId, teamId: 3, role: MembershipRole.OWNER } as any, - ]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - expect(result.hasPermission).toBe(false); - }); - - it("should allow when user has ADMIN/OWNER in all teams", async () => { - const userId = 1; - const teamIds = [1, 2, 3]; - const memberIds = [4, 5]; - - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.ADMIN } as any, - { id: 2, userId, teamId: 2, role: MembershipRole.OWNER } as any, - { id: 3, userId, teamId: 3, role: MembershipRole.ADMIN } as any, - ]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - expect(result.hasPermission).toBe(true); - }); - }); - }); - - describe("validateRemoval", () => { - describe("Owner Protection", () => { - it("should prevent non-owner from removing owner", async () => { - const userId = 1; - const memberIds = [2]; - const teamIds = [1]; - const userRoles = new Map([[1, MembershipRole.ADMIN]]); - - // Member 2 is owner, but userId 1 is not owner - vi.mocked(teamQueries.isTeamOwner) - .mockResolvedValueOnce(true) // isTeamOwner(2, 1) - member is owner - .mockResolvedValueOnce(false); // isTeamOwner(1, 1) - current user is not owner - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true - ) - ).rejects.toThrow( - expect.objectContaining({ - code: "UNAUTHORIZED", - message: "Only a team owner can remove another team owner.", - }) - ); - }); - - it("should allow owner to remove another owner", async () => { - const userId = 1; - const memberIds = [2]; - const teamIds = [1]; - const userRoles = new Map([[1, MembershipRole.OWNER]]); - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(true); - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true - ) - ).resolves.not.toThrow(); - }); - - it("should allow org admin to remove owner", async () => { - const userId = 1; - const memberIds = [2]; - const teamIds = [1]; - const userRoles = new Map([[1, MembershipRole.ADMIN]]); - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(true); - - // Org admin can remove owner - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: true, // Org admin - memberIds, - teamIds, - isOrg: true, - }, - { hasPermission: true, userRoles } - ) - ).resolves.not.toThrow(); - - // isTeamOwner should not be called for org admins - expect(teamQueries.isTeamOwner).not.toHaveBeenCalled(); - }); - }); - - describe("Self-Removal Prevention", () => { - it("should prevent owner from removing themselves", async () => { - const userId = 1; - const memberIds = [1]; // Same as userId - const teamIds = [1]; - const userRoles = new Map([[1, MembershipRole.OWNER]]); - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(true); // User is owner - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true // hasPermission - this should be boolean, not object - ) - ).rejects.toThrow( - expect.objectContaining({ - code: "FORBIDDEN", - message: "You can not remove yourself from a team you own.", - }) - ); - }); - - it("should allow admin to remove themselves", async () => { - const userId = 1; - const memberIds = [1]; // Same as userId - const teamIds = [1]; - const userRoles = new Map([[1, MembershipRole.ADMIN]]); - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(false); - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - { hasPermission: true, userRoles } - ) - ).resolves.not.toThrow(); - }); - - it("should allow member to remove themselves", async () => { - const userId = 1; - const memberIds = [1]; // Same as userId - const teamIds = [1]; - const userRoles = new Map([[1, MembershipRole.MEMBER]]); - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(false); - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - { hasPermission: true, userRoles } - ) - ).resolves.not.toThrow(); - }); - }); - - describe("Multi-Member Validation", () => { - it("should validate each member independently", async () => { - const userId = 1; - const memberIds = [2, 3, 4]; - const teamIds = [1]; - const userRoles = new Map([[1, MembershipRole.ADMIN]]); - - // Member 2 is not owner, member 3 is owner, member 4 is not owner - // Current user (1) is not owner - vi.mocked(teamQueries.isTeamOwner) - .mockResolvedValueOnce(false) // isTeamOwner(2, 1) - member 2 is not owner - .mockResolvedValueOnce(true) // isTeamOwner(3, 1) - member 3 is owner - .mockResolvedValueOnce(false) // isTeamOwner(1, 1) - current user is not owner - .mockResolvedValueOnce(false); // isTeamOwner(4, 1) - member 4 is not owner (if reached) - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - { hasPermission: true, userRoles } - ) - ).rejects.toThrow( - expect.objectContaining({ - code: "UNAUTHORIZED", - message: "Only a team owner can remove another team owner.", - }) - ); - - expect(teamQueries.isTeamOwner).toHaveBeenCalledTimes(4); // Checks member 2, user 1, member 3 (owner), user 1 - }); - }); - }); - - describe("removeMembers", () => { - it("should call TeamService.removeMembers with correct parameters", async () => { - const memberIds = [1, 2, 3]; - const teamIds = [4, 5]; - const isOrg = true; - - vi.mocked(TeamService.removeMembers).mockResolvedValue(undefined); - - await service.removeMembers(memberIds, teamIds, isOrg); - - expect(TeamService.removeMembers).toHaveBeenCalledWith({ - userIds: memberIds, - teamIds, - isOrg, - }); - }); - - it("should propagate errors from TeamService", async () => { - const memberIds = [1]; - const teamIds = [2]; - const isOrg = false; - - const error = new Error("Database error"); - vi.mocked(TeamService.removeMembers).mockRejectedValue(error); - - await expect(service.removeMembers(memberIds, teamIds, isOrg)).rejects.toThrow("Database error"); - }); - }); - - describe("Edge Cases", () => { - it("should handle empty memberIds array", async () => { - const userId = 1; - const memberIds: number[] = []; - const teamIds = [1]; - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - // Should still check permissions even with no members to remove - expect(prisma.membership.findMany).toHaveBeenCalled(); - }); - - it("should handle permission check with no teams in database", async () => { - const userId = 1; - const teamIds = [999]; // Non-existent team - const memberIds = [2]; - - vi.mocked(prisma.membership.findMany).mockResolvedValue([]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }); - - expect(result.hasPermission).toBe(false); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/PBACRemoveMemberService.test.ts b/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/PBACRemoveMemberService.test.ts deleted file mode 100644 index c3da482ed90f13..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/PBACRemoveMemberService.test.ts +++ /dev/null @@ -1,474 +0,0 @@ -import { describe, expect, it, vi, beforeEach, type Mock } from "vitest"; - -import * as teamQueries from "@calcom/features/ee/teams/lib/queries"; -import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper"; -import { Resource, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import { TeamService } from "@calcom/lib/server/service/teamService"; -import { prisma } from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { PBACRemoveMemberService } from "../PBACRemoveMemberService"; - -vi.mock("@calcom/prisma", () => ({ - prisma: { - membership: { - findMany: vi.fn(), - }, - }, -})); - -vi.mock("@calcom/lib/server/service/teamService"); -vi.mock("@calcom/features/ee/teams/lib/queries"); -vi.mock("@calcom/features/pbac/services/permission-check.service"); -vi.mock("@calcom/features/pbac/domain/mappers/PermissionMapper"); - -describe("PBACRemoveMemberService", () => { - let service: PBACRemoveMemberService; - let mockPermissionCheckService: { - getTeamIdsWithPermission: Mock; - }; - - beforeEach(() => { - vi.clearAllMocks(); - - mockPermissionCheckService = { - getTeamIdsWithPermission: vi.fn(), - }; - - vi.mocked(PermissionCheckService).mockImplementation(() => mockPermissionCheckService as any); - - service = new PBACRemoveMemberService(); - }); - - describe("checkRemovePermissions", () => { - describe("PBAC Permission Checks", () => { - it("should check team.remove permission for team context", async () => { - const userId = 1; - const teamIds = [1, 2]; - const memberIds = [3]; - const isOrg = false; - - const removePermission = "team.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue([1, 2]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(true); - - expect(PermissionMapper.toPermissionString).toHaveBeenCalledWith({ - resource: Resource.Team, - action: CustomAction.Remove, - }); - - expect(mockPermissionCheckService.getTeamIdsWithPermission).toHaveBeenCalledWith( - userId, - removePermission - ); - }); - - it("should check organization.remove permission for org context", async () => { - const userId = 1; - const teamIds = [1, 2]; - const memberIds = [3]; - const isOrg = true; - - const removePermission = "organization.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue([1, 2]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(true); - - expect(PermissionMapper.toPermissionString).toHaveBeenCalledWith({ - resource: Resource.Organization, - action: CustomAction.Remove, - }); - }); - - it("should deny when user lacks permission for some teams", async () => { - const userId = 1; - const teamIds = [1, 2, 3]; - const memberIds = [4]; - const isOrg = false; - - const removePermission = "team.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - // User only has permission for teams 1 and 2, not 3 - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue([1, 2]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(false); - }); - - it("should allow when user has permission for all teams", async () => { - const userId = 1; - const teamIds = [1, 2, 3]; - const memberIds = [4, 5]; - const isOrg = false; - - const removePermission = "team.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue([1, 2, 3, 4]); // Has more permissions - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(true); - }); - - it("should deny when user has no permissions", async () => { - const userId = 1; - const teamIds = [1]; - const memberIds = [2]; - const isOrg = false; - - const removePermission = "team.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue([]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(false); - }); - }); - - describe("Multi-team Operations", () => { - it("should require permission for ALL teams in request", async () => { - const userId = 1; - const teamIds = [10, 20, 30, 40]; - const memberIds = [100]; - const isOrg = false; - - const removePermission = "team.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - // Missing permission for team 30 - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue([10, 20, 40]); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(false); - }); - - it("should handle large team lists efficiently", async () => { - const userId = 1; - const teamIds = Array.from({ length: 100 }, (_, i) => i + 1); - const memberIds = [999]; - const isOrg = true; - - const removePermission = "organization.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue(teamIds); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(true); - }); - }); - }); - - describe("validateRemoval", () => { - describe("Owner Protection", () => { - it("should allow removing owners with PBAC permissions", async () => { - const userId = 1; - const memberIds = [2]; - const teamIds = [1]; - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(true); // Member 2 is owner - - // Get user's role - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.ADMIN } as any, - ]); - - // PBAC service doesn't have owner-to-owner protection logic - // If user has PBAC remove permission, they can remove owners (unlike Legacy service) - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true // hasPermission - ) - ).resolves.not.toThrow(); - }); - - it("should allow owner to remove another owner with PBAC", async () => { - const userId = 1; - const memberIds = [2]; - const teamIds = [1]; - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(true); // Member 2 is owner - - // User is owner - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.OWNER } as any, - ]); - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true - ) - ).resolves.not.toThrow(); - }); - }); - - describe("Self-Removal Prevention", () => { - it("should prevent owner from removing themselves", async () => { - const userId = 1; - const memberIds = [1]; // Same as userId - const teamIds = [1]; - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(true); // User is owner of the team - - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.OWNER } as any, - ]); - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true - ) - ).rejects.toThrow( - expect.objectContaining({ - code: "FORBIDDEN", - message: "You can not remove yourself from a team you own.", - }) - ); - }); - - it("should allow admin to remove themselves", async () => { - const userId = 1; - const memberIds = [1]; - const teamIds = [1]; - - vi.mocked(teamQueries.isTeamOwner).mockResolvedValue(false); - - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.ADMIN } as any, - ]); - - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true - ) - ).resolves.not.toThrow(); - }); - }); - - describe("Multi-Member Validation", () => { - it("should allow removing multiple members including owners", async () => { - const userId = 1; - const memberIds = [2, 3, 4]; - const teamIds = [1]; - - // User is admin - vi.mocked(prisma.membership.findMany).mockResolvedValue([ - { id: 1, userId, teamId: 1, role: MembershipRole.ADMIN } as any, - ]); - - // Member 3 is owner - vi.mocked(teamQueries.isTeamOwner) - .mockResolvedValueOnce(false) // member 2 - .mockResolvedValueOnce(true) // member 3 is owner - .mockResolvedValueOnce(false); // member 4 - - // PBAC service doesn't validate owner-to-owner removal - // It only prevents self-removal by owners - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - true - ) - ).resolves.not.toThrow(); - }); - }); - - describe("Edge Cases", () => { - it("should handle validation when hasPermission is false", async () => { - const userId = 1; - const memberIds = [2]; - const teamIds = [1]; - - // Should not perform validation if no permission - await expect( - service.validateRemoval( - { - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg: false, - }, - false // No permission - ) - ).resolves.not.toThrow(); - - // Should not check ownership or roles - expect(teamQueries.isTeamOwner).not.toHaveBeenCalled(); - expect(prisma.membership.findMany).not.toHaveBeenCalled(); - }); - }); - }); - - describe("removeMembers", () => { - it("should call TeamService.removeMembers with correct parameters", async () => { - const memberIds = [1, 2, 3]; - const teamIds = [4, 5]; - const isOrg = true; - - vi.mocked(TeamService.removeMembers).mockResolvedValue(undefined); - - await service.removeMembers(memberIds, teamIds, isOrg); - - expect(TeamService.removeMembers).toHaveBeenCalledWith({ - userIds: memberIds, - teamIds, - isOrg, - }); - }); - - it("should propagate errors from TeamService", async () => { - const memberIds = [1]; - const teamIds = [2]; - const isOrg = false; - - const error = new Error("Database connection failed"); - vi.mocked(TeamService.removeMembers).mockRejectedValue(error); - - await expect(service.removeMembers(memberIds, teamIds, isOrg)).rejects.toThrow( - "Database connection failed" - ); - }); - }); - - describe("Service Initialization", () => { - it("should create PermissionCheckService on instantiation", () => { - const newService = new PBACRemoveMemberService(); - - expect(PermissionCheckService).toHaveBeenCalled(); - }); - }); - - describe("Permission Service Integration", () => { - it("should handle permission service errors gracefully", async () => { - const userId = 1; - const teamIds = [1]; - const memberIds = [2]; - const isOrg = false; - - const removePermission = "team.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - - mockPermissionCheckService.getTeamIdsWithPermission.mockRejectedValue( - new Error("Permission service unavailable") - ); - - await expect( - service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }) - ).rejects.toThrow("Permission service unavailable"); - }); - - it("should handle empty permission results", async () => { - const userId = 1; - const teamIds = [1, 2]; - const memberIds = [3]; - const isOrg = false; - - const removePermission = "team.remove"; - vi.mocked(PermissionMapper.toPermissionString).mockReturnValue(removePermission); - mockPermissionCheckService.getTeamIdsWithPermission.mockResolvedValue(null as any); - - const result = await service.checkRemovePermissions({ - userId, - isOrgAdmin: false, - memberIds, - teamIds, - isOrg, - }); - - expect(result.hasPermission).toBe(false); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/RemoveMemberServiceFactory.test.ts b/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/RemoveMemberServiceFactory.test.ts deleted file mode 100644 index 6709079a31bda5..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/RemoveMemberServiceFactory.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; - -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; - -import { LegacyRemoveMemberService } from "../LegacyRemoveMemberService"; -import { PBACRemoveMemberService } from "../PBACRemoveMemberService"; -import { RemoveMemberServiceFactory } from "../RemoveMemberServiceFactory"; - -vi.mock("@calcom/features/flags/features.repository"); -vi.mock("../LegacyRemoveMemberService"); -vi.mock("../PBACRemoveMemberService"); - -describe("RemoveMemberServiceFactory", () => { - let mockFeaturesRepository: { - checkIfTeamHasFeature: vi.Mock; - }; - - beforeEach(() => { - vi.clearAllMocks(); - - mockFeaturesRepository = { - checkIfTeamHasFeature: vi.fn(), - }; - - vi.mocked(FeaturesRepository).mockImplementation(() => mockFeaturesRepository as any); - }); - - describe("Service Creation", () => { - it("should create LegacyRemoveMemberService when PBAC is disabled", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(false); - - const teamId = 1; - const service = await RemoveMemberServiceFactory.create(teamId); - - expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(teamId, "pbac"); - expect(service).toBeInstanceOf(LegacyRemoveMemberService); - }); - - it("should create PBACRemoveMemberService when PBAC is enabled", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(true); - - const teamId = 1; - const service = await RemoveMemberServiceFactory.create(teamId); - - expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(teamId, "pbac"); - expect(service).toBeInstanceOf(PBACRemoveMemberService); - }); - }); - - describe("Service Creation for Different Teams", () => { - it("should create different services for different teams", async () => { - // Team 1 has PBAC disabled - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false); - // Team 2 has PBAC enabled - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true); - - const service1 = await RemoveMemberServiceFactory.create(1); - const service2 = await RemoveMemberServiceFactory.create(2); - - expect(service1).toBeInstanceOf(LegacyRemoveMemberService); - expect(service2).toBeInstanceOf(PBACRemoveMemberService); - expect(service1).not.toBe(service2); - - expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledTimes(2); - expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac"); - expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(2, "pbac"); - }); - - it("should create service for each call (no caching in current implementation)", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(true); - - const teamId = 1; - - // Make multiple calls - const service1 = await RemoveMemberServiceFactory.create(teamId); - const service2 = await RemoveMemberServiceFactory.create(teamId); - const service3 = await RemoveMemberServiceFactory.create(teamId); - - // All should be different instances (no caching currently) - expect(service1).not.toBe(service2); - expect(service2).not.toBe(service3); - - // Feature flag should be called each time - expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledTimes(3); - }); - }); - - describe("Error Handling", () => { - it("should propagate errors from features repository", async () => { - const error = new Error("Features repository error"); - mockFeaturesRepository.checkIfTeamHasFeature.mockRejectedValue(error); - - await expect(RemoveMemberServiceFactory.create(1)).rejects.toThrow("Features repository error"); - }); - - it("should handle null/undefined feature flag gracefully", async () => { - mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(null as any); - - const service = await RemoveMemberServiceFactory.create(1); - - // Should default to Legacy when feature flag is falsy - expect(service).toBeInstanceOf(LegacyRemoveMemberService); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/removeMember.handler.test.ts b/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/removeMember.handler.test.ts deleted file mode 100644 index c8a357439190e1..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/__tests__/removeMember.handler.test.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; - -import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; - -import { TRPCError } from "@trpc/server"; - -import { removeMemberHandler } from "../../removeMember.handler"; -import type { IRemoveMemberService } from "../IRemoveMemberService"; -import { RemoveMemberServiceFactory } from "../RemoveMemberServiceFactory"; - -vi.mock("@calcom/lib/checkRateLimitAndThrowError"); -vi.mock("../RemoveMemberServiceFactory"); - -describe("removeMemberHandler", () => { - const mockService: IRemoveMemberService = { - checkRemovePermissions: vi.fn(), - validateRemoval: vi.fn(), - removeMembers: vi.fn(), - }; - - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(checkRateLimitAndThrowError).mockResolvedValue({ - success: true, - remaining: 99, - limit: 100, - reset: new Date().getTime() + 60 * 1000, - }); - vi.mocked(RemoveMemberServiceFactory.create).mockResolvedValue(mockService); - vi.mocked(mockService.checkRemovePermissions).mockResolvedValue({ - hasPermission: true, - userRoles: new Map(), - }); - }); - - describe("Rate Limiting", () => { - it("should check rate limit before processing", async () => { - const userId = 1; - const input = { - teamIds: [1], - memberIds: [2], - isOrg: false, - }; - - await removeMemberHandler({ - ctx: { user: { id: userId } }, - input, - }); - - expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({ - identifier: `removeMember.${userId}`, - }); - }); - }); - - describe("Input Validation", () => { - it("should throw BAD_REQUEST when no team IDs provided", async () => { - const input = { - teamIds: [], - memberIds: [2], - isOrg: false, - }; - - await expect( - removeMemberHandler({ - ctx: { user: { id: 1 } }, - input, - }) - ).rejects.toThrow( - expect.objectContaining({ - code: "BAD_REQUEST", - message: "At least one team ID must be provided", - }) - ); - }); - }); - - describe("Service Factory Integration", () => { - it("should create service using primary team ID", async () => { - const primaryTeamId = 1; - const input = { - teamIds: [primaryTeamId, 2, 3], - memberIds: [4, 5], - isOrg: false, - }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue({ hasPermission: true }); - - await removeMemberHandler({ - ctx: { user: { id: 1 } }, - input, - }); - - expect(RemoveMemberServiceFactory.create).toHaveBeenCalledWith(primaryTeamId); - }); - }); - - describe("Permission Checking", () => { - it("should pass org admin status to permission check", async () => { - const userId = 1; - const isOrgAdmin = true; - const input = { - teamIds: [1], - memberIds: [2], - isOrg: true, - }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue({ hasPermission: true }); - - await removeMemberHandler({ - ctx: { - user: { - id: userId, - organization: { isOrgAdmin }, - }, - }, - input, - }); - - expect(mockService.checkRemovePermissions).toHaveBeenCalledWith({ - userId, - isOrgAdmin, - memberIds: input.memberIds, - teamIds: input.teamIds, - isOrg: input.isOrg, - }); - }); - - it("should default isOrgAdmin to false when not provided", async () => { - const userId = 1; - const input = { - teamIds: [1], - memberIds: [2], - isOrg: false, - }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue({ hasPermission: true }); - - await removeMemberHandler({ - ctx: { user: { id: userId } }, - input, - }); - - expect(mockService.checkRemovePermissions).toHaveBeenCalledWith( - expect.objectContaining({ - isOrgAdmin: false, - }) - ); - }); - - it("should throw UNAUTHORIZED when user lacks permission", async () => { - const input = { - teamIds: [1], - memberIds: [2], - isOrg: false, - }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue({ hasPermission: false }); - - await expect( - removeMemberHandler({ - ctx: { user: { id: 1 } }, - input, - }) - ).rejects.toThrow( - expect.objectContaining({ - code: "UNAUTHORIZED", - }) - ); - - expect(mockService.validateRemoval).not.toHaveBeenCalled(); - expect(mockService.removeMembers).not.toHaveBeenCalled(); - }); - }); - - describe("Removal Flow", () => { - it("should complete full removal flow when user has permission", async () => { - const userId = 1; - const isOrgAdmin = false; - const input = { - teamIds: [1, 2], - memberIds: [3, 4], - isOrg: false, - }; - - const hasPermissionResult = { - hasPermission: true, - userRoles: new Map([ - [1, "ADMIN"], - [2, "OWNER"], - ]), - }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue(hasPermissionResult); - mockService.validateRemoval = vi.fn().mockResolvedValue(undefined); - mockService.removeMembers = vi.fn().mockResolvedValue(undefined); - - await removeMemberHandler({ - ctx: { user: { id: userId } }, - input, - }); - - // Verify service calls in order - expect(mockService.checkRemovePermissions).toHaveBeenCalledWith({ - userId, - isOrgAdmin, - memberIds: input.memberIds, - teamIds: input.teamIds, - isOrg: input.isOrg, - }); - - expect(mockService.validateRemoval).toHaveBeenCalledWith( - { - userId, - isOrgAdmin, - memberIds: input.memberIds, - teamIds: input.teamIds, - isOrg: input.isOrg, - }, - hasPermissionResult.hasPermission - ); - - expect(mockService.removeMembers).toHaveBeenCalledWith(input.memberIds, input.teamIds, input.isOrg); - }); - - it("should handle org admin removing members from teams they are not part of", async () => { - const userId = 1; - const isOrgAdmin = true; - const input = { - teamIds: [100, 200], // Teams the org admin is not part of - memberIds: [3, 4], - isOrg: true, - }; - - const hasPermissionResult = { hasPermission: true }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue(hasPermissionResult); - mockService.validateRemoval = vi.fn().mockResolvedValue(undefined); - mockService.removeMembers = vi.fn().mockResolvedValue(undefined); - - await removeMemberHandler({ - ctx: { - user: { - id: userId, - organization: { isOrgAdmin }, - }, - }, - input, - }); - - expect(mockService.checkRemovePermissions).toHaveBeenCalledWith({ - userId, - isOrgAdmin: true, - memberIds: input.memberIds, - teamIds: input.teamIds, - isOrg: input.isOrg, - }); - - expect(mockService.removeMembers).toHaveBeenCalled(); - }); - }); - - describe("Error Handling", () => { - it("should propagate validation errors", async () => { - const input = { - teamIds: [1], - memberIds: [2], - isOrg: false, - }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue({ hasPermission: true }); - mockService.validateRemoval = vi - .fn() - .mockRejectedValue(new TRPCError({ code: "BAD_REQUEST", message: "Cannot remove owner" })); - - await expect( - removeMemberHandler({ - ctx: { user: { id: 1 } }, - input, - }) - ).rejects.toThrow( - expect.objectContaining({ - code: "BAD_REQUEST", - message: "Cannot remove owner", - }) - ); - }); - - it("should propagate service removal errors", async () => { - const input = { - teamIds: [1], - memberIds: [2], - isOrg: false, - }; - - mockService.checkRemovePermissions = vi.fn().mockResolvedValue({ hasPermission: true }); - mockService.validateRemoval = vi.fn().mockResolvedValue(undefined); - mockService.removeMembers = vi.fn().mockRejectedValue(new Error("Database error")); - - await expect( - removeMemberHandler({ - ctx: { user: { id: 1 } }, - input, - }) - ).rejects.toThrow("Database error"); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/teams/removeMember/index.ts b/packages/trpc/server/routers/viewer/teams/removeMember/index.ts deleted file mode 100644 index bcafd99007f652..00000000000000 --- a/packages/trpc/server/routers/viewer/teams/removeMember/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { RemoveMemberServiceFactory } from "./RemoveMemberServiceFactory"; -export type { - IRemoveMemberService, - RemoveMemberContext, - RemoveMemberPermissionResult, -} from "./IRemoveMemberService"; -export { BaseRemoveMemberService } from "./BaseRemoveMemberService"; -export { PBACRemoveMemberService } from "./PBACRemoveMemberService"; -export { LegacyRemoveMemberService } from "./LegacyRemoveMemberService"; diff --git a/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts b/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts index 810d85e904270b..9f81cd2c555b3b 100644 --- a/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/setInviteExpiration.handler.ts @@ -1,4 +1,4 @@ -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; diff --git a/packages/trpc/server/routers/viewer/teams/update.handler.ts b/packages/trpc/server/routers/viewer/teams/update.handler.ts index c2cbc642b8a9f7..8abfa2746f12ba 100644 --- a/packages/trpc/server/routers/viewer/teams/update.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/update.handler.ts @@ -3,7 +3,7 @@ import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder"; import { uploadLogo } from "@calcom/lib/server/avatar"; -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import { RedirectType, RRTimestampBasis } from "@calcom/prisma/enums"; diff --git a/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts b/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts index 48216327c36d63..53a8742ae569d1 100644 --- a/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/updateInternalNotesPresets.handler.ts @@ -1,4 +1,4 @@ -import { isTeamAdmin } from "@calcom/features/ee/teams/lib/queries"; +import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; diff --git a/packages/trpc/server/routers/viewer/webhook/_router.tsx b/packages/trpc/server/routers/viewer/webhook/_router.tsx index b5accc2fe52489..bb728bd81f8450 100644 --- a/packages/trpc/server/routers/viewer/webhook/_router.tsx +++ b/packages/trpc/server/routers/viewer/webhook/_router.tsx @@ -5,7 +5,7 @@ import { ZEditInputSchema } from "./edit.schema"; import { ZGetInputSchema } from "./get.schema"; import { ZListInputSchema } from "./list.schema"; import { ZTestTriggerInputSchema } from "./testTrigger.schema"; -import { createWebhookPbacProcedure } from "./util"; +import { webhookProcedure } from "./util"; type WebhookRouterHandlerCache = { list?: typeof import("./list.handler").listHandler; @@ -20,132 +20,118 @@ type WebhookRouterHandlerCache = { const UNSTABLE_HANDLER_CACHE: WebhookRouterHandlerCache = {}; export const webhookRouter = router({ - list: createWebhookPbacProcedure("webhook.read") - .input(ZListInputSchema) - .query(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.list) { - UNSTABLE_HANDLER_CACHE.list = await import("./list.handler").then((mod) => mod.listHandler); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.list) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.list({ - ctx, - input, - }); - }), - - get: createWebhookPbacProcedure("webhook.read", ["ADMIN", "OWNER", "MEMBER"]) - .input(ZGetInputSchema) - .query(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.get) { - UNSTABLE_HANDLER_CACHE.get = await import("./get.handler").then((mod) => mod.getHandler); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.get) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.get({ - ctx, - input, - }); - }), - - create: createWebhookPbacProcedure("webhook.create") - .input(ZCreateInputSchema) - .mutation(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.create) { - UNSTABLE_HANDLER_CACHE.create = await import("./create.handler").then((mod) => mod.createHandler); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.create) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.create({ - ctx, - input, - }); - }), - - edit: createWebhookPbacProcedure("webhook.update") - .input(ZEditInputSchema) - .mutation(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.edit) { - UNSTABLE_HANDLER_CACHE.edit = await import("./edit.handler").then((mod) => mod.editHandler); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.edit) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.edit({ - ctx, - input, - }); - }), - - delete: createWebhookPbacProcedure("webhook.delete") - .input(ZDeleteInputSchema) - .mutation(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.delete) { - UNSTABLE_HANDLER_CACHE.delete = await import("./delete.handler").then((mod) => mod.deleteHandler); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.delete) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.delete({ - ctx, - input, - }); - }), - - testTrigger: createWebhookPbacProcedure("webhook.update") - .input(ZTestTriggerInputSchema) - .mutation(async ({ ctx, input }) => { - if (!UNSTABLE_HANDLER_CACHE.testTrigger) { - UNSTABLE_HANDLER_CACHE.testTrigger = await import("./testTrigger.handler").then( - (mod) => mod.testTriggerHandler - ); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.testTrigger) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.testTrigger({ - ctx, - input, - }); - }), - - getByViewer: createWebhookPbacProcedure("webhook.read", ["ADMIN", "OWNER", "MEMBER"]).query( - async ({ ctx }) => { - if (!UNSTABLE_HANDLER_CACHE.getByViewer) { - UNSTABLE_HANDLER_CACHE.getByViewer = await import("./getByViewer.handler").then( - (mod) => mod.getByViewerHandler - ); - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.getByViewer) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.getByViewer({ - ctx, - }); + list: webhookProcedure.input(ZListInputSchema).query(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.list) { + UNSTABLE_HANDLER_CACHE.list = await import("./list.handler").then((mod) => mod.listHandler); } - ), + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.list) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.list({ + ctx, + input, + }); + }), + + get: webhookProcedure.input(ZGetInputSchema).query(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.get) { + UNSTABLE_HANDLER_CACHE.get = await import("./get.handler").then((mod) => mod.getHandler); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.get) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.get({ + ctx, + input, + }); + }), + + create: webhookProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.create) { + UNSTABLE_HANDLER_CACHE.create = await import("./create.handler").then((mod) => mod.createHandler); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.create) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.create({ + ctx, + input, + }); + }), + + edit: webhookProcedure.input(ZEditInputSchema).mutation(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.edit) { + UNSTABLE_HANDLER_CACHE.edit = await import("./edit.handler").then((mod) => mod.editHandler); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.edit) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.edit({ + ctx, + input, + }); + }), + + delete: webhookProcedure.input(ZDeleteInputSchema).mutation(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.delete) { + UNSTABLE_HANDLER_CACHE.delete = await import("./delete.handler").then((mod) => mod.deleteHandler); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.delete) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.delete({ + ctx, + input, + }); + }), + + testTrigger: webhookProcedure.input(ZTestTriggerInputSchema).mutation(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.testTrigger) { + UNSTABLE_HANDLER_CACHE.testTrigger = await import("./testTrigger.handler").then( + (mod) => mod.testTriggerHandler + ); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.testTrigger) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.testTrigger({ + ctx, + input, + }); + }), + + getByViewer: webhookProcedure.query(async ({ ctx }) => { + if (!UNSTABLE_HANDLER_CACHE.getByViewer) { + UNSTABLE_HANDLER_CACHE.getByViewer = await import("./getByViewer.handler").then( + (mod) => mod.getByViewerHandler + ); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.getByViewer) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.getByViewer({ + ctx, + }); + }), }); diff --git a/packages/trpc/server/routers/viewer/webhook/create.handler.ts b/packages/trpc/server/routers/viewer/webhook/create.handler.ts index 5fbfd4db8f9b7b..6537cc46370309 100644 --- a/packages/trpc/server/routers/viewer/webhook/create.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/create.handler.ts @@ -1,11 +1,9 @@ import { v4 } from "uuid"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { updateTriggerForExistingBookings } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { prisma } from "@calcom/prisma"; import type { Webhook } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -31,23 +29,6 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { throw new TRPCError({ code: "UNAUTHORIZED" }); } - if (input.teamId) { - const permissionService = new PermissionCheckService(); - - const hasPermission = await permissionService.checkPermission({ - userId: user.id, - teamId: input.teamId, - permission: "webhook.create", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasPermission) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); - } - } - // Add userId if platform, eventTypeId, and teamId are not provided if (!input.platform && !input.eventTypeId && !input.teamId) { webhookData.user = { connect: { id: user.id } }; diff --git a/packages/trpc/server/routers/viewer/webhook/delete.handler.ts b/packages/trpc/server/routers/viewer/webhook/delete.handler.ts index ccc5d782ee0b06..07ae50fdaca868 100644 --- a/packages/trpc/server/routers/viewer/webhook/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/delete.handler.ts @@ -1,12 +1,8 @@ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { updateTriggerForExistingBookings } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; -import { TRPCError } from "@trpc/server"; - import type { TDeleteInputSchema } from "./delete.schema"; type DeleteOptions = { @@ -25,21 +21,6 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => { if (input.eventTypeId) { where.AND.push({ eventTypeId: input.eventTypeId }); } else if (input.teamId) { - const permissionService = new PermissionCheckService(); - - const hasPermission = await permissionService.checkPermission({ - userId: ctx.user.id, - teamId: input.teamId, - permission: "webhook.delete", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasPermission) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); - } - where.AND.push({ teamId: input.teamId }); } else if (ctx.user.role == "ADMIN") { where.AND.push({ OR: [{ platform: true }, { userId: ctx.user.id }] }); diff --git a/packages/trpc/server/routers/viewer/webhook/edit.handler.ts b/packages/trpc/server/routers/viewer/webhook/edit.handler.ts index f80e16961e813f..3d9a37253bbeaf 100644 --- a/packages/trpc/server/routers/viewer/webhook/edit.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/edit.handler.ts @@ -1,11 +1,9 @@ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { updateTriggerForExistingBookings, deleteWebhookScheduledTriggers, cancelNoShowTasksForBooking, } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { prisma } from "@calcom/prisma"; -import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -39,23 +37,6 @@ export const editHandler = async ({ input, ctx }: EditOptions) => { } } - if (webhook.teamId) { - const permissionService = new PermissionCheckService(); - - const hasPermission = await permissionService.checkPermission({ - userId: ctx.user.id, - teamId: webhook.teamId, - permission: "webhook.update", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasPermission) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); - } - } - const updatedWebhook = await prisma.webhook.update({ where: { id, diff --git a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts index 785abbde4fcce0..35aa64a8205763 100644 --- a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts @@ -33,9 +33,7 @@ export type WebhooksByViewer = { }; export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => { - // Use the new PBAC-aware method for fetching webhooks - - return await WebhookRepository.getFilteredWebhooksForUser({ + return await WebhookRepository.getAllWebhooksByUserId({ userId: ctx.user.id, userRole: ctx.user.role, }); diff --git a/packages/trpc/server/routers/viewer/webhook/list.handler.ts b/packages/trpc/server/routers/viewer/webhook/list.handler.ts index 89c500d78ade43..051d25778dd5dc 100644 --- a/packages/trpc/server/routers/viewer/webhook/list.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/list.handler.ts @@ -1,7 +1,5 @@ -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; -import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import type { TListInputSchema } from "./list.schema"; @@ -50,26 +48,8 @@ export const listHandler = async ({ ctx, input }: ListOptions) => { where.AND?.push({ eventTypeId: input.eventTypeId }); } } else { - const permissionService = new PermissionCheckService(); - const teamIds = user?.teams?.map((m) => m.teamId) ?? []; - const allowedTeamIds = ( - await Promise.all( - teamIds.map(async (teamId) => { - const ok = await permissionService.checkPermission({ - userId: ctx.user.id, - teamId, - permission: "webhook.read", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - return ok ? teamId : null; - }) - ) - ).filter((x): x is number => x !== null); - - console.log("Allowed Team IDs:", allowedTeamIds); - where.AND?.push({ - OR: [{ userId: ctx.user.id }, ...(allowedTeamIds.length ? [{ teamId: { in: allowedTeamIds } }] : [])], + OR: [{ userId: ctx.user.id }, { teamId: { in: user?.teams.map((membership) => membership.teamId) } }], }); } diff --git a/packages/trpc/server/routers/viewer/webhook/util.test.ts b/packages/trpc/server/routers/viewer/webhook/util.test.ts deleted file mode 100644 index f3261d031f68bf..00000000000000 --- a/packages/trpc/server/routers/viewer/webhook/util.test.ts +++ /dev/null @@ -1,500 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry"; -import { prisma } from "@calcom/prisma"; -import type { MembershipRole } from "@calcom/prisma/enums"; - -import { TRPCError } from "@trpc/server"; - -import authedProcedure from "../../../procedures/authedProcedure"; -// Import after mocks are set up -import { createWebhookPbacProcedure, webhookProcedure } from "./util"; - -// Mock dependencies - use factory functions to avoid hoisting issues -vi.mock("@calcom/prisma", () => ({ - prisma: { - webhook: { - findUnique: vi.fn(), - }, - eventType: { - findUnique: vi.fn(), - }, - user: { - findUnique: vi.fn(), - }, - }, -})); - -const mockCheckPermission = vi.fn(); - -vi.mock("@calcom/features/pbac/services/permission-check.service", () => ({ - PermissionCheckService: vi.fn().mockImplementation(() => ({ - checkPermission: mockCheckPermission, - })), -})); - -vi.mock("../../../procedures/authedProcedure", () => ({ - default: { - input: vi.fn().mockReturnThis(), - use: vi.fn(), - }, -})); - -// Cast the mocked items to properly typed versions -const mockPrisma = prisma as any; -const mockAuthedProcedure = authedProcedure as any; - -describe("Webhook PBAC Procedures", () => { - const mockCtx = { - user: { - id: 1, - }, - }; - - beforeEach(() => { - vi.clearAllMocks(); - // Reset the mock to ensure clean state - mockCheckPermission.mockReset(); - mockPrisma.webhook.findUnique.mockReset(); - mockPrisma.eventType.findUnique.mockReset(); - mockPrisma.user.findUnique.mockReset(); - mockAuthedProcedure.use.mockClear(); - }); - - describe("createWebhookPbacProcedure", () => { - const testPermission: PermissionString = "webhook.update"; - const fallbackRoles: MembershipRole[] = ["ADMIN", "OWNER"]; - - it("should create a procedure with the specified permission", () => { - const procedure = createWebhookPbacProcedure(testPermission, fallbackRoles); - - // Verify that authedProcedure methods were called - expect(mockAuthedProcedure.input).toHaveBeenCalled(); - expect(mockAuthedProcedure.use).toHaveBeenCalled(); - }); - - describe("middleware behavior", () => { - let middleware: any; - - beforeEach(() => { - createWebhookPbacProcedure(testPermission, fallbackRoles); - // Get the middleware function that was passed to .use() - middleware = mockAuthedProcedure.use.mock.calls[0][0]; - }); - - describe("when webhook ID is provided", () => { - it("should allow access when user has PBAC permission for team webhook", async () => { - const mockWebhook = { - id: "webhook-1", - teamId: 10, - userId: null, - eventTypeId: null, - user: null, - team: { id: 10 }, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - mockCheckPermission.mockResolvedValue(true); - - const next = vi.fn().mockResolvedValue("success"); - const result = await middleware({ - ctx: mockCtx, - input: { id: "webhook-1", teamId: 10 }, - next, - }); - - expect(result).toBe("success"); - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 10, - permission: testPermission, - fallbackRoles, - }); - }); - - it("should throw FORBIDDEN when user lacks PBAC permission for team webhook", async () => { - const mockWebhook = { - id: "webhook-1", - teamId: 10, - userId: null, - eventTypeId: null, - user: null, - team: { id: 10 }, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - mockCheckPermission.mockResolvedValue(false); - - const next = vi.fn(); - - await expect( - middleware({ - ctx: mockCtx, - input: { id: "webhook-1", teamId: 10 }, - next, - }) - ).rejects.toThrow( - new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${testPermission}`, - }) - ); - }); - - it("should allow access for personal webhook when user is the owner", async () => { - const mockWebhook = { - id: "webhook-1", - teamId: null, - userId: 1, - eventTypeId: null, - user: { id: 1 }, - team: null, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - - const next = vi.fn().mockResolvedValue("success"); - const result = await middleware({ - ctx: mockCtx, - input: { id: "webhook-1" }, - next, - }); - - expect(result).toBe("success"); - expect(mockCheckPermission).not.toHaveBeenCalled(); - }); - - it("should throw FORBIDDEN for personal webhook when user is not the owner", async () => { - const mockWebhook = { - id: "webhook-1", - teamId: null, - userId: 2, - eventTypeId: null, - user: { id: 2 }, - team: null, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - - const next = vi.fn(); - - await expect( - middleware({ - ctx: mockCtx, - input: { id: "webhook-1" }, - next, - }) - ).rejects.toThrow( - new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${testPermission}`, - }) - ); - }); - - it("should check team permissions for team event type webhook", async () => { - const mockWebhook = { - id: "webhook-1", - teamId: null, - userId: null, - eventTypeId: 100, - user: null, - team: null, - eventType: { id: 100, teamId: 10, userId: 2 }, - }; - - const mockEventType = { - id: 100, - teamId: 10, - userId: 2, - team: { id: 10 }, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - mockPrisma.eventType.findUnique.mockResolvedValue(mockEventType); - mockCheckPermission.mockResolvedValue(true); - - const next = vi.fn().mockResolvedValue("success"); - const result = await middleware({ - ctx: mockCtx, - input: { id: "webhook-1", eventTypeId: 100 }, - next, - }); - - expect(result).toBe("success"); - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 10, - permission: testPermission, - fallbackRoles, - }); - }); - - it("should throw NOT_FOUND when webhook doesn't exist", async () => { - mockPrisma.webhook.findUnique.mockResolvedValue(null); - - const next = vi.fn(); - - await expect( - middleware({ - ctx: mockCtx, - input: { id: "webhook-1" }, - next, - }) - ).rejects.toThrow(new TRPCError({ code: "NOT_FOUND" })); - }); - - it("should throw UNAUTHORIZED when teamId doesn't match webhook teamId", async () => { - const mockWebhook = { - id: "webhook-1", - teamId: 10, - userId: null, - eventTypeId: null, - user: null, - team: { id: 10 }, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - - const next = vi.fn(); - - await expect( - middleware({ - ctx: mockCtx, - input: { id: "webhook-1", teamId: 20 }, - next, - }) - ).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED" })); - }); - }); - - describe("when creating new webhook (no ID provided)", () => { - it("should allow creation with team PBAC permission", async () => { - mockCheckPermission.mockResolvedValue(true); - - const next = vi.fn().mockResolvedValue("success"); - const result = await middleware({ - ctx: mockCtx, - input: { teamId: 10 }, - next, - }); - - expect(result).toBe("success"); - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 10, - permission: testPermission, - fallbackRoles, - }); - }); - - it("should throw FORBIDDEN when lacking team PBAC permission", async () => { - mockCheckPermission.mockResolvedValue(false); - - const next = vi.fn(); - - await expect( - middleware({ - ctx: mockCtx, - input: { teamId: 10 }, - next, - }) - ).rejects.toThrow( - new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${testPermission}`, - }) - ); - }); - - it("should check team permissions for team event type creation", async () => { - const mockEventType = { - id: 100, - teamId: 10, - userId: 2, - team: { id: 10 }, - }; - - mockPrisma.eventType.findUnique.mockResolvedValue(mockEventType); - mockCheckPermission.mockResolvedValue(true); - - const next = vi.fn().mockResolvedValue("success"); - const result = await middleware({ - ctx: mockCtx, - input: { eventTypeId: 100 }, - next, - }); - - expect(result).toBe("success"); - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 10, - permission: testPermission, - fallbackRoles, - }); - }); - - it("should allow personal event type webhook creation for owner", async () => { - const mockEventType = { - id: 100, - teamId: null, - userId: 1, - team: null, - }; - - mockPrisma.eventType.findUnique.mockResolvedValue(mockEventType); - - const next = vi.fn().mockResolvedValue("success"); - const result = await middleware({ - ctx: mockCtx, - input: { eventTypeId: 100 }, - next, - }); - - expect(result).toBe("success"); - expect(mockCheckPermission).not.toHaveBeenCalled(); - }); - }); - - describe("when no input is provided", () => { - it("should call next() directly", async () => { - const next = vi.fn().mockResolvedValue("success"); - const result = await middleware({ - ctx: mockCtx, - input: undefined, - next, - }); - - expect(result).toBe("success"); - expect(next).toHaveBeenCalled(); - expect(mockPrisma.webhook.findUnique).not.toHaveBeenCalled(); - expect(mockPrisma.eventType.findUnique).not.toHaveBeenCalled(); - }); - }); - }); - }); - - describe("webhookProcedure (legacy wrapper)", () => { - it("should work as expected", () => { - // The legacy webhookProcedure uses createWebhookPbacProcedure internally - // This is verified by the functional tests above - expect(createWebhookPbacProcedure).toBeDefined(); - }); - }); - - describe("Different permission scenarios", () => { - const permissions: { permission: PermissionString; operation: string }[] = [ - { permission: "webhook.read", operation: "read" }, - { permission: "webhook.create", operation: "create" }, - { permission: "webhook.update", operation: "update" }, - { permission: "webhook.delete", operation: "delete" }, - ]; - - permissions.forEach(({ permission, operation }) => { - it(`should use ${permission} for ${operation} operations`, async () => { - createWebhookPbacProcedure(permission); - const middleware = - mockAuthedProcedure.use.mock.calls[mockAuthedProcedure.use.mock.calls.length - 1][0]; - - const mockWebhook = { - id: "webhook-1", - teamId: 10, - userId: null, - eventTypeId: null, - user: null, - team: { id: 10 }, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - mockCheckPermission.mockResolvedValue(true); - - const next = vi.fn().mockResolvedValue("success"); - await middleware({ - ctx: mockCtx, - input: { id: "webhook-1" }, - next, - }); - - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 10, - permission, - fallbackRoles: ["ADMIN", "OWNER"], - }); - }); - }); - }); - - describe("Fallback role behavior", () => { - it("should use custom fallback roles when provided", async () => { - const customFallback: MembershipRole[] = ["OWNER"]; - createWebhookPbacProcedure("webhook.delete", customFallback); - const middleware = mockAuthedProcedure.use.mock.calls[mockAuthedProcedure.use.mock.calls.length - 1][0]; - - const mockWebhook = { - id: "webhook-1", - teamId: 10, - userId: null, - eventTypeId: null, - user: null, - team: { id: 10 }, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - mockCheckPermission.mockResolvedValue(true); - - const next = vi.fn().mockResolvedValue("success"); - await middleware({ - ctx: mockCtx, - input: { id: "webhook-1" }, - next, - }); - - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 10, - permission: "webhook.delete", - fallbackRoles: customFallback, - }); - }); - - it("should use default fallback roles when not provided", async () => { - createWebhookPbacProcedure("webhook.update"); - const middleware = mockAuthedProcedure.use.mock.calls[mockAuthedProcedure.use.mock.calls.length - 1][0]; - - const mockWebhook = { - id: "webhook-1", - teamId: 10, - userId: null, - eventTypeId: null, - user: null, - team: { id: 10 }, - eventType: null, - }; - - mockPrisma.webhook.findUnique.mockResolvedValue(mockWebhook); - mockCheckPermission.mockResolvedValue(true); - - const next = vi.fn().mockResolvedValue("success"); - await middleware({ - ctx: mockCtx, - input: { id: "webhook-1" }, - next, - }); - - expect(mockCheckPermission).toHaveBeenCalledWith({ - userId: 1, - teamId: 10, - permission: "webhook.update", - fallbackRoles: ["ADMIN", "OWNER"], - }); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/webhook/util.ts b/packages/trpc/server/routers/viewer/webhook/util.ts index e576ed64659de2..19909e92b83fb1 100644 --- a/packages/trpc/server/routers/viewer/webhook/util.ts +++ b/packages/trpc/server/routers/viewer/webhook/util.ts @@ -1,157 +1,140 @@ -import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; +import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; import { prisma } from "@calcom/prisma"; -import type { MembershipRole } from "@calcom/prisma/enums"; +import type { Membership } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; import authedProcedure from "../../../procedures/authedProcedure"; import { webhookIdAndEventTypeIdSchema } from "./types"; -/** - * Creates a webhook procedure with configurable PBAC permissions - * @param permission - The specific permission required (e.g., "webhook.create", "webhook.update") - * @param fallbackRoles - Roles to check when PBAC is disabled (defaults to ["ADMIN", "OWNER"]) - * @returns A procedure that checks the specified permission - */ -export const createWebhookPbacProcedure = ( - permission: PermissionString, - fallbackRoles: MembershipRole[] = ["ADMIN", "OWNER"] -) => { - return authedProcedure.input(webhookIdAndEventTypeIdSchema.optional()).use(async ({ ctx, input, next }) => { - // Endpoints that just read the logged in user's data - like 'list' don't necessarily have any input +export const webhookProcedure = authedProcedure + .input(webhookIdAndEventTypeIdSchema.optional()) + .use(async ({ ctx, input, next }) => { + // Endpoints that just read the logged in user's data - like 'list' don't necessary have any input if (!input) return next(); - const { id, teamId, eventTypeId } = input; - const permissionCheckService = new PermissionCheckService(); + + const assertPartOfTeamWithRequiredAccessLevel = (memberships?: Membership[], teamId?: number) => { + if (!memberships) return false; + if (teamId) { + return memberships.some( + (membership) => membership.teamId === teamId && checkAdminOrOwner(membership.role) + ); + } + return memberships.some( + (membership) => membership.userId === ctx.user.id && checkAdminOrOwner(membership.role) + ); + }; if (id) { - // Check if user is authorized to edit webhook + //check if user is authorized to edit webhook const webhook = await prisma.webhook.findUnique({ - where: { id }, - select: { - id: true, - userId: true, - teamId: true, - eventTypeId: true, + where: { + id: id, + }, + include: { + user: true, + team: true, + eventType: true, }, }); - if (!webhook) { - throw new TRPCError({ code: "NOT_FOUND" }); - } - - // Validate consistency - if (teamId && teamId !== webhook.teamId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - - if (eventTypeId && eventTypeId !== webhook.eventTypeId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - - // For team webhooks, check PBAC permissions - if (webhook.teamId) { - const hasPermission = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId: webhook.teamId, - permission, - fallbackRoles, - }); - - if (!hasPermission) { + if (webhook) { + if (teamId && teamId !== webhook.teamId) { throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, + code: "UNAUTHORIZED", }); } - } else if (webhook.eventTypeId) { - // For event type webhooks, check if the user owns the event type or has team permissions - const eventType = await prisma.eventType.findUnique({ - where: { id: webhook.eventTypeId }, - include: { team: true }, - }); - if (!eventType) { - throw new TRPCError({ code: "NOT_FOUND" }); + if (eventTypeId && eventTypeId !== webhook.eventTypeId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); } - if (eventType.userId !== ctx.user.id) { - // Check team permissions if it's a team event type - if (eventType.teamId) { - const hasPermission = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId: eventType.teamId, - permission, - fallbackRoles, + if (webhook.teamId) { + const user = await prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + include: { + teams: true, + }, + }); + + const userHasAdminOwnerPermissionInTeam = + user && + user.teams.some( + (membership) => membership.teamId === webhook.teamId && checkAdminOrOwner(membership.role) + ); + + if (!userHasAdminOwnerPermissionInTeam) { + throw new TRPCError({ + code: "UNAUTHORIZED", }); + } + } else if (webhook.eventTypeId) { + const eventType = await prisma.eventType.findUnique({ + where: { + id: webhook.eventTypeId, + }, + include: { + team: { + include: { + members: true, + }, + }, + }, + }); - if (!hasPermission) { + if (eventType && eventType.userId !== ctx.user.id) { + if (!assertPartOfTeamWithRequiredAccessLevel(eventType.team?.members)) { throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, + code: "UNAUTHORIZED", }); } - } else { - throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, - }); } + } else if (webhook.userId && webhook.userId !== ctx.user.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + }); } - } else if (webhook.userId && webhook.userId !== ctx.user.id) { - // For personal webhooks, only the owner can manage - throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, - }); } } else { - // Check if user is authorized to create webhook on event type or team + //check if user is authorized to create webhook on event type or team if (teamId) { - const hasPermission = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId, - permission, - fallbackRoles, + const user = await prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + include: { + teams: true, + }, }); - if (!hasPermission) { + if (!assertPartOfTeamWithRequiredAccessLevel(user?.teams, teamId)) { throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, + code: "UNAUTHORIZED", }); } } else if (eventTypeId) { const eventType = await prisma.eventType.findUnique({ - where: { id: eventTypeId }, - include: { team: true }, + where: { + id: eventTypeId, + }, + include: { + team: { + include: { + members: true, + }, + }, + }, }); - if (!eventType) { - throw new TRPCError({ code: "NOT_FOUND" }); - } - - if (eventType.userId !== ctx.user.id) { - // Check team permissions if it's a team event type - if (eventType.teamId) { - const hasPermission = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId: eventType.teamId, - permission, - fallbackRoles, - }); - - if (!hasPermission) { - throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, - }); - } - } else { + if (eventType && eventType.userId !== ctx.user.id) { + if (!assertPartOfTeamWithRequiredAccessLevel(eventType.team?.members)) { throw new TRPCError({ - code: "FORBIDDEN", - message: `Permission required: ${permission}`, + code: "UNAUTHORIZED", }); } } @@ -160,10 +143,3 @@ export const createWebhookPbacProcedure = ( return next(); }); -}; - -/** - * Legacy webhook procedure - uses the new PBAC procedure with webhook.update permission - * This maintains backward compatibility while supporting PBAC - */ -export const webhookProcedure = createWebhookPbacProcedure("webhook.update", ["ADMIN", "OWNER"]); diff --git a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts index 6e6f32988ff08b..20bac521c880de 100644 --- a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts @@ -1,7 +1,6 @@ import { scheduleEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager"; import { scheduleSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager"; import { scheduleWhatsappReminder } from "@calcom/features/ee/workflows/lib/reminders/whatsappReminderManager"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import { WorkflowRepository } from "@calcom/lib/server/repository/workflow"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; @@ -38,6 +37,9 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType some: { userId: ctx.user.id, accepted: true, + NOT: { + role: MembershipRole.MEMBER, + }, }, }, }, @@ -95,21 +97,6 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType if (!eventType) throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authorized to edit this event type" }); - if (eventType.teamId) { - const permissionCheckService = new PermissionCheckService(); - - const hasPermissionToActivate = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId: eventType.teamId, - permission: "eventType.update", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasPermissionToActivate) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authorized to edit this event type" }); - } - } - // at this point we know that the event type belongs to the user or team // so we don't use OR, we use logic. const whereClause = eventType.teamId ? { teamId: eventType.teamId } : { userId: ctx.user.id }; diff --git a/packages/trpc/server/routers/viewer/workflows/delete.handler.test.ts b/packages/trpc/server/routers/viewer/workflows/delete.handler.test.ts deleted file mode 100644 index febd4d72ef734f..00000000000000 --- a/packages/trpc/server/routers/viewer/workflows/delete.handler.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { prisma } from "@calcom/prisma/__mocks__/prisma"; - -import { describe, it, expect, vi, beforeEach } from "vitest"; - -import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone"; -import { WorkflowRepository } from "@calcom/lib/server/repository/workflow"; -import { WorkflowActions } from "@calcom/prisma/enums"; - -import { TRPCError } from "@trpc/server"; - -import { deleteHandler } from "./delete.handler"; -import { - isAuthorized, - removeSmsReminderFieldForEventTypes, - removeAIAgentCallPhoneNumberFieldForEventTypes, -} from "./util"; - -vi.mock("@calcom/prisma", () => ({ - prisma, -})); - -vi.mock("@calcom/features/calAIPhone", () => ({ - createDefaultAIPhoneServiceProvider: vi.fn(), -})); - -vi.mock("@calcom/lib/server/repository/workflow", () => ({ - WorkflowRepository: { - deleteAllWorkflowReminders: vi.fn(), - }, -})); - -vi.mock("./util", () => ({ - isAuthorized: vi.fn(), - removeSmsReminderFieldForEventTypes: vi.fn(), - removeAIAgentCallPhoneNumberFieldForEventTypes: vi.fn(), -})); - -describe("deleteHandler", () => { - const mockCreateDefaultAIPhoneServiceProvider = vi.mocked(createDefaultAIPhoneServiceProvider); - const mockIsAuthorized = vi.mocked(isAuthorized); - const mockRemoveSmsReminderFieldForEventTypes = vi.mocked(removeSmsReminderFieldForEventTypes); - const mockRemoveAIAgentCallPhoneNumberFieldForEventTypes = vi.mocked( - removeAIAgentCallPhoneNumberFieldForEventTypes - ); - const mockDeleteAllWorkflowReminders = vi.mocked(WorkflowRepository.deleteAllWorkflowReminders); - - const mockUser = { - id: 123, - name: "Test User", - email: "test@example.com", - }; - - const mockCtx = { - user: mockUser, - }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("Authorization", () => { - it("should throw UNAUTHORIZED when user is not authorized or workflow not found", async () => { - const workflowId = 1; - - prisma.workflow.findUnique.mockResolvedValue({ - id: workflowId, - teamId: 456, - userId: 789, - activeOn: [], - activeOnTeams: [], - steps: [], - team: null, - }); - mockIsAuthorized.mockResolvedValue(false); - - await expect(deleteHandler({ ctx: mockCtx, input: { id: workflowId } })).rejects.toThrow( - new TRPCError({ code: "UNAUTHORIZED" }) - ); - - prisma.workflow.findUnique.mockResolvedValue(null); - - await expect(deleteHandler({ ctx: mockCtx, input: { id: workflowId } })).rejects.toThrow( - new TRPCError({ code: "UNAUTHORIZED" }) - ); - }); - }); - - describe("Booking field cleanup", () => { - it("should remove both SMS reminder and AI agent phone number fields", async () => { - const workflowId = 1; - const eventTypeIds = [10, 20]; - const mockWorkflow = { - id: workflowId, - teamId: null, - userId: mockUser.id, - activeOn: eventTypeIds.map((id) => ({ eventTypeId: id })), - activeOnTeams: [], - steps: [], - team: null, - }; - - prisma.workflow.findUnique.mockResolvedValue(mockWorkflow); - mockIsAuthorized.mockResolvedValue(true); - prisma.workflowReminder.findMany.mockResolvedValue([]); - prisma.workflow.deleteMany.mockResolvedValue({ count: 1 }); - - await deleteHandler({ ctx: mockCtx, input: { id: workflowId } }); - - expect(mockRemoveSmsReminderFieldForEventTypes).toHaveBeenCalledWith({ - activeOnToRemove: eventTypeIds, - workflowId: workflowId, - isOrg: false, - }); - - expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalledWith({ - activeOnToRemove: eventTypeIds, - workflowId: workflowId, - isOrg: false, - }); - }); - - it("should handle organization workflows correctly", async () => { - const workflowId = 1; - const teamIds = [100, 200]; - const mockWorkflow = { - id: workflowId, - teamId: 456, - userId: mockUser.id, - activeOn: [], - activeOnTeams: teamIds.map((id) => ({ teamId: id })), - steps: [], - team: { - isOrganization: true, - }, - }; - - prisma.workflow.findUnique.mockResolvedValue(mockWorkflow); - mockIsAuthorized.mockResolvedValue(true); - prisma.workflowReminder.findMany.mockResolvedValue([]); - prisma.workflow.deleteMany.mockResolvedValue({ count: 1 }); - - await deleteHandler({ ctx: mockCtx, input: { id: workflowId } }); - - expect(mockRemoveSmsReminderFieldForEventTypes).toHaveBeenCalledWith({ - activeOnToRemove: teamIds, - workflowId: workflowId, - isOrg: true, - }); - - expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalledWith({ - activeOnToRemove: teamIds, - workflowId: workflowId, - isOrg: true, - }); - }); - }); - - describe("CAL AI phone call cleanup", () => { - let mockAIPhoneService: { - cancelPhoneNumberSubscription: ReturnType; - deletePhoneNumber: ReturnType; - deleteAgent: ReturnType; - }; - - beforeEach(() => { - mockAIPhoneService = { - cancelPhoneNumberSubscription: vi.fn(), - deletePhoneNumber: vi.fn(), - deleteAgent: vi.fn(), - }; - mockCreateDefaultAIPhoneServiceProvider.mockReturnValue(mockAIPhoneService); - }); - - it("should cleanup AI phone resources based on subscription status", async () => { - const workflowId = 1; - const mockWorkflow = { - id: workflowId, - teamId: null, - userId: mockUser.id, - activeOn: [], - activeOnTeams: [], - steps: [ - { - action: WorkflowActions.CAL_AI_PHONE_CALL, - agent: { - id: "agent-1", - outboundPhoneNumbers: [ - { - id: "phone-active", - phoneNumber: "+1111111111", - subscriptionStatus: "ACTIVE", - }, - { - id: "phone-null", - phoneNumber: "+2222222222", - subscriptionStatus: null, - }, - ], - }, - }, - ], - team: null, - }; - - prisma.workflow.findUnique.mockResolvedValue(mockWorkflow); - mockIsAuthorized.mockResolvedValue(true); - prisma.workflowReminder.findMany.mockResolvedValue([]); - prisma.workflow.deleteMany.mockResolvedValue({ count: 1 }); - - await deleteHandler({ ctx: mockCtx, input: { id: workflowId } }); - - expect(mockAIPhoneService.cancelPhoneNumberSubscription).toHaveBeenCalledWith({ - phoneNumberId: "phone-active", - userId: mockUser.id, - }); - - expect(mockAIPhoneService.deletePhoneNumber).toHaveBeenCalledWith({ - phoneNumber: "+2222222222", - userId: mockUser.id, - deleteFromDB: true, - }); - - expect(mockAIPhoneService.deleteAgent).toHaveBeenCalledWith({ - id: "agent-1", - userId: mockUser.id, - teamId: undefined, - }); - - expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalled(); - }); - }); - - describe("Workflow deletion flow", () => { - it("should complete full deletion flow successfully", async () => { - const workflowId = 1; - const mockReminders = [ - { - id: 1, - workflowStepId: 1, - scheduled: true, - referenceId: "ref-1", - }, - ]; - const mockWorkflow = { - id: workflowId, - teamId: null, - userId: mockUser.id, - activeOn: [{ eventTypeId: 10 }], - activeOnTeams: [], - steps: [], - team: null, - }; - - prisma.workflow.findUnique.mockResolvedValue(mockWorkflow); - mockIsAuthorized.mockResolvedValue(true); - prisma.workflowReminder.findMany.mockResolvedValue(mockReminders); - prisma.workflow.deleteMany.mockResolvedValue({ count: 1 }); - - const result = await deleteHandler({ ctx: mockCtx, input: { id: workflowId } }); - - expect(prisma.workflowReminder.findMany).toHaveBeenCalledWith({ - where: { - workflowStep: { - workflowId: workflowId, - }, - scheduled: true, - NOT: { - referenceId: null, - }, - }, - }); - - expect(mockDeleteAllWorkflowReminders).toHaveBeenCalledWith(mockReminders); - - expect(mockRemoveSmsReminderFieldForEventTypes).toHaveBeenCalled(); - expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalled(); - - expect(prisma.workflow.deleteMany).toHaveBeenCalledWith({ - where: { - id: workflowId, - }, - }); - - expect(result).toEqual({ id: workflowId }); - }); - }); -}); diff --git a/packages/trpc/server/routers/viewer/workflows/delete.handler.ts b/packages/trpc/server/routers/viewer/workflows/delete.handler.ts index 181b114d1d50ad..e51dcc5647f0f6 100644 --- a/packages/trpc/server/routers/viewer/workflows/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/delete.handler.ts @@ -7,11 +7,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; import type { TDeleteInputSchema } from "./delete.schema"; -import { - isAuthorized, - removeSmsReminderFieldForEventTypes, - removeAIAgentCallPhoneNumberFieldForEventTypes, -} from "./util"; +import { isAuthorized, removeSmsReminderFieldForEventTypes } from "./util"; type DeleteOptions = { ctx: { @@ -142,11 +138,6 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => { : workflowToDelete.activeOn.map((activeOn) => activeOn.eventTypeId); await removeSmsReminderFieldForEventTypes({ activeOnToRemove, workflowId: workflowToDelete.id, isOrg }); - await removeAIAgentCallPhoneNumberFieldForEventTypes({ - activeOnToRemove, - workflowId: workflowToDelete.id, - isOrg, - }); // automatically deletes all steps and reminders connected to this workflow await prisma.workflow.deleteMany({ diff --git a/packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts b/packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts index e1b17fa9d26f94..d580cf34167a99 100644 --- a/packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.handler.ts @@ -1,6 +1,5 @@ import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; -import { MembershipRole } from "@calcom/prisma/enums"; +import { isTeamMember } from "@calcom/lib/server/queries/teams"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import { TRPCError } from "@trpc/server"; @@ -26,39 +25,20 @@ export const getAllActiveWorkflowsHandler = async ({ input, ctx }: GetAllActiveW metadata: eventType.metadata, }; - if ( - eventType.userId && - eventType.userId !== ctx.user.id && - !eventType.teamId && - !eventType.parent?.teamId - ) { + if (eventType.userId && eventType.userId !== ctx.user.id) { throw new TRPCError({ code: "UNAUTHORIZED", }); } - const permissionCheckService = new PermissionCheckService(); - if (eventType.teamId) { - const hasPermissionToViewWorkflows = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId: eventType.teamId, - permission: "workflow.read", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasPermissionToViewWorkflows) throw new TRPCError({ code: "UNAUTHORIZED" }); + const team = await isTeamMember(ctx.user?.id, eventType.teamId); + if (!team) throw new TRPCError({ code: "UNAUTHORIZED" }); } if (eventType.parent?.teamId) { - const hasPermissionToViewWorkflows = await permissionCheckService.checkPermission({ - userId: ctx.user.id, - teamId: eventType.parent?.teamId, - permission: "workflow.read", - fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }); - - if (!hasPermissionToViewWorkflows) throw new TRPCError({ code: "UNAUTHORIZED" }); + const team = await isTeamMember(ctx.user?.id, eventType.parent?.teamId); + if (!team) throw new TRPCError({ code: "UNAUTHORIZED" }); } const allActiveWorkflows = await getAllWorkflowsFromEventType( diff --git a/packages/trpc/server/routers/viewer/workflows/list.handler.ts b/packages/trpc/server/routers/viewer/workflows/list.handler.ts index ba2a159f666325..67c1f58ee6dccd 100644 --- a/packages/trpc/server/routers/viewer/workflows/list.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/list.handler.ts @@ -1,9 +1,7 @@ import type { WorkflowType } from "@calcom/features/ee/workflows/components/WorkflowListPage"; // import dayjs from "@calcom/dayjs"; // import { getErrorFromUnknown } from "@calcom/lib/errors"; -import { addPermissionsToWorkflows } from "@calcom/lib/server/repository/workflow-permissions"; import { prisma } from "@calcom/prisma"; -import type { PrismaClient } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -12,7 +10,6 @@ import type { TListInputSchema } from "./list.schema"; type ListOptions = { ctx: { user: NonNullable; - prisma: PrismaClient; }; input: TListInputSchema; }; @@ -159,13 +156,7 @@ export const listHandler = async ({ ctx, input }: ListOptions) => { workflows.push(...workflowsWithReadOnly); - // Add permissions to each workflow - const workflowsWithPermissions = await addPermissionsToWorkflows(workflows, ctx.user.id); - - // Filter workflows based on view permission - const filteredWorkflows = workflowsWithPermissions.filter((workflow) => workflow.permissions.canView); - - return { workflows: filteredWorkflows }; + return { workflows }; } if (input && input.userId) { @@ -207,13 +198,7 @@ export const listHandler = async ({ ctx, input }: ListOptions) => { workflows.push(...userWorkflows); - // Add permissions to each workflow - const workflowsWithPermissions = await addPermissionsToWorkflows(workflows, ctx.user.id); - - // Filter workflows based on view permission - const filteredWorkflows = workflowsWithPermissions.filter((workflow) => workflow.permissions.canView); - - return { workflows: filteredWorkflows }; + return { workflows }; } const allWorkflows = await prisma.workflow.findMany({ @@ -274,11 +259,5 @@ export const listHandler = async ({ ctx, input }: ListOptions) => { workflows.push(...workflowsWithReadOnly); - // Add permissions to each workflow - const workflowsWithPermissions = await addPermissionsToWorkflows(workflows, ctx.user.id); - - // Filter workflows based on view permission - const filteredWorkflows = workflowsWithPermissions.filter((workflow) => workflow.permissions.canView); - - return { workflows: filteredWorkflows }; + return { workflows }; }; diff --git a/packages/ui/components/test-setup.tsx b/packages/ui/components/test-setup.tsx index 95ed0b4700a4db..237b587cd0f888 100644 --- a/packages/ui/components/test-setup.tsx +++ b/packages/ui/components/test-setup.tsx @@ -80,8 +80,8 @@ vi.mock("@calcom/atoms/hooks/useIsPlatform", () => ({ }, })); -vi.mock("@calcom/features/eventtypes/lib/getEventTypesByViewer", () => ({})); -vi.mock("@calcom/features/eventtypes/lib/getEventTypesPublic", () => ({})); +vi.mock("@calcom/lib/event-types/getEventTypesByViewer", () => ({})); +vi.mock("@calcom/lib/event-types/getEventTypesPublic", () => ({})); vi.mock("@calcom/ui/classNames", () => ({ default: (...args: string[]) => { return args.filter(Boolean).join(" "); diff --git a/scripts/seed-performance-testing.ts b/scripts/seed-performance-testing.ts index 408ce2aecf19bd..a244275c24a1f6 100644 --- a/scripts/seed-performance-testing.ts +++ b/scripts/seed-performance-testing.ts @@ -11,7 +11,7 @@ import zoomMeta from "@calcom/app-store/zoomvideo/_metadata"; import dayjs from "@calcom/dayjs"; import { BookingStatus } from "@calcom/prisma/enums"; -import { createUserAndEventType } from "./seed-utils"; +import { createUserAndEventType } from "../packages/prisma/seed-utils"; async function _createManyDifferentUsersWithDifferentEventTypesAndBookings({ tillUser, @@ -322,4 +322,4 @@ async function createAUserWithManyBookings() { // startFrom: 10000, // }); -createAUserWithManyBookings(); +createAUserWithManyBookings(); \ No newline at end of file diff --git a/scripts/seed.ts b/scripts/seed.ts index cb52433c21efe0..d60e1a19f17022 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -6,7 +6,7 @@ import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata"; import zoomMeta from "@calcom/app-store/zoomvideo/_metadata"; import dayjs from "@calcom/dayjs"; import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; -import { hashPassword } from "@calcom/lib/auth/hashPassword"; +import { hashPassword } from "@calcom/features/auth/lib/hashPassword"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import prisma from "@calcom/prisma"; import type { Membership, Team, User, UserPermissionRole } from "@calcom/prisma/client"; @@ -14,10 +14,10 @@ import { Prisma } from "@calcom/prisma/client"; import { BookingStatus, MembershipRole, RedirectType, SchedulingType } from "@calcom/prisma/enums"; import type { Ensure } from "@calcom/types/utils"; +import mainHugeEventTypesSeed from "../packages/prisma/seed-huge-event-types"; +import { createUserAndEventType } from "../packages/prisma/seed-utils"; import type { teamMetadataSchema } from "../packages/prisma/zod-utils"; import mainAppStore from "./seed-app-store"; -import mainHugeEventTypesSeed from "./seed-huge-event-types"; -import { createUserAndEventType } from "./seed-utils"; type PlatformUser = { email: string; diff --git a/tests/libs/__mocks__/CalendarManager.ts b/tests/libs/__mocks__/CalendarManager.ts index ecfdc6d4a2c970..4554b060e2af28 100644 --- a/tests/libs/__mocks__/CalendarManager.ts +++ b/tests/libs/__mocks__/CalendarManager.ts @@ -1,9 +1,9 @@ import { beforeEach, vi } from "vitest"; import { mockReset, mockDeep } from "vitest-mock-extended"; -import type * as CalendarManager from "@calcom/features/calendars/lib/CalendarManager"; +import type * as CalendarManager from "@calcom/lib/CalendarManager"; -vi.mock("@calcom/features/calendars/lib/CalendarManager", () => CalendarManagerMock); +vi.mock("@calcom/lib/CalendarManager", () => CalendarManagerMock); beforeEach(() => { mockReset(CalendarManagerMock); diff --git a/tests/libs/__mocks__/prisma.ts b/tests/libs/__mocks__/prisma.ts index ae47d5f0b40732..500ac1cec87c20 100644 --- a/tests/libs/__mocks__/prisma.ts +++ b/tests/libs/__mocks__/prisma.ts @@ -2,9 +2,10 @@ import { createPrismock } from "prismock"; import { beforeEach, vi } from "vitest"; import logger from "@calcom/lib/logger"; -import { Prisma as PrismaType } from "@calcom/prisma/client"; import * as selects from "@calcom/prisma/selects"; +import { Prisma as PrismaType } from "@calcom/prisma/client"; + vi.stubEnv("DATABASE_URL", "postgresql://user:password@localhost:5432/testdb"); vi.mock("@calcom/prisma", () => ({ diff --git a/tests/libs/__mocks__/videoClient.ts b/tests/libs/__mocks__/videoClient.ts index 7e5891215c8b17..cc21f8c1918603 100644 --- a/tests/libs/__mocks__/videoClient.ts +++ b/tests/libs/__mocks__/videoClient.ts @@ -1,9 +1,9 @@ import { beforeEach, vi } from "vitest"; import { mockReset, mockDeep } from "vitest-mock-extended"; -import type * as videoClient from "@calcom/app-store/videoClient"; +import type * as videoClient from "@calcom/lib/videoClient"; -vi.mock("@calcom/app-store/videoClient", () => videoClientMock); +vi.mock("@calcom/lib/videoClient", () => videoClientMock); beforeEach(() => { mockReset(videoClientMock); diff --git a/turbo.json b/turbo.json index 354a54b469b361..1042760af8085f 100644 --- a/turbo.json +++ b/turbo.json @@ -276,18 +276,10 @@ "NEXT_PUBLIC_WEBAPP_URL", "NEXT_PUBLIC_WEBSITE_URL", "BUILD_STANDALONE", - "ATOMS_E2E_APPLE_ID", - "ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE", "INTERCOM_API_TOKEN", - "NEXT_PUBLIC_INTERCOM_APP_ID", - "_CAL_INTERNAL_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS" + "NEXT_PUBLIC_INTERCOM_APP_ID" ], "tasks": { - "@calcom/web#copy-app-store-static": { - "inputs": ["../../packages/app-store/**/static/**/*"], - "outputLogs": "new-only", - "outputs": ["public/app-store/**"] - }, "@calcom/prisma#build": { "cache": false, "dependsOn": ["post-install"] diff --git a/yarn.lock b/yarn.lock index 50f62e771b9666..88481ec2d7febc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2936,7 +2936,6 @@ __metadata: "@types/react": ^18 "@types/react-dom": ^18 autoprefixer: ^10.0.1 - dotenv: ^17.2.1 eslint: ^8.34.0 eslint-config-next: 14.0.4 next: 14.0.4 @@ -3822,6 +3821,7 @@ __metadata: resolution: "@calcom/prisma@workspace:packages/prisma" dependencies: "@calcom/lib": "*" + "@faker-js/faker": 9.2.0 "@prisma/client": ^6.7.0 "@prisma/extension-accelerate": ^1.3.0 "@prisma/generator-helper": ^6.7.0 @@ -6031,9 +6031,9 @@ __metadata: linkType: hard "@faker-js/faker@npm:^7.3.0": - version: 7.6.0 - resolution: "@faker-js/faker@npm:7.6.0" - checksum: 942af6221774e8c98a0eb6bc75265e05fb81a941170377666c3439aab9495dd321d6beedc5406f07e6ad44262b3e43c20961f666d116ad150b78e7437dd1bb2b + version: 7.4.0 + resolution: "@faker-js/faker@npm:7.4.0" + checksum: 1acebb84bfb142c08e6ba2942910d16bd92ea147fa585fa2fa9ce9983f7a8c7c016002beb7fc20ef6f7aef6c4ae9cb3fa680b275823402e34802b8489d2b980d languageName: node linkType: hard @@ -21668,7 +21668,6 @@ __metadata: "@changesets/cli": 2.29.4 "@daily-co/daily-js": ^0.83.1 "@evyweb/ioctopus": ^1.2.0 - "@faker-js/faker": 9.2.0 "@jetstreamapp/soql-parser-js": ^6.1.0 "@next/third-parties": ^14.2.5 "@playwright/test": ^1.45.3 @@ -24752,13 +24751,6 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^17.2.1": - version: 17.2.2 - resolution: "dotenv@npm:17.2.2" - checksum: 673825993b16a6722332b2e1f8c24b1c2ebe3dd3b81ae5df9be35f1483bf52e0b463555b09da65b756c7abee3cf55ba2ae2628c22874a899556fa787fac56019 - languageName: node - linkType: hard - "dotenv@npm:^8.1.0": version: 8.6.0 resolution: "dotenv@npm:8.6.0"