Compare commits

..
Author SHA1 Message Date
github-actions[bot] 55df0e3c9d chore: version packages (#23927)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-22 14:41:51 +00:00
sean-brydonandMorgan 730aab3e3d fix: always check team.parentID even if no membership (#23976)
* fix: always check team.parentID even if no membership

* refactor permission handling in create event type handler

* remove hasMembership

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-09-22 12:19:06 +00:00
Rajiv Sahal 63740c02c7 feat: calendar view atom v1 (#23840)
* fix: refactor

* add `isMonthViewProp` to header

* feat: init v1 for calendar view atom

* test breaking toggle buttons

* fix: make sure week start is always sunday for calendar view atom

* fixup

* fix: remove extra comments

* fix: add calendar view page in examples app

* chore: add changesets

* fix: coderabbit feedback

* fixup
2025-09-22 12:17:33 +00:00
Hariom Balhara c5942bcfcd Fix nan prefetch in platform (#23913) 2025-09-22 12:41:22 +03:00
Calcom BotandLingo.dev c4547c51a2 feat: update translations via @LingoDotDev (#23767)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-20 10:12:41 -03:00
Anik Dhabal Babu cf89046ede fix: few more e2e flakes (#23966) 2025-09-20 17:17:01 +05:30
chauhan_sandAnik Dhabal Babu d5d44fb64b fix: remove platform client ID from attendee list when adding team members as attendee in a team event (#23866)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-20 09:28:27 +00:00
David Johnson 4bb092c345 docs: Updated getting started process (#23920)
* Updated gettintg started process

* updated the common errors and added an advanced section to link to the docker repo
2025-09-20 09:26:18 +00:00
Morgan e1f7e94e36 fix: org admins can manage team even-types (#23953)
* fix: org admins can manage team even-types

* fix: org admins can manage team even-types
2025-09-19 21:16:26 +01:00
AnshumancanrockandDevanshu Sharma 2e9d5f7df1 fix: loading spinner appears in top-left corner during Round Robin setup (#23823)
* fix

* fix

* fix

* fix

* fix

* fixed

* fix

* Update globals.css

---------

Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
2025-09-19 19:30:03 +00:00
Morgan 8c943e8817 chore: documentation of event-type bookingRequiresAuthentication (#23942)
* chore: incomplete documentation of event-type bookingRequiresAuthentication setting

* fixup! chore: incomplete documentation of event-type bookingRequiresAuthentication setting
2025-09-19 18:06:32 +00:00
Morgan 56821553fe chore: do not log whole error stack in getConnectedCalendars (#23951) 2025-09-19 17:57:52 +00:00
Amit SharmaandAnik Dhabal Babu b512ceb217 fix: sender name/id missing in workflows (#23950)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-19 17:32:56 +00:00
cfeb18de3f feat: Billing page redesign plus credits (#23908)
* manage billing section

* wip billing credits

* WIP

* WIP

* Download expense log

* credit worth

* skeleton fixes

* add org tip

* add teams tip

* restore service

* type check

* type check

* fix types

* additional credits

* fix progress bar

* add dashed prop

* match new designs

* hide area with no monthly credits

* fix i18n

* show current balance label

* Update apps/web/modules/settings/billing/billing-view.tsx

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* spacing + monthly credits not showing additional

* Remove additional credits from monthly calculations

* feat: replace add members redirect with invite modal in billing settings

- Replace Button href with onClick handler to open MemberInvitationModal
- Add MemberInvitationModalWithoutMembers import and state management
- Maintain existing team/org context support
- Follow established modal usage patterns from other components
- Fix lint error by using undefined instead of empty arrow function

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* Remove redudant vars from method

* fix type check

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-19 16:18:42 +00:00
Eunjae LeeandDevin AI b407be933d feat: add date and time columns to insights CSV exports (#23771)
* feat: add ISO timestamp columns to booking CSV exports

- Add createdAt_iso, startTime_iso, endTime_iso columns to getCsvData() method
- Use .toISOString() format for new columns: 2025-09-09T13:03:55+02:00
- Keep existing timestamp columns unchanged to avoid breaking changes
- Addresses user request for parseable timestamp format in CSV downloads

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

* feat: add ISO timestamp columns to routing CSV exports

- Add ISO format columns for createdAt, bookingCreatedAt, bookingStartTime, bookingEndTime
- Preserve original timestamp format to avoid breaking changes
- Complete implementation for both /insights and /insights/routing pages

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

* feat: replace ISO columns with timezone-aware date/time columns

- Replace _iso columns with separate _date and _time columns
- Use user timezone for proper date/time conversion
- Add timezone parameter to CSV export methods
- Maintain backward compatibility with original timestamp format
- Date format: YYYY-MM-DD, Time format: HH:mm:ss

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

* fix: resolve TypeScript type compatibility issues

- Fix WhereForTeamOrAllTeams type compatibility in routing-events.ts
- Restructure conditional object creation to ensure required properties
- Clean up merge conflict remnants from previous rebase

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

* clean up

* fix: use user profile timezone for CSV exports instead of browser timezone

- Modified rawData endpoint to use ctx.user.timeZone instead of input.timeZone
- Removed timeZone field from bookingRepositoryBaseInputSchema
- Updated useInsightsBookingParameters to remove timeZone property
- Fixed RecentNoShowGuestsChart to use useDataTable for timezone access
- Resolves timezone discrepancy where CSV exports showed incorrect time values

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

* Revert "fix: use user profile timezone for CSV exports instead of browser timezone"

This reverts commit 6356657bd0c503f41349c8d1463bb4dd427b1a2c.

* default columns are formatted as iso

* address feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-19 16:16:54 +00:00
Eunjae Lee 4e1920471d fix: remove page permission check for /insights (#23945)
* fix: remove page permission check for /insights

* add e2e test
2025-09-19 16:04:33 +00:00
Eunjae LeeandAlex van Andel 6ee752aed8 feat: add index [startTime, endTime] for BookingDenormalized (#23944)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-09-19 15:21:23 +00:00
Eunjae LeeandDevin AI a1922efab5 feat: make orgId optional for user and team scopes in /insights and /insights/routing (#23912)
* feat: make orgId optional for user and team scopes in InsightsRoutingBaseService

- User scope authorization only checks formUserId and formTeamId IS NULL
- Team scope now supports standalone teams without organizations
- Add validation logic to return NOTHING_CONDITION if team belongs to org but no orgId provided
- Add comprehensive test coverage for null/undefined orgId scenarios in both scopes
- Aligns schema with actual usage patterns and supports teams without organizations

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

* feat: extend orgId optional support to InsightsBookingBaseService

- Make orgId optional for user and team scopes in InsightsBookingBaseService
- Update InsightsBookingServicePublicOptions type to allow orgId: number | null
- Add validation logic for team scope to handle missing orgId
- Add comprehensive test coverage for null/undefined orgId scenarios
- Fix type casting issues in test file
- Maintains backward compatibility while supporting teams without organizations

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

* fix: correct authorization logic for optional orgId in team scope

- Skip isOwnerOrAdmin check for team scope when orgId is null (standalone teams)
- Maintain security for org scope and team scope with orgId
- Fixes integration test failures for null orgId test cases

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

* fix: use != null instead of !== undefined for orgId checks

- Properly handle both null and undefined orgId values in authorization logic
- Fix integration test failures where null orgId was incorrectly triggering isOwnerOrAdmin check
- Ensure team scope with null orgId skips ownership validation for standalone teams

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

* fix: always validate team membership for team scope

- Remove orgId condition from isOwnerOrAdmin check for team scope
- Ensure both standalone teams and org-based teams require ownership validation
- Maintain orgId validation logic in buildTeamAuthorizationCondition methods

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

* fix: use nullish() for orgId schema validation

- Change from .optional() to .nullish() to allow both null and undefined
- Fixes schema validation when tests pass orgId: null
- Resolves authorization logic returning NOTHING_CONDITION for valid cases

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

* feat: replace isOwnerOrAdmin with PBAC checkPermission in insights services

- Replace isOwnerOrAdmin method in InsightsBookingBaseService with checkPermission from PermissionCheckService
- Replace isOwnerOrAdmin method in InsightsRoutingBaseService with checkPermission from PermissionCheckService
- Use permission 'insights.read' with fallback roles MembershipRole.OWNER and MembershipRole.ADMIN
- Maintain same method signature and behavior while leveraging PBAC system

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

* clean up types

* add comment

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-19 14:13:21 +00:00
Benny Joo 76332a759b refactor: circular deps between app store and lib [5] (#23936)
* getBulkEventTypes

* 2 eventtypes related utils to features

* locationsResolver

* checkForEmptyAssignment

* mv defaultEvents to features

* update imports

* PrismaAppRepository

* mv currencyConversions from appstore to lib

* useAppsData

* videoClient

* analytics files

* fix

* mv

* prettier

* use named import
2025-09-19 10:16:56 -03:00
emrysal dfdda8a5b8 chore: release v5.7.2
Post release / release (push) Canceled after 0s
2025-09-19 11:45:35 +00:00
Amit Sharma 286ee6b1bd fix: disable intercom while being impersonated (#23935) 2025-09-19 10:32:24 +00:00
Benny Joo ef8b0fa41c chore: eslint rule for blocking importing features from appstore, lib, prisma (#23832)
* eslint rule

* improve

* fix

* improve msg
2025-09-19 08:05:53 +00:00
7fbeeae25b fix: event type not found 400 and other (#23904)
* fix: various fixes specifically to event-types

* Revamp error handling a little; highly flawed

* fix: Test cases that depended on defaultResponder behaviour

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-09-18 18:56:00 +00:00
Anik Dhabal Babu 5cf0553199 fix: fix few e2e flakes (#23926) 2025-09-18 14:57:15 -03:00
Lauris Skraucis 4f114ef8d3 fix: EventTypeSettings Checkbox booking field label (#23891) 2025-09-18 17:14:06 +00:00
Anik Dhabal BabuandDevin AI a9d6fbd14d fix: resolve blank form in analytics app installation step 3 (#23924)
- Add mounted state to ensure component renders after hydration
- Fix hydration mismatch between server and client rendering
- Ensure ConfigureStepCard renders properly on initial load in production

Fixes the issue where GTM analytics app configuration form appears blank
on step 3 until page refresh.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-18 17:11:02 +00:00
Eunjae Lee 3e1629add9 chore: compare values in DateTargetSelector (#23916) 2025-09-18 17:10:14 +00:00
f1ac0642d1 fix: correct route pattern and assertions in duplicate API calls E2E test (#23287)
- Fix route pattern from **/api/trpc/viewer.slots.getSchedule** to **/api/trpc/slots/getSchedule**
- Remove permissive assertions that allowed tests to pass with 0 API calls
- Tests now properly fail when duplicate getSchedule calls are detected
- Addresses issue where E2E tests weren't catching duplicate API calls on main branch

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-09-18 17:09:45 +00:00
4689775a1f chore: Skip static file copy if not required (#23903)
* chore: Skip static file copy if not required

* feat: Replace MD5 hash checks with Turborepo caching for static file copying

- Add copy-app-store-static Turborepo task with proper inputs/outputs
- Remove manual MD5 hash calculation logic that was causing performance issues
- Update package.json scripts to use Turborepo task
- Addresses Keith's feedback about using Turborepo cache instead of manual checks

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: Add copy-app-store-static script to package.json for Turborepo task execution

- Add missing script definition in apps/web/package.json
- Enables Turborepo to properly execute the static file copying task
- Fixes 'No tasks were executed' issue by providing the command to run

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: Remove skipped jobs from required check failure condition

- Allow conditional E2E jobs to be skipped without failing the required check
- Maintain failure detection for actual job failures and cancellations
- Apply fix consistently to both pr.yml and all-checks.yml workflows

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* Revert "fix: Remove skipped jobs from required check failure condition"

This reverts commit 429f0506d009eaae7fa88be6db077681d5124f2a.

* Only write new logs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-09-18 16:44:06 +00:00
Benny Joo 4011733271 refactor: remove @calcom/features dependency from @calcom/prisma (#23820) 2025-09-18 16:40:39 +00:00
Keith Williams f41aa234d7 test: Skip broken atom E2E test (#23925) 2025-09-18 13:11:36 -03:00
a44a3e5b84 feat: add Webhook resource to PBAC system with permission enforcement (#23614)
* feat: add Webhook resource to PBAC system with permission enforcement

- Add Webhook resource to PBAC permission registry with CRUD actions
- Implement PBAC permission checks in webhook handlers (create, edit, delete)
- Add webhook permission translations to common.json
- Use PermissionCheckService with fallback roles [ADMIN, OWNER] for team webhooks
- Maintain backward compatibility when PBAC is disabled
- Follow same pattern as workflow PBAC implementation from PR #22845

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* fix: implement PBAC permission filtering in webhook list handler

- Add PermissionCheckService to filter team webhooks by webhook.read permission
- Only show webhooks from teams where user has proper permissions
- Maintain backward compatibility with fallback to all team memberships

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* add migration for default roles

* new forUserMethod

* update webhook repository

* fix UI showing/hiding webhooks for webhoo.create teams

* WIP pbac procedure migratoin + tests

* add more roles to get fallback

* permissions in cmponents instead of readOnly

* passPermissions to list item

* push instant events logic

* Git merge

* wip teamId accessable refactor

* fix delete handler

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-18 07:37:45 -03:00
Hariom Balhara 98a075d062 handle NaN != NaN prefetch (#23910) 2025-09-18 11:34:03 +01:00
Eunjae LeeandDevin AI bdc3cb9d6e feat: allow to choose dateTarget for /insights (startTime by default) (#23752)
* feat: allow to choose dateTarget for /insights (startTime by default)

* feat: add timestamp selector for insights date filtering

- Add TimestampFilter component with Start Time/Created At options
- Extend useInsightsBookingParameters hook with timestamp selection
- Update all insight components to use dateTarget parameter
- Add i18n translations for new UI strings
- Position selector next to DateRangeFilter as requested

Addresses user request to add select box next to date range filter
allowing users to choose between startTime (default) and createdAt
for displaying booking metrics on the Insights page.

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

* refactor: rename TimestampFilter to DateTargetSelector with nuqs URL state

- Rename TimestampFilter component to DateTargetSelector
- Implement nuqs hook in InsightsPageContent for URL state management
- Update useInsightsBookingParameters to return dateTarget from URL state
- Add dateTarget field to insightsRoutingServiceInputSchema and related types
- Simplify individual insight components to use insightsBookingParams directly
- Remove manual timestampTarget destructuring from all components
- Update all tRPC routing service calls to include dateTarget parameter
- All TypeScript checks now pass successfully

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

* update styles

* fix inconsistency

* feat: replace Select with Command component and rename filter ID

- Replace Select with Command + Popover for compact width and wider dropdown
- Add descriptive option labels with translations
- Change filter ID from 'createdAt' to 'timestamp' across all components
- Maintain URL state management with nuqs
- Fix ESLint warning for missing dependency in useEffect

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

* fix: revert routing components to use createdAt filter ID

- Keep timestamp filter ID change scoped only to main insights page
- Routing components should continue using createdAt as filter ID
- Only insights-view.tsx and related booking hooks use timestamp

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

* update styles

* fix trpc router

* refactor timestamp column for insights booking service

* fix

* update text

* rename and clean up

* fix endDate in DateRangeFilter

* fix type errors

* fix startTime filter and type errors

* provide default date range

* add completed to getMembersStatsWithCount

* add unit tests

* fix type error

* address feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-18 11:32:50 +01:00
Rajiv SahalandAlex van Andel 616e4c4908 chore: add stale time for billing check (#23907)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-09-18 09:47:08 +00:00
emrysal eaf695824b chore: release v5.7.1
Post release / release (push) Canceled after 0s
2025-09-18 09:20:19 +00:00
sean-brydon 69a76f7fb7 fix: reassign PBAC role before deleting (#23889) 2025-09-18 08:30:56 +00:00
Anik Dhabal Babu d70fa46260 fix: event startTime is in utc (#23905) 2025-09-18 05:15:32 +00:00
Anik Dhabal Babu efba58a36a fix: users still get updated email when reassign event after disabling standard emails (#23906)
* fix: users still get updated email

* Update yarn.lock

* update
2025-09-18 05:13:04 +00:00
Benny Joo b3a28dca25 refactor: circular deps between app store and lib [4] (#23829)
* mv team queries to features from lib

* update imports

* same

* wip

* move event manager test

* calendar manager to features from lib

* update imports

* update imports

* fix

* mv getEventTypeById

* fix

* fix

* remove

* fix type check error

* fix tests

* fix tests

* fix
2025-09-18 01:42:40 +01:00
Anik Dhabal Babu 3b471f0e25 fix: one e2e flake visible in ci (#23897)
* fix: e2e flake visible in ci

* fix
2025-09-17 21:25:53 +05:30
Anwar SadathandAnik Dhabal Babu aac5fa3537 fix: Showing 0 in onboard members to team screen (#23882)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-17 15:19:59 +00:00
Anwar SadathandAnik Dhabal Babu 134f7e591e fix: Bookings unconfirmed count not getting updated in left sidebar badge (#23886)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-17 15:14:39 +00:00
5bb2bc3ba4 fix: resolve JSON parsing error in Stripe subscription endpoint (#23892)
* fix: resolve JSON parsing error in Stripe subscription endpoint

- Fix 'Unexpected end of JSON input' error during premium username upgrade
- Replace res.status(404).end() with proper JSON response
- Add descriptive error message for missing userId or intentUsername parameters
- Ensure consistent JSON response format across all error paths

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: add proper session handling to Stripe subscription endpoint

- Import getServerSession from @calcom/features/auth/lib/getServerSession
- Use getServerSession({ req }) to properly populate user session
- Fix session validation that was causing 'Unexpected end of JSON input' error
- Ensure subscription endpoint handles sessions consistently with other Cal.com API endpoints

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-17 15:03:16 +00:00
Anik Dhabal Babu bb6f5cca93 fix: Zod error caused by number-type responses (#23804)
* fix: getting zoderror due to number type responses

* update

* fix lint erro
2025-09-17 15:01:21 +00:00
Anik Dhabal BabuandDevin AI 81bd8033a9 fix: resolve hydration mismatch in analytics app installation (#23877)
- Wrap ConfigureStepCard content with NoSSR component
- Prevents blank step 3 configuration page in production
- Fixes hydration mismatch between server and client rendering
- Ensures analytics apps (GTM, Fathom, GA4) installation works properly
- Uses Cal.com's established NoSSR pattern for client-side only rendering

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-17 20:09:28 +05:30
Hariom Balhara c2a44e9319 fix api-v2 tests (#23896) 2025-09-17 14:32:39 +00:00
Hariom BalharaandDevin AI 39917ebfa6 fix: Alternate reschedule behaviour for past bookings - Don't allow new booking (#23736)
* fix: Past booking redirect

* fix: rename ENV_PAST_BOOKING_RESCHEDULE_CHANGE to ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS for clarity

- Renamed environment variable to better reflect that it contains comma-separated team IDs
- Updated all references in constants.ts, implementation file, tests, and turbo.json
- Addresses PR comment feedback about misleading variable name

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-17 12:47:45 +01:00
Anwar SadathandAnik Dhabal Babu 9ee06b410e fix: Teams list not getting refreshed after adding a new team (#23881)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-17 10:09:37 +00:00
76ced2d3b8 feat: event type pbac (#22618)
* fix members page crash with pbac feature flag

* invite member to org backend

* update org permissions

* feat: org profile update settings

* org general page

* remove redudant me call

* privacy page

* add attributes to pbac

* dync + sso on organization permissions

* add tests for resource-permission util

* pass permissions to attributes\

* restore invite members

* update org update and attribute backends

* fix type errors

* fix orgId

* fix types attempt two

* fix types attempt two

* fix type error

* show/hide team event types based on eventType.read permission

* fix dupe string in i18n

* fix tests

* fix team-dsync

* update session to use profile

* use profile metadata to get orgRolew

* fix dsync

* fix typing

* Event type create pbac check

* fix the readonly permission check. WIP

* refactor getUserEventTypes

* fix test

* add generate util for update

* update router to use new generator function for pbac procedures

* update transform utils

* fix members being able to update on UI

* fix allSettled

* fix fallback permissions

* fix permission logic

* fix compare memebrship type error

* fix unit test error

* fix manage test error

* fix nested permissions

* filter based on canView permission in getUserEventGroups

* Update packages/trpc/server/routers/viewer/eventTypes/utils/EventTypeGroupFilter.test.ts

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

* fix tests

* fix tests

* feat: Adding workflow permission checks inside of eventTypes (#23038)

* add permission checks for workflows

* wip getall active workflows

* check eventType.update permission on activiating workflow on event type

* pass permissions to context and use context in client

* remove local parsePermissionString and use registery

* Use new scoped function

* default scope to orgs

* use object values for workflow permissions

* fix type errors

* Fix permission traversal + fix all state caluclation

* fix eventType group to use permissions.canRead

* fix: update failing PBAC unit tests and resolve ESLint warnings

- Fix getUserEventGroups test: MEMBER role should not have update permissions in fallback scenario
- Fix EventTypeGroupFilter test: add missing canRead property to mock permissions map
- Replace 'as any' type assertions with proper 'as unknown as' type assertions to resolve ESLint warnings

Both tests now pass and align with PBAC permission model where MEMBER role has read-only access to event types.

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* add pbac to _heavy router

* Merge main

* restore versions

* Parallelizable members query

* fix nits

* fix create

* Restore lock file

* Fix import from merge artifact

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-17 10:25:30 +01:00
Rajiv Sahal eaa1ad6578 feat: e2e tests for Connect atoms (#23433)
* e2e tests for apple connect atom

* update globalEnv for turbo.json

* update pages for tests

* update playwright config so we can use env variables inside tests

* update packages

* update variable names

* update playwright config

* fix: import variables from env instead of separate files

* update env variables for atoms e2e yml
2025-09-17 11:20:38 +02:00
Rajiv Sahal f9095d298d update env variables (#23883) 2025-09-17 08:21:39 +00:00
HichemandKartik Saini fd87954048 feat: show refresh and back button is app mode (#23838)
* Change display mode from 'standalone' to 'minimal-ui'

this shows the refresh and back button for easier navigation ux

* Update display settings in site.webmanifest

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-09-17 12:53:35 +05:30
Morgan be249a94de chore: add auth strategy test only x-cal-client-id (#23872) 2025-09-17 09:05:10 +02:00
59582cf8f5 fix: unable to add or edit user from admin page and add tests (#23871)
* fix

* Update UserForm.tsx

* test: add e2e tests for admin user creation and editing

- Add test for successful user creation via /settings/admin/users/add
- Add test for successful user editing via /settings/admin/users/{id}
- Tests verify form submission, API responses, and database changes
- Follow Cal.com e2e testing patterns with admin authentication
- Use Playwright for end-to-end form interaction testing

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* Update admin-users.e2e.ts

* Update admin-users.e2e.ts

* update

* tweak

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-16 23:47:56 +05:30
Udit Takkar 5c70060992 feat: voice and language support in cal.ai (#23865)
* feat: lang support

* fix: type errors

* feat: select voice agent

* refactor: address feedback

* refactor: address feedback

* refactor: missing import

* fix: types
2025-09-16 20:15:11 +05:30
Lauris Skraucis ae7f8f1653 feat: v2 show hidden attendees for authenticated requests (#23868)
* refactor: return attendees when fetching all bookings

* refactor: v2 show hidden attendees for authenticated requests

* chore: regenerate docs

* chore: add test
2025-09-16 19:57:42 +05:30
Eunjae LeeandDevin AI 4d0458b0e8 feat: add client-side auth protection to routing forms pages (#23743)
* feat: add client-side auth protection to routing forms pages

- Create RoutingFormsShell component with useRedirectToLoginIfUnauthenticated hook
- Wrap form-edit, route-builder, and incomplete-booking pages with RoutingFormsShell
- Ensures consistent auth behavior during client-side navigation
- Maintains existing server-side auth protection
- Fix ESLint warnings: replace 'any' types with 'unknown' types and add eslint-disable for restricted imports

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

* refactor: clean up RoutingFormsShell implementation

- Remove unused isPublic parameter from RoutingFormsShell component
- Keep only essential client-side authentication protection functionality
- Fix remaining ESLint warnings to allow commit to pass

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

* remove unnecessary hook

* use shell at layout

* rename component

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-16 10:39:42 -03:00
55e6682832 feat: Custom Fields OR Add-Ons with a fee associated (#19997)
* feat: Addons to bookings

* Add tests

* Add tests

* Fix price field not shown immediately after enabling payment for an event

* Add support for all currencies in addons and fix type errors

* Address review comments

* Add tests to cover all input types

* Address cubic-dev-ai comments

* Remove openapi.json from git tracking

* Revert changes

* Revert unrelated linter/formatter changes

* Address cubic-dev-ai comments

* Addres cubic-dev-ai comment

* Update to use test id instead of hardcoded label

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

* Address coderabbits review comments

* Fix failing unit test

* some fixes...

* Fix tests

* Fix import

* Move handlePayment test file to appropriate location

* Fix test

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
2025-09-16 21:33:23 +09:00
Morgan 65448c017f chore: use local platform libraries instead of npm package (#23834)
* chore: use local platform libraries instead of npm package

* chore: use local platform libraries instead of npm package

* chore: use local platform libraries instead of npm package

* chore: use local platform libraries instead of npm package

* chore: use local platform libraries instead of npm package
2025-09-16 15:00:51 +03:00
Anik Dhabal Babu e9fd23d0d3 fix: unable to add users from the admin page (#23869) 2025-09-16 10:59:21 +00:00
Hariom Balhara badc857bcd Move modules to moduleLoader format (#23740) 2025-09-16 10:58:23 +00:00
Eunjae Lee 7fe80dd3dd Revert "feat: implement generalized navigation permission system with PBAC (#23706)" (#23867)
This reverts commit 34b62f7516.
2025-09-16 11:17:08 +01:00
Udit Takkar 5feca13cfb fix: phone number cancel billing (#23854)
* fix: phone number billing bug

* fix: phone number billing bug

* fix: use raw code

* fix: use zod

* chore: update test

* fix: double cancel

* fix: double cancel

* fix: double cancel

* fix: chore update

* fix: schema
2025-09-16 15:39:32 +05:30
Hariom BalharaandDevin AI f267cd9cd9 fix: change no-restricted-imports to @typescript-eslint/no-restricted-imports for allowTypeImports support (#23860)
The allowTypeImports option is only supported by @typescript-eslint/no-restricted-imports,
not the standard ESLint no-restricted-imports rule. This fixes the configuration error:
'Configuration for rule no-restricted-imports is invalid: allowTypeImports should NOT have additional properties'

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-16 10:00:53 +00:00
Morgan e8c2d4fa95 chore: add build platform libs in api v2 github actions (#23862) 2025-09-16 09:07:47 +00:00
Morgan 7e415b4542 fix: eslint no-restricted-imports app-store rule (#23845) 2025-09-15 22:40:01 +05:30
445 changed files with 14482 additions and 3391 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@calcom/platform-libraries"],
"ignore": ["@calcom/platform-libraries", "@calcom/api-v2"],
"privatePackages": {
"version": false,
"tag": false
+82 -7
View File
@@ -1,4 +1,3 @@
// This configuration only applies to the package manager root.
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: ["./packages/config/eslint-preset.js"],
@@ -7,29 +6,105 @@ module.exports = {
"import/no-cycle": ["warn", { maxDepth: Infinity }],
},
overrides: [
// WARN: features must not be imported by app-store or lib
{
files: ["packages/lib/**/*.{ts,tsx,js,jsx}", "packages/prisma/**/*.{ts,tsx,js,jsx}"],
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"warn",
{
paths: ["@calcom/app-store"],
patterns: ["@calcom/app-store/*"],
patterns: [
{
group: [
// Catch all relative paths into features
"**/features",
"**/features/*",
// Catch all alias imports
"@calcom/features",
"@calcom/features/*",
],
message: "Avoid importing @calcom/features from @calcom/app-store or @calcom/lib.",
},
],
},
],
},
},
// WARN: lib must not import app-store or features
{
files: ["packages/lib/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"warn",
{
patterns: [
{
group: [
// Catch all relative paths into app-store
"**/app-store",
"**/app-store/*",
// Catch all relative paths into features
"**/features",
"**/features/*",
// Catch alias imports
"@calcom/app-store",
"@calcom/app-store/*",
"@calcom/features",
"@calcom/features/*",
],
message: "@calcom/lib should not import @calcom/app-store or @calcom/features.",
},
],
},
],
},
},
// ERROR: app-store must not import trpc
{
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"error",
{
allowTypeImports: false,
patterns: [
{
group: ["@calcom/trpc/*", "@trpc/*"],
message: "tRPC imports are blocked in packages/app-store. Move UI to apps/web/components/apps or introduce an API boundary.",
group: [
// Catch all relative paths into trpc
"**/trpc",
"**/trpc/*",
// Catch alias imports
"@calcom/trpc",
"@calcom/trpc/*",
"@trpc",
"@trpc/*",
],
message:
"@calcom/app-store must not import trpc. Move UI to apps/web/components/apps or introduce an API boundary.",
},
],
},
],
},
},
// ERROR: prisma must not import `features` package
{
files: ["packages/prisma/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: [
// Catch all relative paths into features
"**/features",
"**/features/*",
// Catch all alias imports
"@calcom/features",
"@calcom/features/*",
],
message: "Avoid importing @calcom/features from @calcom/prisma.",
},
],
},
+2 -2
View File
@@ -71,9 +71,9 @@ jobs:
- name: Run Tests
working-directory: apps/api/v2
run: |
yarn test:e2e
yarn workspace @calcom/platform-libraries build && yarn test:e2e
EXIT_CODE=$?
echo "yarn test:e2e command exit code: $EXIT_CODE"
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
exit $EXIT_CODE
- name: Upload Test Results
if: ${{ always() }}
+2
View File
@@ -13,6 +13,8 @@ env:
ATOMS_E2E_API_URL: ${{ secrets.ATOMS_E2E_API_URL }}
ATOMS_E2E_ORG_ID: ${{ secrets.ATOMS_E2E_ORG_ID }}
ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED }}
ATOMS_E2E_APPLE_ID: ${{ secrets.ATOMS_E2E_APPLE_ID }}
ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE: ${{ secrets.ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
+1 -1
View File
@@ -21,4 +21,4 @@ jobs:
working-directory: apps/api/v2
run: |
export NODE_OPTIONS="--max_old_space_size=8192"
yarn test
yarn workspace @calcom/platform-libraries build && yarn test
+2
View File
@@ -0,0 +1,2 @@
undecided:
- calcom-monorepo
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+2 -8
View File
@@ -1,20 +1,14 @@
import { captureException as SentryCaptureException } from "@sentry/nextjs";
import type { NextMiddleware } from "next-api-middleware";
import { redactError } from "@calcom/lib/redactError";
export const captureErrors: NextMiddleware = async (_req, res, next) => {
try {
// Catch any errors that are thrown in remaining
// middleware and the API route handler
await next();
} catch (error) {
console.error(error);
SentryCaptureException(error);
const redactedError = redactError(error);
if (redactedError instanceof Error) {
res.status(400).json({ message: redactedError.message, error: redactedError });
return;
}
res.status(400).json({ message: "Something went wrong", error });
res.status(500).json({ message: "Something went wrong" });
}
};
@@ -30,7 +30,7 @@ describe("rateLimitApiKey middleware", () => {
query: {},
userId: testUserId,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res._getStatusCode()).toBe(401);
@@ -43,7 +43,7 @@ describe("rateLimitApiKey middleware", () => {
query: { apiKey: "test-key" },
userId: testUserId,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
limit: 100,
remaining: 99,
@@ -51,6 +51,7 @@ describe("rateLimitApiKey middleware", () => {
});
// @ts-expect-error weird typing between middleware and createMocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await rateLimitApiKey(req, res, vi.fn() as any);
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
@@ -74,15 +75,14 @@ describe("rateLimitApiKey middleware", () => {
success: true,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
}
);
// @ts-expect-error weird typing between middleware and createMocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
@@ -96,10 +96,11 @@ describe("rateLimitApiKey middleware", () => {
query: { apiKey: "test-key" },
userId: testUserId,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
// @ts-expect-error weird typing between middleware and createMocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res._getStatusCode()).toBe(429);
@@ -121,6 +122,7 @@ describe("rateLimitApiKey middleware", () => {
};
// Mock rate limiter to trigger the onRateLimiterResponse callback
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
@@ -131,6 +133,7 @@ describe("rateLimitApiKey middleware", () => {
vi.mocked(handleAutoLock).mockResolvedValueOnce(true);
// @ts-expect-error weird typing between middleware and createMocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await rateLimitApiKey(req, res, vi.fn() as any);
expect(handleAutoLock).toHaveBeenCalledWith({
@@ -158,6 +161,7 @@ describe("rateLimitApiKey middleware", () => {
};
// Mock rate limiter to trigger the onRateLimiterResponse callback
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
@@ -168,6 +172,7 @@ describe("rateLimitApiKey middleware", () => {
vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key."));
// @ts-expect-error weird typing between middleware and createMocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await rateLimitApiKey(req, res, vi.fn() as any);
expect(handleAutoLock).toHaveBeenCalledWith({
@@ -195,6 +200,7 @@ describe("rateLimitApiKey middleware", () => {
};
// Mock rate limiter to trigger the onRateLimiterResponse callback
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(checkRateLimitAndThrowError as any).mockImplementationOnce(
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
onRateLimiterResponse(rateLimiterResponse);
@@ -235,6 +241,7 @@ describe("rateLimitApiKey middleware", () => {
);
// @ts-expect-error weird typing between middleware and createMocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await rateLimitApiKey(req, res, vi.fn() as any);
expect(res._getStatusCode()).toBe(429);
+68
View File
@@ -0,0 +1,68 @@
import type { Prisma } from "@calcom/prisma/client";
export const eventTypeSelect = {
id: true,
title: true,
slug: true,
length: true,
hidden: true,
position: true,
userId: true,
teamId: true,
scheduleId: true,
eventName: true,
timeZone: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
requiresConfirmation: true,
recurringEvent: true,
disableGuests: true,
hideCalendarNotes: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
schedulingType: true,
price: true,
currency: true,
slotInterval: true,
parentId: true,
successRedirectUrl: true,
description: true,
locations: true,
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
bookingLimits: true,
onlyShowFirstAvailableSlot: true,
durationLimits: true,
customInputs: {
select: { id: true, label: true, required: true, options: true, type: true, placeholder: true },
},
hashedLink: {
select: { link: true },
},
team: {
select: { slug: true },
},
hosts: {
select: {
userId: true,
isFixed: true,
scheduleId: true,
},
},
owner: {
select: { username: true, id: true },
},
children: {
select: {
id: true,
userId: true,
},
},
} satisfies Prisma.EventTypeSelect;
+1 -66
View File
@@ -6,13 +6,12 @@ import {
MIN_EVENT_DURATION_MINUTES,
} from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema";
import { HostSchema } from "@calcom/prisma/zod/modelSchema/HostSchema";
import { Frequency } from "~/lib/types";
import { jsonSchema } from "./shared/jsonSchema";
import { schemaQueryUserId } from "./shared/queryUserId";
import { timeZone } from "./shared/timeZone";
@@ -118,67 +117,3 @@ const schemaEventTypeEditParams = z
.strict();
export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams);
export const schemaEventTypeReadPublic = EventTypeSchema.pick({
id: true,
title: true,
slug: true,
length: true,
hidden: true,
position: true,
userId: true,
teamId: true,
scheduleId: true,
eventName: true,
timeZone: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
requiresConfirmation: true,
recurringEvent: true,
disableGuests: true,
hideCalendarNotes: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
schedulingType: true,
price: true,
currency: true,
slotInterval: true,
parentId: true,
successRedirectUrl: true,
description: true,
locations: true,
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
bookingLimits: true,
onlyShowFirstAvailableSlot: true,
durationLimits: true,
}).merge(
z.object({
children: z.array(childrenSchema).optional().default([]),
hosts: z.array(hostSchema).optional().default([]),
locations: z
.array(
z.object({
link: z.string().optional(),
address: z.string().optional(),
hostPhoneNumber: z.string().optional(),
type: z.any().optional(),
})
)
.nullable(),
metadata: jsonSchema.nullable(),
customInputs: customInputSchema.array().optional(),
link: z.string().optional(),
hashedLink: z
.array(z.object({ link: z.string() }))
.optional()
.default([]),
bookingFields: eventTypeBookingFields.optional().nullable(),
})
);
@@ -102,8 +102,11 @@ export async function getHandler(req: NextApiRequest) {
const booking = await prisma.booking.findUnique({
where: { id },
include: {
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
attendees: true,
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
user: true,
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
payment: true,
eventType: expand.includes("team") ? { include: { team: true } } : false,
},
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/lib/videoClient";
} from "@calcom/app-store/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -63,6 +63,7 @@ export async function getHandler(req: NextApiRequest) {
const booking = await prisma.booking.findUnique({
where: { id },
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { references: true },
});
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/lib/videoClient";
} from "@calcom/app-store/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -61,6 +61,7 @@ export async function getHandler(req: NextApiRequest) {
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
const booking = await prisma.booking.findUnique({
where: { id: bookingId },
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { references: true },
});
@@ -1,8 +1,8 @@
import type { NextApiRequest } from "next";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
import prisma from "@calcom/prisma";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -44,6 +44,7 @@ export async function getHandler(req: NextApiRequest) {
const booking = await prisma.booking.findUnique({
where: { id },
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { references: true },
});
@@ -1,7 +1,10 @@
import type { NextApiRequest } from "next";
import type { z } from "zod";
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
import {
getCalendarCredentialsWithoutDelegation,
getConnectedCalendars,
} from "@calcom/features/calendars/lib/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
@@ -1,6 +1,9 @@
import type { NextApiRequest } from "next";
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
import {
getCalendarCredentialsWithoutDelegation,
getConnectedCalendars,
} from "@calcom/features/calendars/lib/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
@@ -1,8 +1,7 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
@@ -66,4 +65,4 @@ async function checkPermissions(req: NextApiRequest) {
throw new HttpError({ statusCode: 403, message: "Forbidden" });
}
export default defaultResponder(deleteHandler);
export default deleteHandler;
+15 -15
View File
@@ -1,11 +1,10 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { eventTypeSelect } from "~/lib/selects/event-type";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware";
@@ -49,15 +48,13 @@ export async function getHandler(req: NextApiRequest) {
const eventType = await prisma.eventType.findUnique({
where: { id },
include: {
customInputs: true,
hashedLink: { select: { link: true } },
team: { select: { slug: true } },
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
select: eventTypeSelect,
});
if (!eventType) {
throw new HttpError({ statusCode: 404, message: "Event type not found" });
}
await checkPermissions(req, eventType);
const link = eventType ? getCalLink(eventType) : null;
@@ -74,9 +71,12 @@ export async function getHandler(req: NextApiRequest) {
eventType.scheduleId = user.defaultScheduleId;
}
// TODO: eventType when not found should be a 404
// but API consumers may depend on the {} behaviour.
return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) };
return {
event_type: {
...eventType,
link,
},
};
}
type BaseEventTypeCheckPermissions = {
@@ -100,4 +100,4 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
throw new HttpError({ statusCode: 403, message: "Forbidden" });
}
export default defaultResponder(getHandler);
export default getHandler;
@@ -2,13 +2,13 @@ import type { NextApiRequest } from "next";
import type { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import { eventTypeSelect } from "~/lib/selects/event-type";
import type { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type";
import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { schemaEventTypeEditBodyParams } from "~/lib/validations/event-type";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
import ensureOnlyMembersAsHosts from "~/pages/api/event-types/_utils/ensureOnlyMembersAsHosts";
@@ -235,8 +235,8 @@ export async function patchHandler(req: NextApiRequest) {
};
}
await checkPermissions(req, parsedBody);
const eventType = await prisma.eventType.update({ where: { id }, data });
return { event_type: schemaEventTypeReadPublic.parse(eventType) };
const eventType = await prisma.eventType.update({ where: { id }, data, select: eventTypeSelect });
return { event_type: eventType };
}
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
@@ -249,4 +249,4 @@ async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schema
await checkTeamEventEditPermission(req, body);
}
export default defaultResponder(patchHandler);
export default patchHandler;
+4 -11
View File
@@ -2,10 +2,10 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { eventTypeSelect } from "~/lib/selects/event-type";
import { schemaQuerySlug } from "~/lib/validations/shared/querySlug";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
@@ -54,14 +54,7 @@ async function getHandler(req: NextApiRequest) {
userId: shouldUseUserId ? { in: userIds } : undefined,
slug: slug, // slug will be undefined if not provided in query
},
include: {
customInputs: true,
hashedLink: { select: { link: true } },
team: { select: { slug: true } },
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
select: eventTypeSelect,
});
// this really should return [], but backwards compatibility..
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
@@ -69,7 +62,7 @@ async function getHandler(req: NextApiRequest) {
event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map(
(eventType) => {
const link = getCalLink(eventType);
return schemaEventTypeReadPublic.parse({ ...eventType, link });
return { ...eventType, link };
}
),
};
+5 -4
View File
@@ -2,11 +2,12 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { eventTypeSelect } from "~/lib/selects/event-type";
import { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type";
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
@@ -306,10 +307,10 @@ async function postHandler(req: NextApiRequest) {
data.hosts = { createMany: { data: hosts } };
}
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
const eventType = await prisma.eventType.create({ data, select: eventTypeSelect });
return {
event_type: schemaEventTypeReadPublic.parse(eventType),
event_type: eventType,
message: "Event type created successfully",
};
}
+1
View File
@@ -46,6 +46,7 @@ export async function paymentById(
if (safeQuery.success && method === "GET") {
const userWithBookings = await prisma.user.findUnique({
where: { id: userId },
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { bookings: true },
});
await prisma.payment
+5 -1
View File
@@ -75,7 +75,11 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const data = await prisma.schedule.findUniqueOrThrow({ where: { id }, include: { availability: true } });
const data = await prisma.schedule.findUniqueOrThrow({
where: { id },
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { availability: true },
});
return { schedule: schemaSchedulePublic.parse(data) };
}
@@ -62,6 +62,7 @@ export async function patchHandler(req: NextApiRequest) {
/** Only OWNERS and ADMINS can edit teams */
const _team = await prisma.team.findFirst({
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
include: { members: true },
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
});
@@ -2,10 +2,10 @@ import type { NextApiRequest } from "next";
import { z } from "zod";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { eventTypeSelect } from "~/lib/selects/event-type";
const querySchema = z.object({
teamId: z.coerce.number(),
@@ -57,17 +57,11 @@ async function getHandler(req: NextApiRequest) {
members: { some: { userId } },
},
},
include: {
customInputs: true,
team: { select: { slug: true } },
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
select: eventTypeSelect,
};
return {
event_types: await prisma.eventType.findMany(args),
};
const data = await prisma.eventType.findMany(args);
return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) };
}
export default defaultResponder(getHandler);
@@ -9,7 +9,7 @@ import { buildBooking } from "@calcom/lib/test/builder";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/lib/videoClient";
} from "@calcom/app-store/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
const adminUserId = 1;
const memberUserId = 10;
vi.mock("@calcom/lib/videoClient", () => {
vi.mock("@calcom/app-store/videoClient", () => {
return {
getRecordingsOfCalVideoByRoomName: vi.fn(),
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/lib/videoClient";
} from "@calcom/app-store/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -19,7 +19,7 @@ import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[reco
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/lib/videoClient", () => {
vi.mock("@calcom/app-store/videoClient", () => {
return {
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
checkIfRoomNameMatchesInRecording: vi.fn(),
@@ -6,7 +6,7 @@ import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -16,7 +16,7 @@ import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/lib/videoClient", () => {
vi.mock("@calcom/app-store/videoClient", () => {
return {
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
};
+1 -1
View File
@@ -86,7 +86,7 @@ vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
}),
}));
vi.mock("@calcom/lib/EventManager", () => ({
vi.mock("@calcom/features/bookings/lib/EventManager", () => ({
default: vi.fn().mockImplementation(() => ({
reschedule: vi.fn().mockResolvedValue({
results: [],
@@ -60,7 +60,7 @@ describe("DELETE /api/event-types/[id]", () => {
describe("Error", async () => {
test("Fails to remove event type if user is not OWNER/ADMIN of team associated with event type", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "DELETE",
body: {},
query: {
@@ -71,15 +71,18 @@ describe("DELETE /api/event-types/[id]", () => {
// Assign userId to the request objects
req.userId = memberUser;
await handler(req, res);
expect(res.statusCode).toBe(403); // Check if the deletion was successful
await expect(handler(req)).rejects.toThrowError(
expect.objectContaining({
statusCode: 403,
})
);
});
});
describe("Success", async () => {
test("Removes event type if user is owner of team associated with event type", async () => {
// Mocks for DELETE request
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "DELETE",
body: {},
query: {
@@ -90,8 +93,8 @@ describe("DELETE /api/event-types/[id]", () => {
// Assign userId to the request objects
req.userId = adminUser;
await handler(req, res);
expect(res.statusCode).toBe(200); // Check if the deletion was successful
const deletedEventType = await handler(req);
expect(deletedEventType).not.toBeNull();
});
});
});
@@ -16,7 +16,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
describe("GET /api/event-types/[id]", () => {
describe("Errors", () => {
test("Returns 403 if user not admin/team member/event owner", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
@@ -32,16 +32,40 @@ describe("GET /api/event-types/[id]", () => {
);
req.userId = 333333;
await handler(req, res);
expect(res.statusCode).toBe(403);
await expect(handler(req)).rejects.toThrowError(
expect.objectContaining({
statusCode: 403,
})
);
});
test("Returns 404 if event type not found", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
id: 123456,
},
});
req.isSystemWideAdmin = true;
prismaMock.eventType.findUnique.mockResolvedValue(null);
req.userId = 333333;
await expect(handler(req)).rejects.toThrowError(
expect.objectContaining({
statusCode: 404,
})
);
});
});
describe("Success", async () => {
test("Returns event type if user is admin", async () => {
const eventTypeId = 123456;
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
@@ -57,17 +81,17 @@ describe("GET /api/event-types/[id]", () => {
req.isSystemWideAdmin = true;
req.userId = 333333;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
const responseData = await handler(req);
expect(responseData.event_type.id).toEqual(eventTypeId);
});
test("Returns event type if user is in team associated with event type", async () => {
const eventTypeId = 123456;
const teamId = 9999;
const userId = 333333;
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
@@ -94,10 +118,10 @@ describe("GET /api/event-types/[id]", () => {
req.isSystemWideAdmin = false;
req.userId = userId;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
const responseData = await handler(req);
expect(responseData.event_type.id).toEqual(eventTypeId);
expect(prismaMock.team.findFirst).toHaveBeenCalledWith({
where: {
id: teamId,
@@ -116,7 +140,7 @@ describe("GET /api/event-types/[id]", () => {
test("Returns event type if user is the event type owner", async () => {
const eventTypeId = 123456;
const userId = 333333;
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
body: {},
query: {
@@ -134,10 +158,10 @@ describe("GET /api/event-types/[id]", () => {
req.isSystemWideAdmin = false;
req.userId = userId;
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
const responseData = await handler(req);
expect(responseData.event_type.id).toEqual(eventTypeId);
expect(prismaMock.team.findFirst).not.toHaveBeenCalled();
});
});
+2
View File
@@ -22,6 +22,8 @@ RUN yarn workspace @calcom/api-v2 run generate-schemas
RUN rm -rf apps/api/v2/node_modules
RUN yarn install
RUN yarn workspace @calcom/platform-libraries run build
RUN yarn workspace @calcom/api-v2 run build
EXPOSE 80
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.359",
"@calcom/platform-libraries": "*",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -11,7 +11,12 @@ import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/se
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import {
API_KEY_OR_ACCESS_TOKEN_HEADER,
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import {
AuthOptionalUser,
@@ -22,8 +27,8 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
Post,
@@ -101,6 +106,9 @@ export class BookingsController_2024_08_13 {
@Post("/")
@UseGuards(OptionalApiAuthGuard)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Create a booking",
description: `
@@ -122,6 +130,9 @@ export class BookingsController_2024_08_13 {
And 2 ways to book and event type belonging to a team:
1. Provide \`eventTypeId\` in the request body.
2. Provide \`eventTypeSlug\` and \`teamSlug\` and optionally \`organizationSlug\` if the team with the teamSlug is within an organization.
If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
`,
})
@ApiBody({
@@ -161,7 +172,10 @@ export class BookingsController_2024_08_13 {
}
@Get("/:bookingUid")
@UseGuards(BookingUidGuard)
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Get a booking",
description: `\`:bookingUid\` can be
@@ -170,10 +184,17 @@ export class BookingsController_2024_08_13 {
2. uid of one of the recurring booking recurrences
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).`,
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
`,
})
async getBooking(@Param("bookingUid") bookingUid: string): Promise<GetBookingOutput_2024_08_13> {
const booking = await this.bookingsService.getBooking(bookingUid);
async getBooking(
@Param("bookingUid") bookingUid: string,
@GetOptionalUser() user: AuthOptionalUser
): Promise<GetBookingOutput_2024_08_13> {
const booking = await this.bookingsService.getBooking(bookingUid, user);
return {
status: SUCCESS_STATUS,
@@ -218,7 +239,7 @@ export class BookingsController_2024_08_13 {
@ApiOperation({ summary: "Get all bookings" })
async getBookings(
@Query() queryParams: GetBookingsInput_2024_08_13,
@GetUser() user: UserWithProfile
@GetUser() user: ApiAuthGuardUser
): Promise<GetBookingsOutput_2024_08_13> {
const profile = this.usersService.getUserMainProfile(user);
@@ -236,7 +257,10 @@ export class BookingsController_2024_08_13 {
}
@Post("/:bookingUid/reschedule")
@UseGuards(BookingUidGuard)
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({
summary: "Reschedule a booking",
description: "Reschedule a booking or seated booking",
@@ -248,17 +272,20 @@ export class BookingsController_2024_08_13 {
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
],
},
description:
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2).
If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.`,
})
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
async rescheduleBooking(
@Param("bookingUid") bookingUid: string,
@Body(new RescheduleBookingInputPipe())
body: RescheduleBookingInput,
@Req() request: Request
@Req() request: Request,
@GetOptionalUser() user: AuthOptionalUser
): Promise<RescheduleBookingOutput_2024_08_13> {
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body);
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body, user);
await this.bookingsService.billRescheduledBooking(newBooking, bookingUid);
return {
@@ -268,7 +295,10 @@ export class BookingsController_2024_08_13 {
}
@Post("/:bookingUid/cancel")
@UseGuards(BookingUidGuard)
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: "Cancel a booking",
@@ -281,7 +311,11 @@ export class BookingsController_2024_08_13 {
\nCancelling recurring seated bookings:
For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
`,
})
@ApiBody({
schema: {
@@ -298,9 +332,10 @@ export class BookingsController_2024_08_13 {
@Req() request: Request,
@Param("bookingUid") bookingUid: string,
@Body(new CancelBookingInputPipe())
body: CancelBookingInput
body: CancelBookingInput,
@GetOptionalUser() user: AuthOptionalUser
): Promise<CancelBookingOutput_2024_08_13> {
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body);
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body, user);
return {
status: SUCCESS_STATUS,
@@ -342,7 +377,7 @@ export class BookingsController_2024_08_13 {
})
async reassignBooking(
@Param("bookingUid") bookingUid: string,
@GetUser() user: UserWithProfile
@GetUser() user: ApiAuthGuardUser
): Promise<ReassignBookingOutput_2024_08_13> {
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
@@ -392,7 +427,7 @@ export class BookingsController_2024_08_13 {
})
async confirmBooking(
@Param("bookingUid") bookingUid: string,
@GetUser() user: UserWithProfile
@GetUser() user: ApiAuthGuardUser
): Promise<GetBookingOutput_2024_08_13> {
const booking = await this.bookingsService.confirmBooking(bookingUid, user);
@@ -414,7 +449,7 @@ export class BookingsController_2024_08_13 {
async declineBooking(
@Param("bookingUid") bookingUid: string,
@Body() body: DeclineBookingInput_2024_08_13,
@GetUser() user: UserWithProfile
@GetUser() user: ApiAuthGuardUser
): Promise<GetBookingOutput_2024_08_13> {
const booking = await this.bookingsService.declineBooking(bookingUid, user, body.reason);
@@ -437,61 +437,209 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should book an event type with attendees disabled", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
attendee: {
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: "Europe/Rome",
language: "it",
},
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "100",
},
};
describe("book an event type with attendees disabled but auth provided", () => {
let booking: CreateSeatedBookingOutput_2024_08_13;
it("should book an event type with attendees disabled and no auth provided", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
attendee: {
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: "Europe/Rome",
language: "it",
},
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.seatUid).toBeDefined();
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
expect(data.eventType).toEqual({
id: seatedEventTypeIdAttendeesDisabledId,
slug: seatedEventSlugAttendeesDisabled,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected seated booking but received non array response"
);
}
});
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.seatUid).toBeDefined();
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
expect(data.eventType).toEqual({
id: seatedEventTypeIdAttendeesDisabledId,
slug: seatedEventSlugAttendeesDisabled,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
booking = data;
} else {
throw new Error(
"Invalid response data - expected seated booking but received non array response"
);
}
});
});
it("should fetch booking and not have attendees because no auth provided", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${booking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsGetSeatedBooking(responseBody.data)) {
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts.length).toEqual(1);
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(booking.start);
expect(data.end).toEqual(booking.end);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
expect(data.eventType).toEqual({
id: seatedEventTypeIdAttendeesDisabledId,
slug: seatedEventSlugAttendeesDisabled,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
await bookingsRepositoryFixture.deleteById(data.id);
} else {
throw new Error("Invalid response data - expected booking but received array response");
}
});
});
});
describe("book an event type with attendees disabled but auth provided", () => {
let booking: CreateSeatedBookingOutput_2024_08_13;
it("should book an event type with attendees disabled and owner auth provided", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
attendee: {
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: "Europe/Rome",
language: "it",
},
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", apiKeyString)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.seatUid).toBeDefined();
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
expect(data.eventType).toEqual({
id: seatedEventTypeIdAttendeesDisabledId,
slug: seatedEventSlugAttendeesDisabled,
});
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0].email).toEqual(emailAttendeeOne);
expect(data.attendees[0].name).toEqual(nameAttendeeOne);
expect(data.attendees[0].language).toEqual("it");
expect(data.attendees[0].seatUid).toBeDefined();
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
booking = data;
} else {
throw new Error(
"Invalid response data - expected seated booking but received non array response"
);
}
});
});
it("should fetch booking and have attendees", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${booking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", apiKeyString)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsGetSeatedBooking(responseBody.data)) {
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts.length).toEqual(1);
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(booking.start);
expect(data.end).toEqual(booking.end);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
expect(data.eventType).toEqual({
id: seatedEventTypeIdAttendeesDisabledId,
slug: seatedEventSlugAttendeesDisabled,
});
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0].email).toEqual(emailAttendeeOne);
expect(data.attendees[0].name).toEqual(nameAttendeeOne);
expect(data.attendees[0].language).toEqual("it");
expect(data.attendees[0].seatUid).toBeDefined();
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
await bookingsRepositoryFixture.deleteById(data.id);
} else {
throw new Error("Invalid response data - expected booking but received array response");
}
});
});
});
describe("cancel seated booking", () => {
@@ -7,6 +7,7 @@ import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { getPagination } from "@/lib/pagination/pagination";
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
@@ -120,7 +121,10 @@ export class BookingsService_2024_08_13 {
if (!eventType) {
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
}
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
const userIsEventTypeAdminOrOwner = authUser
? await this.userIsEventTypeAdminOrOwner(authUser, eventType)
: false;
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser, userIsEventTypeAdminOrOwner);
if (eventType.schedulingType === "MANAGED") {
throw new BadRequestException(
@@ -140,13 +144,13 @@ export class BookingsService_2024_08_13 {
await this.hasRequiredBookingFieldsResponses(body, eventType);
if (isRecurring && isSeated) {
return await this.createRecurringSeatedBooking(request, body, eventType);
return await this.createRecurringSeatedBooking(request, body, eventType, userIsEventTypeAdminOrOwner);
}
if (isRecurring && !isSeated) {
return await this.createRecurringBooking(request, body, eventType);
}
if (isSeated) {
return await this.createSeatedBooking(request, body, eventType);
return await this.createSeatedBooking(request, body, eventType, userIsEventTypeAdminOrOwner);
}
return await this.createRegularBooking(request, body, eventType);
@@ -157,7 +161,8 @@ export class BookingsService_2024_08_13 {
async checkBookingRequiresAuthenticationSetting(
eventType: EventTypeWithOwnerAndTeam,
authUser: AuthOptionalUser
authUser: AuthOptionalUser,
userIsEventTypeAdminOrOwner: boolean
) {
if (!eventType.bookingRequiresAuthentication) return true;
if (!authUser) {
@@ -166,15 +171,23 @@ export class BookingsService_2024_08_13 {
);
}
if (!userIsEventTypeAdminOrOwner) {
throw new ForbiddenException(
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
);
}
}
async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType) {
const authUserId = authUser.id;
const authUserRole = authUser.role;
const eventTypeId = eventType.id;
const teamId = eventType.teamId;
const bookingUserId = eventType.owner?.id || null;
const eventTypeOwnerId = eventType.userId || null;
if (authUserRole === "ADMIN") return;
if (authUserRole === "ADMIN") return true;
if (bookingUserId === authUserId) return;
if (eventTypeOwnerId === authUserId) return true;
if (eventTypeId) {
const [isUserHost, isUserAssigned] = await Promise.all([
@@ -182,7 +195,7 @@ export class BookingsService_2024_08_13 {
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
]);
if (isUserHost || isUserAssigned) return;
if (isUserHost || isUserAssigned) return true;
}
if (teamId) {
@@ -190,19 +203,17 @@ export class BookingsService_2024_08_13 {
authUserId,
teamId
);
if (membership) return;
if (membership) return true;
}
if (
bookingUserId &&
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, bookingUserId))
eventTypeOwnerId &&
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId))
) {
return;
return true;
}
throw new ForbiddenException(
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
);
return false;
}
async getBookedEventType(body: CreateBookingInput) {
@@ -476,7 +487,8 @@ export class BookingsService_2024_08_13 {
async createRecurringSeatedBooking(
request: Request,
body: CreateRecurringBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
eventType: EventTypeWithOwnerAndTeam,
userIsEventTypeAdminOrOwner: boolean
) {
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
const bookings = await handleNewRecurringBooking({
@@ -491,7 +503,8 @@ export class BookingsService_2024_08_13 {
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
});
return this.outputService.getOutputCreateRecurringSeatedBookings(
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" })),
userIsEventTypeAdminOrOwner
);
}
@@ -528,7 +541,8 @@ export class BookingsService_2024_08_13 {
async createSeatedBooking(
request: Request,
body: CreateBookingInput_2024_08_13,
eventType: EventTypeWithOwnerAndTeam
eventType: EventTypeWithOwnerAndTeam,
userIsEventTypeAdminOrOwner: boolean
) {
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
try {
@@ -554,7 +568,11 @@ export class BookingsService_2024_08_13 {
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
}
return this.outputService.getOutputCreateSeatedBooking(databaseBooking, booking.seatReferenceUid || "");
return this.outputService.getOutputCreateSeatedBooking(
databaseBooking,
booking.seatReferenceUid || "",
userIsEventTypeAdminOrOwner
);
} catch (error) {
if (error instanceof Error) {
if (error.message === "booking_seats_full_error") {
@@ -565,8 +583,12 @@ export class BookingsService_2024_08_13 {
}
}
async getBooking(uid: string) {
async getBooking(uid: string, authUser: AuthOptionalUser) {
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
const userIsEventTypeAdminOrOwner =
authUser && booking?.eventType
? await this.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
: false;
if (booking) {
const isRecurring = !!booking.recurringEventId;
@@ -576,13 +598,12 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputRecurringBooking(booking);
}
if (isRecurring && isSeated) {
return this.outputService.getOutputRecurringSeatedBooking(
booking,
!!booking.eventType?.seatsShowAttendees
);
const showAttendees = userIsEventTypeAdminOrOwner || !!booking.eventType?.seatsShowAttendees;
return this.outputService.getOutputRecurringSeatedBooking(booking, showAttendees);
}
if (isSeated) {
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
const showAttendees = userIsEventTypeAdminOrOwner || !!booking.eventType?.seatsShowAttendees;
return this.outputService.getOutputSeatedBooking(booking, showAttendees);
}
return this.outputService.getOutputBooking(booking);
}
@@ -594,10 +615,9 @@ export class BookingsService_2024_08_13 {
const ids = recurringBooking.map((booking) => booking.id);
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
if (isRecurringSeated) {
return this.outputService.getOutputRecurringSeatedBookings(
ids,
!!recurringBooking[0].eventType?.seatsShowAttendees
);
const showAttendees =
userIsEventTypeAdminOrOwner || !!recurringBooking[0].eventType?.seatsShowAttendees;
return this.outputService.getOutputRecurringSeatedBookings(ids, showAttendees);
}
return this.outputService.getOutputRecurringBookings(ids);
@@ -663,19 +683,9 @@ export class BookingsService_2024_08_13 {
if (isRecurring && !isSeated) {
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
} else if (isRecurring && isSeated) {
formattedBookings.push(
this.outputService.getOutputRecurringSeatedBooking(
formatted,
!!formatted.eventType?.seatsShowAttendees
)
);
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted, true));
} else if (isSeated) {
formattedBookings.push(
await this.outputService.getOutputSeatedBooking(
formatted,
!!formatted.eventType?.seatsShowAttendees
)
);
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted, true));
} else {
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
}
@@ -718,7 +728,12 @@ export class BookingsService_2024_08_13 {
return queryParamsAttendeeEmail;
}
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
async rescheduleBooking(
request: Request,
bookingUid: string,
body: RescheduleBookingInput,
authUser: AuthOptionalUser
) {
try {
await this.canRescheduleBooking(bookingUid);
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
@@ -747,6 +762,10 @@ export class BookingsService_2024_08_13 {
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
}
const userIsEventTypeAdminOrOwner =
authUser && databaseBooking.eventType
? await this.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
: false;
const isRecurring = !!databaseBooking.recurringEventId;
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;
@@ -756,13 +775,15 @@ export class BookingsService_2024_08_13 {
if (isRecurring && isSeated) {
return this.outputService.getOutputCreateRecurringSeatedBooking(
databaseBooking,
booking?.seatReferenceUid || ""
booking?.seatReferenceUid || "",
userIsEventTypeAdminOrOwner
);
}
if (isSeated) {
return this.outputService.getOutputCreateSeatedBooking(
databaseBooking,
booking.seatReferenceUid || ""
booking.seatReferenceUid || "",
userIsEventTypeAdminOrOwner
);
}
return this.outputService.getOutputBooking(databaseBooking);
@@ -790,7 +811,12 @@ export class BookingsService_2024_08_13 {
return booking;
}
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
async cancelBooking(
request: Request,
bookingUid: string,
body: CancelBookingInput,
authUser: AuthOptionalUser
) {
if (this.inputService.isCancelSeatedBody(body)) {
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
@@ -821,13 +847,13 @@ export class BookingsService_2024_08_13 {
}
if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) {
return this.getAllRecurringBookingsByIndividualUid(bookingUid);
return this.getAllRecurringBookingsByIndividualUid(bookingUid, authUser);
}
return this.getBooking(bookingUid);
return this.getBooking(bookingUid, authUser);
}
private async getAllRecurringBookingsByIndividualUid(bookingUid: string) {
private async getAllRecurringBookingsByIndividualUid(bookingUid: string, authUser: AuthOptionalUser) {
const booking = await this.bookingsRepository.getByUid(bookingUid);
const recurringBookingUid = booking?.recurringEventId;
if (!recurringBookingUid) {
@@ -836,7 +862,7 @@ export class BookingsService_2024_08_13 {
);
}
return await this.getBooking(recurringBookingUid);
return await this.getBooking(recurringBookingUid, authUser);
}
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
@@ -1003,7 +1029,7 @@ export class BookingsService_2024_08_13 {
}
}
async confirmBooking(bookingUid: string, requestUser: UserWithProfile) {
async confirmBooking(bookingUid: string, requestUser: ApiAuthGuardUser) {
const booking = await this.bookingsRepository.getByUid(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
@@ -1032,10 +1058,10 @@ export class BookingsService_2024_08_13 {
},
});
return this.getBooking(bookingUid);
return this.getBooking(bookingUid, requestUser);
}
async declineBooking(bookingUid: string, requestUser: UserWithProfile, reason?: string) {
async declineBooking(bookingUid: string, requestUser: ApiAuthGuardUser, reason?: string) {
const booking = await this.bookingsRepository.getByUid(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
@@ -1065,7 +1091,7 @@ export class BookingsService_2024_08_13 {
},
});
return this.getBooking(bookingUid);
return this.getBooking(bookingUid, requestUser);
}
async getCalendarLinks(bookingUid: string): Promise<CalendarLink[]> {
@@ -229,7 +229,8 @@ export class OutputBookingsService_2024_08_13 {
status: databaseBooking.status.toLowerCase(),
cancellationReason:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
cancelledByEmail: databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
cancelledByEmail:
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
reschedulingReason: bookingResponses?.rescheduledReason,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
@@ -269,12 +270,11 @@ export class OutputBookingsService_2024_08_13 {
async getOutputCreateSeatedBooking(
databaseBooking: DatabaseBooking,
seatUid: string
seatUid: string,
userIsEventTypeAdminOrOwner: boolean
): Promise<CreateSeatedBookingOutput_2024_08_13> {
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
databaseBooking,
!!databaseBooking.eventType?.seatsShowAttendees
);
const showAttendees = userIsEventTypeAdminOrOwner || !!databaseBooking.eventType?.seatsShowAttendees;
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking, showAttendees);
return { ...getSeatedBookingOutput, seatUid };
}
@@ -377,7 +377,10 @@ export class OutputBookingsService_2024_08_13 {
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
}
async getOutputCreateRecurringSeatedBookings(bookings: { uid: string; seatUid: string }[]) {
async getOutputCreateRecurringSeatedBookings(
bookings: { uid: string; seatUid: string }[],
userIsEventTypeAdminOrOwner: boolean
) {
const transformed = [];
for (const booking of bookings) {
@@ -386,7 +389,13 @@ export class OutputBookingsService_2024_08_13 {
if (!databaseBooking) {
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
}
transformed.push(this.getOutputCreateRecurringSeatedBooking(databaseBooking, booking.seatUid));
transformed.push(
this.getOutputCreateRecurringSeatedBooking(
databaseBooking,
booking.seatUid,
userIsEventTypeAdminOrOwner
)
);
}
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
@@ -394,11 +403,13 @@ export class OutputBookingsService_2024_08_13 {
getOutputCreateRecurringSeatedBooking(
databaseBooking: DatabaseBooking,
seatUid: string
seatUid: string,
userIsEventTypeAdminOrOwner: boolean
): CreateRecurringSeatedBookingOutput_2024_08_13 {
const showAttendees = userIsEventTypeAdminOrOwner || !!databaseBooking.eventType?.seatsShowAttendees;
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
databaseBooking,
!!databaseBooking.eventType?.seatsShowAttendees
showAttendees
);
return { ...getRecurringSeatedBookingOutput, seatUid };
}
@@ -32,7 +32,7 @@ import { randomString } from "test/utils/randomString";
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
import { ApiAuthGuardRequest } from "./api-auth.strategy";
import { ApiAuthGuardRequest, ONLY_CLIENT_ID_PROVIDED_MESSAGE } from "./api-auth.strategy";
import { ApiAuthStrategy } from "./api-auth.strategy";
describe("ApiAuthStrategy", () => {
@@ -244,6 +244,29 @@ describe("ApiAuthStrategy", () => {
}
});
it("should throw 401 if only OAuth ID is provided", async () => {
const context: ExecutionContext = {
switchToHttp: () => ({
getRequest: () => ({
headers: {
[X_CAL_CLIENT_ID]: `${oAuthClient.id}gibberish`,
},
get: (key: string) => ({ origin: "http://localhost:3000" }[key]),
}),
}),
} as ExecutionContext;
const request = context.switchToHttp().getRequest();
try {
await strategy.authenticate(request);
} catch (error) {
if (error instanceof HttpException) {
expect(error.getStatus()).toEqual(401);
expect(error.message).toContain(ONLY_CLIENT_ID_PROVIDED_MESSAGE);
}
}
});
it("should throw 401 if OAuth ID is invalid", async () => {
const context: ExecutionContext = {
switchToHttp: () => ({
@@ -35,7 +35,7 @@ import type {
import type { User, Team } from "@calcom/prisma/client";
describe("Organizations Event Types Endpoints", () => {
describe("User Authentication - User is Org Admin", () => {
describe("User Authentication - User is Org Admin but not team admin", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
File diff suppressed because it is too large Load Diff
@@ -134,7 +134,7 @@ export async function getCRMData(
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
"@calcom/lib/server/getTeamMemberEmailFromCrm"
"@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm"
);
const {
email,
@@ -41,8 +41,8 @@ import type {
import type { zodRoutes } from "@calcom/app-store/routing-forms/zod";
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import type { EventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer";
import { areTheySiblingEntities } from "@calcom/lib/entityPermissionUtils.shared";
import type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { buildEmptyQueryValue, raqbQueryValueUtils } from "@calcom/lib/raqb/raqbUtils";
import type { Prisma } from "@calcom/prisma/client";
@@ -0,0 +1,9 @@
"use client";
import { useRedirectToLoginIfUnauthenticated } from "@calcom/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated";
export function RoutingFormAuthGuard({ children }: { children: React.ReactNode }) {
useRedirectToLoginIfUnauthenticated();
return <>{children}</>;
}
@@ -1,7 +1,22 @@
import type { ReactNode } from "react";
import FormProvider from "./FormProvider";
import { RoutingFormAuthGuard } from "./RoutingFormAuthGuard";
export default async function Layout({ children }: { children: ReactNode }) {
return <FormProvider>{children}</FormProvider>;
interface LayoutProps {
children: ReactNode;
params: { pages: string[] };
}
export default async function Layout({ children, params }: LayoutProps) {
const isPublic = params.pages?.[0] === "routing-link";
if (isPublic) {
return <FormProvider>{children}</FormProvider>;
}
return (
<RoutingFormAuthGuard>
<FormProvider>{children}</FormProvider>
</RoutingFormAuthGuard>
);
}
@@ -8,6 +8,10 @@ import { z } from "zod";
import { EventTypeWebWrapper } from "@calcom/atoms/event-types/wrappers/EventTypeWebWrapper";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -40,6 +44,95 @@ const getCachedEventType = unstable_cache(
{ revalidate: 3600 } // Cache for 1 hour
);
const getEventPermissions = async (userId: number, teamId: number | null) => {
// Personal event - has all perms
if (!teamId)
return {
eventTypes: {
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
},
workflows: {
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
},
};
const membership = await prisma.membership.findFirst({
where: {
userId,
teamId,
},
select: {
role: true,
},
});
if (!membership) throw new Error("Membership not found");
const [eventTypePermissions, workflowPermissions] = await Promise.all([
getResourcePermissions({
userId,
teamId,
resource: Resource.EventType,
userRole: membership.role,
fallbackRoles: {
read: {
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
},
update: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
delete: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
create: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
}),
getResourcePermissions({
userId,
teamId,
resource: Resource.Workflow,
userRole: membership.role,
fallbackRoles: {
read: {
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
},
update: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
delete: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
create: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
}),
]);
return {
eventTypes: {
canRead: eventTypePermissions.canRead,
canCreate: eventTypePermissions.canCreate,
canUpdate: eventTypePermissions.canEdit,
canDelete: eventTypePermissions.canDelete,
},
workflows: {
canRead: workflowPermissions.canRead,
canCreate: workflowPermissions.canCreate,
canUpdate: workflowPermissions.canEdit,
canDelete: workflowPermissions.canDelete,
},
};
};
const ServerPage = async ({ params }: PageProps) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.id) {
@@ -59,7 +152,10 @@ const ServerPage = async ({ params }: PageProps) => {
throw new Error("This event type does not exist");
}
return <EventTypeWebWrapper data={data} id={eventTypeId} />;
// Fetch permissions for the event type's team
const permissions = await getEventPermissions(session.user.id, data.eventType.teamId);
return <EventTypeWebWrapper data={data} id={eventTypeId} permissions={permissions} />;
};
export default ServerPage;
@@ -3,7 +3,6 @@ import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { hasInsightsPermission } from "@calcom/features/insights/server/hasInsightsPermission";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -21,14 +20,5 @@ export async function checkInsightsPagePermission() {
redirect("/auth/login");
}
const hasPermission = await hasInsightsPermission({
userId: session.user.id,
organizationId: session.user.org?.id,
});
if (!hasPermission) {
redirect("/");
}
return session;
}
+2 -4
View File
@@ -1,8 +1,6 @@
import { headers } from "next/headers";
import Script from "next/script";
import { NavigationPermissionsProvider } from "@calcom/features/shell/permissions/NavigationPermissionsProvider";
import PageWrapper from "@components/PageWrapperAppDir";
export default async function PageWrapperLayout({ children }: { children: React.ReactNode }) {
@@ -23,7 +21,7 @@ export default async function PageWrapperLayout({ children }: { children: React.
].filter((script): script is { id: string; script: string } => !!script.script);
return (
<NavigationPermissionsProvider>
<>
<PageWrapper requiresLicense={false} nonce={nonce}>
{children}
{scripts.map((script) => (
@@ -38,6 +36,6 @@ export default async function PageWrapperLayout({ children }: { children: React.
/>
))}
</PageWrapper>
</NavigationPermissionsProvider>
</>
);
}
@@ -141,7 +141,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
<div className="col-span-2 mb-6 font-semibold">
<Price
currency={paymentAppData.currency}
price={paymentAppData.price}
price={props.payment?.amount ?? paymentAppData.price}
displayAlternateSymbol={false}
/>
</div>
@@ -6,7 +6,6 @@ import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
import { APP_NAME } from "@calcom/lib/constants";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -26,11 +25,10 @@ const WebhooksViewServerWrapper = async () => {
redirect("/auth/login");
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
const caller = await createRouterCaller(webhookRouter);
const data = await caller.getByViewer();
return <WebhooksView data={data} isAdmin={isAdmin} />;
return <WebhooksView data={data} />;
};
export default WebhooksViewServerWrapper;
@@ -26,7 +26,7 @@ const Page = async () => {
<SettingsHeader
title={t("billing")}
description={t("manage_billing_description")}
borderInShellHeader={true}>
borderInShellHeader={false}>
<BillingView />
</SettingsHeader>
);
@@ -107,9 +107,9 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
const { isAdvancedMode, permissions, color } = form.watch();
const filteredResources = useMemo(() => {
const { filteredResources, scopedRegistry } = useMemo(() => {
const scopedRegistry = getPermissionsForScope(scope);
return Object.keys(scopedRegistry).filter((resource) =>
const filteredResources = Object.keys(scopedRegistry).filter((resource) =>
t(
scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]]
?.i18nKey || ""
@@ -117,6 +117,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
return { filteredResources, scopedRegistry };
}, [searchQuery, t, scope]);
const createMutation = trpc.viewer.pbac.createRole.useMutation({
@@ -212,7 +213,6 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
disabled={isSystemRole}
/>
</div>
{filteredResources.map((resource) => (
<AdvancedPermissionGroup
key={resource}
@@ -222,7 +222,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
disabled={isSystemRole}
scope={scope}
/>
))}
))}{" "}
</div>
) : (
<div className="bg-muted rounded-xl p-1">
@@ -237,7 +237,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
</div>
</div>
<div className="bg-default border-subtle divide-subtle divide-y rounded-[10px] border">
{Object.keys(getPermissionsForScope(scope)).map((resource) => (
{Object.keys(scopedRegistry).map((resource) => (
<SimplePermissionItem
key={resource}
resource={resource}
@@ -247,7 +247,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
scope={scope}
/>
))}
</div>
</div>{" "}
</div>
)}
</div>
@@ -1,7 +1,11 @@
"use client";
import type { Resource, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import {
Scope,
PERMISSION_REGISTRY,
getPermissionsForScope,
} from "@calcom/features/pbac/domain/types/permission-registry";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ToggleGroup } from "@calcom/ui/components/form";
@@ -21,11 +25,13 @@ export function SimplePermissionItem({
permissions,
onChange,
disabled,
scope,
scope = Scope.Organization,
}: SimplePermissionItemProps) {
const { t } = useLocale();
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope);
const scopedRegistry = getPermissionsForScope(scope);
const registry = scopedRegistry || PERMISSION_REGISTRY;
const isAllResources = resource === "*";
const options = isAllResources
? [
@@ -41,7 +47,7 @@ export function SimplePermissionItem({
return (
<div className="flex items-center justify-between px-3 py-2">
<span className="text-default text-sm font-medium leading-none">
{t(PERMISSION_REGISTRY[resource as Resource]._resource?.i18nKey || resource)}
{t(registry[resource as Resource]._resource?.i18nKey || resource)}
</span>
<ToggleGroup
onValueChange={(val) => {
@@ -1,8 +1,5 @@
import { CrudAction, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
import {
PERMISSION_REGISTRY,
getPermissionsForScope,
} from "@calcom/features/pbac/domain/types/permission-registry";
import { getPermissionsForScope } from "@calcom/features/pbac/domain/types/permission-registry";
import {
getTransitiveDependencies,
getTransitiveDependents,
@@ -52,7 +49,8 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
return permissions.includes("*.*") ? "all" : "none";
}
const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY];
const scopedRegistry = getPermissionsForScope(scope);
const resourceConfig = scopedRegistry[resource as keyof typeof scopedRegistry];
if (!resourceConfig) return "none";
// Check if global all permissions (*.*) is present
@@ -111,7 +109,19 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
allResourcePerms = Object.keys(resourceConfig)
.filter((action) => !action.startsWith("_"))
.map((action) => `${resource}.${action}`);
// Add the resource permissions
newPermissions.push(...allResourcePerms);
// Add all transitive dependencies for each permission
allResourcePerms.forEach((perm) => {
const dependencies = getTransitiveDependencies(perm, scope);
dependencies.forEach((dep) => {
if (!newPermissions.includes(dep)) {
newPermissions.push(dep);
}
});
});
break;
}
@@ -132,15 +142,12 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
// First, remove *.* since we're modifying individual permissions
let newPermissions = currentPermissions.filter((p) => p !== "*.*");
// Parse the permission to get resource and action
const [resource, action] = permission.split(".");
if (enabled) {
// Add the requested permission
newPermissions.push(permission);
// Add all transitive dependencies
const dependencies = getTransitiveDependencies(permission);
const dependencies = getTransitiveDependencies(permission, scope);
dependencies.forEach((dependency) => {
if (!newPermissions.includes(dependency)) {
newPermissions.push(dependency);
@@ -151,7 +158,7 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
newPermissions = newPermissions.filter((p) => p !== permission);
// Remove all transitive dependents
const dependents = getTransitiveDependents(permission);
const dependents = getTransitiveDependents(permission, scope);
dependents.forEach((dependent) => {
newPermissions = newPermissions.filter((p) => p !== dependent);
});
@@ -11,7 +11,7 @@ import { Resource, CrudAction, Scope } from "@calcom/features/pbac/domain/types/
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -4,8 +4,8 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { validPassword } from "@calcom/features/auth/lib/validPassword";
import { hashPassword } from "@calcom/lib/auth/hashPassword";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
+2 -2
View File
@@ -4,8 +4,8 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import z from "zod";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { hashPassword } from "@calcom/lib/auth/hashPassword";
import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid";
import { emailRegex } from "@calcom/lib/emailSchema";
import { HttpError } from "@calcom/lib/http-error";
import slugify from "@calcom/lib/slugify";
@@ -6,7 +6,7 @@ import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/lib/CalendarManager";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import notEmpty from "@calcom/lib/notEmpty";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
@@ -17,7 +17,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
import {
getAllTranscriptsAccessLinkFromMeetingId,
submitBatchProcessorTranscriptionJob,
} from "@calcom/lib/videoClient";
} from "@calcom/app-store/videoClient";
import { generateVideoToken } from "@calcom/lib/videoTokens";
import prisma from "@calcom/prisma";
import { getBooking } from "@calcom/web/lib/daily-webhook/getBooking";
@@ -1,12 +1,12 @@
import { NextResponse } from "next/server";
import { describe, expect, test, vi, afterEach } from "vitest";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient";
import { verifyVideoToken } from "@calcom/lib/videoTokens";
import { GET } from "../route";
vi.mock("@calcom/lib/videoClient", () => ({
vi.mock("@calcom/app-store/videoClient", () => ({
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
}));
+1 -1
View File
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient";
import { verifyVideoToken } from "@calcom/lib/videoTokens";
export async function GET(request: Request) {
@@ -7,9 +7,10 @@ import { useFieldArray, useFormContext } from "react-hook-form";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
import type { LocationObject } from "@calcom/app-store/locations";
import { locationsResolver } from "@calcom/app-store/locations";
import NoSSR from "@calcom/lib/components/NoSSR";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { LocationObject } from "@calcom/lib/location";
import type { AppCategories } from "@calcom/prisma/enums";
import type { EventTypeMetaDataSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { Avatar } from "@calcom/ui/components/avatar";
@@ -188,7 +189,7 @@ const EventTypeGroup = ({
);
};
export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
const ConfigureStepCardContent: FC<ConfigureStepCardProps> = (props) => {
const { loading, formPortalRef, handleSetUpLater } = props;
const { t } = useLocale();
const { control, watch } = useFormContext<TEventTypesForm>();
@@ -212,6 +213,7 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
);
const [submit, setSubmit] = useState(false);
const [mounted, setMounted] = useState(false);
const allUpdated = updatedEventTypesStatus.every((item) => item.every((iitem) => iitem.updated));
useEffect(() => {
@@ -221,60 +223,73 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
}
}, [submit, allUpdated, mainForSubmitRef]);
return (
formPortalRef?.current &&
createPortal(
<div className="mt-8">
{fields.map((group, groupIndex) => (
<div key={group.fieldId}>
{eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && (
<div className="mb-2 mt-4 flex items-center">
<Avatar
alt=""
imageSrc={group.image} // if no image, use default avatar
size="md"
className="inline-flex justify-center"
/>
<p className="text-subtle block pl-2">{group.slug}</p>
</div>
)}
<EventTypeGroup
groupIndex={groupIndex}
setUpdatedEventTypesStatus={setUpdatedEventTypesStatus}
submitRefs={submitRefs}
{...props}
/>
</div>
))}
<button form="outer-event-type-form" type="submit" className="hidden" ref={mainForSubmitRef}>
Save
</button>
<Button
className="text-md mt-6 w-full justify-center"
type="button"
data-testid="configure-step-save"
onClick={() => {
submitRefs.current.forEach((ref) => ref?.click());
setSubmit(true);
}}
loading={loading}>
{t("save")}
</Button>
useEffect(() => {
setMounted(true);
}, []);
<div className="flex w-full flex-row justify-center">
<Button
color="minimal"
data-testid="set-up-later"
onClick={(event) => {
event.preventDefault();
handleSetUpLater();
}}
className="mt-8 cursor-pointer px-4 py-2 font-sans text-sm font-medium">
{t("set_up_later")}
</Button>
if (!formPortalRef?.current || !mounted) {
return null;
}
return createPortal(
<div className="mt-8">
{fields.map((group, groupIndex) => (
<div key={group.fieldId}>
{eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && (
<div className="mb-2 mt-4 flex items-center">
<Avatar
alt=""
imageSrc={group.image} // if no image, use default avatar
size="md"
className="inline-flex justify-center"
/>
<p className="text-subtle block pl-2">{group.slug}</p>
</div>
)}
<EventTypeGroup
groupIndex={groupIndex}
setUpdatedEventTypesStatus={setUpdatedEventTypesStatus}
submitRefs={submitRefs}
{...props}
/>
</div>
</div>,
formPortalRef?.current
)
))}
<button form="outer-event-type-form" type="submit" className="hidden" ref={mainForSubmitRef}>
Save
</button>
<Button
className="text-md mt-6 w-full justify-center"
type="button"
data-testid="configure-step-save"
onClick={() => {
submitRefs.current.forEach((ref) => ref?.click());
setSubmit(true);
}}
loading={loading}>
{t("save")}
</Button>
<div className="flex w-full flex-row justify-center">
<Button
color="minimal"
data-testid="set-up-later"
onClick={(event) => {
event.preventDefault();
handleSetUpLater();
}}
className="mt-8 cursor-pointer px-4 py-2 font-sans text-sm font-medium">
{t("set_up_later")}
</Button>
</div>
</div>,
formPortalRef.current
);
};
export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
return (
<NoSSR>
<ConfigureStepCardContent {...props} />
</NoSSR>
);
};
@@ -2,7 +2,7 @@ import { useEffect, type FC } from "react";
import { EventTypeAppSettings } from "@calcom/app-store/_components/EventTypeAppSettingsInterface";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import useAppsData from "@calcom/lib/hooks/useAppsData";
import useAppsData from "@calcom/features/apps/hooks/useAppsData";
import type { ConfigureStepCardProps } from "@components/apps/installation/ConfigureStepCard";
@@ -164,6 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
showToast(t("booking_confirmation_success"), "success");
}
utils.viewer.bookings.invalidate();
utils.viewer.me.bookingUnconfirmedCount.invalidate();
},
onError: () => {
showToast(t("booking_confirmation_failed"), "error");
+1 -1
View File
@@ -5,7 +5,7 @@ import React from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { useLocale } from "@calcom/lib/hooks/useLocale";
+1 -1
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import type { TeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import type { UserProfile } from "@calcom/types/UserProfile";
import { UserAvatar } from "@calcom/ui/components/avatar";
@@ -10,7 +10,7 @@ import {
ROUTING_FORM_QUEUED_RESPONSE_ID_QUERY_STRING,
} from "@calcom/app-store/routing-forms/lib/constants";
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/lib/server/getTeamMemberEmailFromCrm";
import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm";
import { SchedulingType } from "@calcom/prisma/enums";
vi.mock("@calcom/app-store/routing-forms/appBookingFormHandler", () => ({
@@ -2,13 +2,13 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import type { LocationObject } from "@calcom/app-store/locations";
import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import type { LocationObject } from "@calcom/lib/location";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
@@ -14,6 +14,7 @@ export const useCheckTeamBilling = (teamId?: number | null, isPlatformTeam?: boo
return data.data;
},
enabled: !!teamId && !!isPlatformTeam,
staleTime: 5000,
});
return isTeamBilledAlready;
@@ -4,14 +4,12 @@ import { URLSearchParams } from "url";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getFullName } from "@calcom/features/form-builder/utils";
import { determineReschedulePreventionRedirect } from "@calcom/features/bookings/lib/reschedule/determineReschedulePreventionRedirect";
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { getSafe } from "@calcom/lib/getSafe";
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
const querySchema = z.object({
uid: z.string(),
@@ -63,6 +61,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
allowReschedulingCancelledBookings: true,
team: {
select: {
id: true,
parentId: true,
slug: true,
},
@@ -98,7 +97,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
notFound: true,
} as const;
}
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
const userRepo = new UserRepository(prisma);
@@ -112,59 +110,36 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
profileEnrichedBookingUser: enrichedBookingUser,
});
const isForcedRescheduleForCancelledBooking = allowRescheduleForCancelledBooking;
// If booking is already REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
// If the booking is CANCELLED and allowRescheduleForCancelledBooking is false, we redirect the user to the original event link.
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
// This comes from query param and thus is considered forced
const canBookThroughCancelledBookingRescheduleLink = booking.eventType?.allowReschedulingCancelledBookings;
const isNonRescheduleableBooking =
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
if (isDisabledRescheduling) {
return {
redirect: {
destination: `/booking/${uid}`,
permanent: false,
},
};
}
if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) {
const canReschedule =
booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink;
return {
redirect: {
destination: canReschedule ? eventUrl : `/booking/${uid}`,
permanent: false,
},
};
}
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
// TODO: Show something in UI to let user know that this booking is not rescheduleable
return {
notFound: true,
} as {
notFound: true;
};
} as const;
}
const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date();
if (isBookingInPast && !eventType.allowReschedulingPastBookings) {
const destinationUrlSearchParams = new URLSearchParams();
const responses = bookingSeat ? getSafe<string>(bookingSeat.data, ["responses"]) : booking.responses;
const name = getFullName(getSafe<string | { firstName: string; lastName?: string }>(responses, ["name"]));
const email = getSafe<string>(responses, ["email"]);
// Check if reschedule should be prevented based on booking status and event type settings
const reschedulePreventionRedirectUrl = determineReschedulePreventionRedirect({
booking: {
uid,
status: booking.status,
endTime: booking.endTime,
responses: booking.responses,
eventType: {
disableRescheduling: !!eventType?.disableRescheduling,
allowReschedulingPastBookings: eventType.allowReschedulingPastBookings,
allowBookingFromCancelledBookingReschedule: !!eventType.allowReschedulingCancelledBookings,
teamId: eventType.team?.id ?? null,
},
},
eventUrl,
forceRescheduleForCancelledBooking: allowRescheduleForCancelledBooking,
bookingSeat,
});
if (name) destinationUrlSearchParams.set("name", name);
if (email) destinationUrlSearchParams.set("email", email);
const searchParamsString = destinationUrlSearchParams.toString();
if (reschedulePreventionRedirectUrl) {
return {
redirect: {
destination: searchParamsString ? `${eventUrl}?${searchParamsString}` : eventUrl,
destination: reschedulePreventionRedirectUrl,
permanent: false,
},
};
@@ -108,7 +108,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
"@calcom/lib/server/getTeamMemberEmailFromCrm"
"@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm"
);
const {
email,
@@ -11,7 +11,7 @@ import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import slugify from "@calcom/lib/slugify";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
@@ -15,7 +15,7 @@ import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
import { getAppOnboardingUrl } from "@calcom/lib/apps/getAppOnboardingUrl";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { LocationObject } from "@calcom/lib/location";
import type { LocationObject } from "@calcom/app-store/locations";
import type { Team } from "@calcom/prisma/client";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -6,7 +6,7 @@ import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-util
import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
import { shouldHideBrandingForEvent } from "@calcom/lib/hideBranding";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
@@ -27,7 +27,7 @@ import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { parseEventTypeColor } from "@calcom/lib/isEventTypeColor";
import { localStorage } from "@calcom/lib/webstorage";
import type { MembershipRole } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
@@ -954,6 +954,24 @@ export const EventTypesCTA = ({ userEventGroupsData }: Omit<Props, "user">) => {
userEventGroupsData?.profiles
?.filter((profile) => !profile.readOnly)
?.filter((profile) => !profile.eventTypesLockedByOrg)
?.filter((profile) => {
// For personal profiles (teamId is null), always allow creation
if (!profile.teamId) {
return true;
}
// For team profiles, check if user has eventType.create permission
// This will be populated by the server-side PBAC check
// Fallback to role-based check (admin/owner) if canCreateEventTypes is not set
if (profile.canCreateEventTypes !== undefined) {
return profile.canCreateEventTypes;
}
// Fallback: allow admin and owner roles
return (
profile.membershipRole === MembershipRole.ADMIN || profile.membershipRole === MembershipRole.OWNER
);
})
?.map((profile) => {
return {
teamId: profile.teamId,
+29 -2
View File
@@ -1,5 +1,7 @@
"use client";
import { useState, useCallback } from "react";
import {
DataTableProvider,
DataTableFilters,
@@ -7,6 +9,7 @@ import {
ColumnFilterType,
type FilterableColumn,
} from "@calcom/features/data-table";
import { useDataTable } from "@calcom/features/data-table/hooks/useDataTable";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
import {
AverageEventDurationChart,
@@ -27,11 +30,13 @@ import {
TimezoneBadge,
} from "@calcom/features/insights/components/booking";
import { InsightsOrgTeamsProvider } from "@calcom/features/insights/context/InsightsOrgTeamsProvider";
import { DateTargetSelector, type DateTarget } from "@calcom/features/insights/filters/DateTargetSelector";
import { Download } from "@calcom/features/insights/filters/Download";
import { OrgTeamsFilter } from "@calcom/features/insights/filters/OrgTeamsFilter";
import { useInsightsBookings } from "@calcom/features/insights/hooks/useInsightsBookings";
import { useInsightsOrgTeams } from "@calcom/features/insights/hooks/useInsightsOrgTeams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
export default function InsightsPage({ timeZone }: { timeZone: string }) {
return (
@@ -49,10 +54,26 @@ const createdAtColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_R
type: ColumnFilterType.DATE_RANGE,
};
const startTimeColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_RANGE }> = {
id: "startTime",
title: "startTime",
type: ColumnFilterType.DATE_RANGE,
};
function InsightsPageContent() {
const { t } = useLocale();
const { table } = useInsightsBookings();
const { isAll, teamId, userId } = useInsightsOrgTeams();
const { removeFilter } = useDataTable();
const [dateTarget, _setDateTarget] = useState<"startTime" | "createdAt">("startTime");
const setDateTarget = useCallback(
(target: "startTime" | "createdAt") => {
_setDateTarget(target);
removeFilter(target === "startTime" ? "createdAt" : "startTime");
},
[_setDateTarget, removeFilter]
);
return (
<>
@@ -63,10 +84,16 @@ function InsightsPageContent() {
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
<DataTableFilters.ActiveFilters table={table} />
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
<DataTableFilters.ClearFiltersButton exclude={["startTime", "createdAt"]} />
<div className="grow" />
<Download />
<DateRangeFilter column={createdAtColumn} />
<ButtonGroup combined>
<DateRangeFilter
column={dateTarget === "startTime" ? startTimeColumn : createdAtColumn}
options={{ convertToTimeZone: true }}
/>
<DateTargetSelector value={dateTarget as DateTarget} onChange={setDateTarget} />
</ButtonGroup>
<TimezoneBadge />
</div>
@@ -77,21 +77,26 @@ const BillingView = () => {
return (
<>
<div className="border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-8 text-sm sm:space-y-8">
<CtaRow title={t("view_and_manage_billing_details")} description={t("view_and_edit_billing_details")}>
<Button color="primary" href={billingHref} target="_blank" EndIcon="external-link">
<div className="bg-muted border-muted mt-5 rounded-xl border p-1">
<div className="bg-default border-muted flex rounded-[10px] border px-5 py-4">
<div className="flex w-full flex-col gap-1">
<h3 className="text-emphasis text-sm font-semibold leading-none">{t("manage_billing")}</h3>
<p className="text-subtle text-sm font-medium leading-tight">
{t("view_and_manage_billing_details")}
</p>
</div>
<Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link">
{t("billing_portal")}
</Button>
</CtaRow>
</div>
<BillingCredits />
<div className="border-subtle mt-6 space-y-6 rounded-lg border px-6 py-8 text-sm sm:space-y-8">
<CtaRow title={t("need_anything_else")} description={t("further_billing_help")}>
<Button color="secondary" onClick={onContactSupportClick}>
</div>
<div className="flex items-center justify-between px-4 py-5">
<p className="text-subtle text-sm font-medium leading-tight">{t("need_help")}</p>
<Button color="secondary" size="sm" onClick={onContactSupportClick}>
{t("contact_support")}
</Button>
</CtaRow>
</div>
</div>
<BillingCredits />
</>
);
};
@@ -8,17 +8,21 @@ import { useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { MemberInvitationModalWithoutMembers } from "@calcom/features/ee/teams/components/MemberInvitationModal";
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants";
import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { trpc } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Select } from "@calcom/ui/components/form";
import { TextField, Label, InputError } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { ProgressBar } from "@calcom/ui/components/progress-bar";
import { showToast } from "@calcom/ui/components/toast";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton";
@@ -29,6 +33,39 @@ type MonthOption = {
endDate: string;
};
type CreditRowProps = {
label: string;
value: number;
isBold?: boolean;
underline?: "dashed" | "solid";
className?: string;
};
const CreditRow = ({ label, value, isBold = false, underline, className = "" }: CreditRowProps) => {
const numberFormatter = new Intl.NumberFormat();
return (
<div
className={classNames(
`my-1 flex justify-between`,
underline === "dashed"
? "border-subtle border-b border-dashed"
: underline === "solid"
? "border-subtle border-b border-solid"
: "mt-1",
className
)}>
<span
className={classNames("text-sm", isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}>
{label}
</span>
<span
className={classNames(`text-sm`, isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}>
{numberFormatter.format(value)}
</span>
</div>
);
};
const getMonthOptions = (): MonthOption[] => {
const options: MonthOption[] = [];
const minDate = dayjs.utc("2025-05-01");
@@ -60,6 +97,7 @@ export default function BillingCredits() {
const monthOptions = useMemo(() => getMonthOptions(), []);
const [selectedMonth, setSelectedMonth] = useState<MonthOption>(monthOptions[0]);
const [isDownloading, setIsDownloading] = useState(false);
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
const utils = trpc.useUtils();
const {
@@ -71,6 +109,7 @@ export default function BillingCredits() {
const params = useParamsWithFallback();
const orgId = session.data?.user?.org?.id;
const orgSlug = session.data?.user?.org?.slug;
const parsedTeamId = Number(params.id);
const teamId: number | undefined = Number.isFinite(parsedTeamId)
@@ -132,117 +171,153 @@ export default function BillingCredits() {
buyCreditsMutation.mutate({ quantity: data.quantity, teamId });
};
const teamCreditsPercentageUsed =
creditsData.credits.totalMonthlyCredits > 0
? (creditsData.credits.totalRemainingMonthlyCredits / creditsData.credits.totalMonthlyCredits) * 100
: 0;
const totalCredits = creditsData.credits.totalMonthlyCredits ?? 0;
const totalUsed = creditsData.credits.totalCreditsUsedThisMonth ?? 0;
const teamCreditsPercentageUsed = totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0;
const numberFormatter = new Intl.NumberFormat();
return (
<div className="border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 pb-6 text-sm sm:space-y-8">
<div>
<h2 className="text-base font-semibold">{t("credits")}</h2>
<ServerTrans
t={t}
i18nKey="view_and_manage_credits_description"
components={[
<Link
key="Credit System"
className="underline underline-offset-2"
target="_blank"
href="https://cal.com/help/billing-and-usage/messaging-credits">
Learn more
</Link>,
]}
/>
<div className="-mx-6 mt-6">
<hr className="border-subtle" />
<>
<div className="bg-muted border-muted mt-5 rounded-xl border p-1">
<div className="flex flex-col gap-1 px-4 py-5">
<h2 className="text-default text-base font-semibold leading-none">{t("credits")}</h2>
<p className="text-subtle text-sm font-medium leading-tight">{t("view_and_manage_credits")}</p>
</div>
<div className="mt-6">
{creditsData.credits.totalMonthlyCredits > 0 ? (
<div className="mb-4">
<Label>{t("monthly_credits")}</Label>
<ProgressBar
color="green"
percentageValue={teamCreditsPercentageUsed}
label={`${Math.max(0, Math.round(teamCreditsPercentageUsed))}%`}
/>
<div className="text-subtle">
<div>
{t("total_credits", {
totalCredits: creditsData.credits.totalMonthlyCredits,
})}
<div className="bg-default border-muted flex w-full rounded-[10px] border px-5 py-4">
<div className="w-full">
{totalCredits > 0 ? (
<>
<div className="mb-4">
<CreditRow
label={t("monthly_credits")}
value={totalCredits}
isBold={true}
underline="dashed"
/>
<CreditRow label={t("credits_used")} value={totalUsed} underline="solid" />
<CreditRow
label={t("total_credits_remaining")}
value={creditsData.credits.totalRemainingMonthlyCredits}
/>
<div className="mt-4">
<ProgressBar color="green" percentageValue={100 - teamCreditsPercentageUsed} />
</div>
{/*750 credits per tip*/}
<div className="mt-4 flex flex-1 items-center justify-between">
<p className="text-subtle text-sm font-medium leading-tight">
{orgSlug ? t("credits_per_tip_org") : t("credits_per_tip_teams")}
</p>
<Button onClick={() => setShowMemberInvitationModal(true)} size="sm" color="secondary">
{t("add_members_no_ellipsis")}
</Button>
</div>
</div>
<div>
{t("remaining_credits", {
remainingCredits: creditsData.credits.totalRemainingMonthlyCredits,
})}
<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>
</>
) : (
<></>
)}
{/*Auto Top-Up goes here when we have it*/}
{/*<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>*/}
{/*Additional Credits*/}
<form onSubmit={handleSubmit(onSubmit)} className="mt-4 flex">
<div className="-mb-1 mr-auto w-full">
<div className="flex justify-between">
<Label>{t("additional_credits")}</Label>
<div className="mb-2 flex items-center gap-1">
<p className="text-sm font-semibold leading-none">
<span className="text-subtle font-medium">{t("current_balance")}</span>{" "}
{numberFormatter.format(creditsData.credits.additionalCredits)}
</p>
<Tooltip content={t("view_additional_credits_expense_tip")}>
<Icon name="info" className="text-emphasis h-3 w-3" />
</Tooltip>
</div>
</div>
<div className="flex w-full items-center gap-2">
<TextField
required
type="number"
{...register("quantity", {
required: t("error_required_field"),
min: { value: 50, message: t("minimum_of_credits_required") },
valueAsNumber: true,
})}
label=""
containerClassName="w-full -mt-1"
size="sm"
onChange={(e) => setValue("quantity", Number(e.target.value))}
min={50}
addOnSuffix={<>{t("credits")}</>}
/>
<Button color="secondary" target="_blank" size="sm" type="submit" data-testid="buy-credits">
{t("buy")}
</Button>
</div>
</div>
</div>
) : (
<></>
)}
<Label>
{creditsData.credits.totalMonthlyCredits ? t("additional_credits") : t("available_credits")}
</Label>
<div className="mt-2 text-sm">{creditsData.credits.additionalCredits}</div>
<div className="-mx-6 mb-6 mt-6">
<hr className="border-subtle mb-3 mt-3" />
</div>
<form onSubmit={handleSubmit(onSubmit)} className="flex">
<div className="-mb-1 mr-auto">
<Label>{t("buy_additional_credits")}</Label>
<div className="flex flex-col">
<TextField
required
type="number"
{...register("quantity", {
required: t("error_required_field"),
min: { value: 50, message: t("minimum_of_credits_required") },
valueAsNumber: true,
})}
label=""
containerClassName="w-60"
onChange={(e) => setValue("quantity", Number(e.target.value))}
min={50}
addOnSuffix={<>{t("credits")}</>}
/>
{errors.quantity && <InputError message={errors.quantity.message ?? t("invalid_input")} />}
</div>
</form>
<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>
<div className="mt-auto">
<Button
color="primary"
target="_blank"
EndIcon="external-link"
type="submit"
data-testid="buy-credits">
{t("buy_credits")}
</Button>
</div>
</form>
<div className="-mx-6 mb-6 mt-6">
<hr className="border-subtle mb-3 mt-3" />
</div>
<div className="flex">
<div className="mr-auto">
<Label className="mb-4">{t("download_expense_log")}</Label>
<div className="mt-2 flex flex-col">
<Select
options={monthOptions}
value={selectedMonth}
onChange={(option) => option && setSelectedMonth(option)}
/>
{/*Download Expense Log*/}
<div className="mt-4 flex">
<div className="mr-auto w-full">
<Label className="mb-4">{t("download_expense_log")}</Label>
<div className="mr-2 mt-1">
<Select
size="sm"
className="w-full"
innerClassNames={{
control: "font-medium text-emphasis",
}}
options={monthOptions}
value={selectedMonth}
onChange={(option) => option && setSelectedMonth(option)}
/>
</div>
</div>
<div className="mt-auto">
<Button onClick={handleDownload} loading={isDownloading} color="secondary" size="sm">
{t("download")}
</Button>
</div>
</div>
<div className="mt-auto">
<Button onClick={handleDownload} loading={isDownloading} StartIcon="file-down">
{t("download")}
</Button>
</div>
</div>
</div>
{/*Credit Worth Section*/}
<div className="text-subtle px-5 py-4 text-sm font-medium leading-tight">
<ServerTrans
t={t}
i18nKey="credit_worth_description"
components={[
<Link
key="Credit System"
className="underline underline-offset-2"
target="_blank"
href="https://cal.com/help/billing-and-usage/messaging-credits">
Learn more
</Link>,
]}
/>
</div>
</div>
</div>
{teamId && (
<MemberInvitationModalWithoutMembers
teamId={teamId}
showMemberInvitationModal={showMemberInvitationModal}
hideInvitationModal={() => setShowMemberInvitationModal(false)}
onSettingsOpen={() => {
return;
}}
/>
)}
</>
);
}
@@ -3,77 +3,76 @@ import { SkeletonText, SkeletonContainer, SkeletonButton } from "@calcom/ui/comp
export function BillingCreditsSkeleton() {
return (
<SkeletonContainer>
<div className="border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 text-sm sm:space-y-8">
{/* Title and Description */}
<div>
<div>
<SkeletonText className="mb-2 h-4 w-20" /> {/* Credits title */}
</div>
<div>
<SkeletonText className="h-4 w-72" /> {/* Description */}
</div>
<div className="-mx-6 mt-6">
<hr className="border-subtle" />
</div>
{/* Monthly credits */}
<div className="mt-[20px]">
<SkeletonText className="mb-4 h-4 w-32" /> {/* Monthly credits label */}
<div className="h-2 w-full rounded-md bg-gray-200" /> {/* Progress bar */}
<div className="text-subtle mt-2">
<div>
<SkeletonText className="h-4 w-40" /> {/* Total credits */}
<div className="bg-muted border-muted mt-5 rounded-xl p-1">
<div className="flex flex-col gap-1 px-4 py-5">
<SkeletonText className="h-4 w-20" /> {/* Credits title */}
<SkeletonText className="h-4 w-48" /> {/* View and manage credits description */}
</div>
<div className="bg-default border-muted flex w-full rounded-[10px] px-5 py-4">
<div className="w-full">
{/* Credits section */}
<div className="mb-4">
{/* Monthly credits row */}
<div className="mt-1 flex justify-between">
<SkeletonText className="h-4 w-32" />
<SkeletonText className="h-4 w-20" />
</div>
<div>
<SkeletonText className="h-4 w-40" /> {/* Remaining credits */}
{/* Additional credits row */}
<div className="mt-1 flex justify-between">
<SkeletonText className="h-4 w-36" />
<SkeletonText className="h-4 w-20" />
</div>
{/* Remaining row */}
<div className="mt-1 flex justify-between">
<SkeletonText className="h-4 w-24" />
<SkeletonText className="h-4 w-20" />
</div>
{/* Progress bar */}
<div className="mt-4">
<div className="h-2 w-full rounded-md bg-gray-200" />
</div>
{/* Credits per tip */}
<div className="mt-4 flex flex-1 justify-between">
<SkeletonText className="h-4 w-32" />
<SkeletonButton className="h-8 w-24 rounded-md" />
</div>
</div>
<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>
{/* Additional Credits form */}
<div className="mt-4 flex">
<div className="-mb-1 mr-auto w-full">
<SkeletonText className="mb-2 h-4 w-32" /> {/* Additional credits label */}
<div className="flex w-full">
<SkeletonText className="h-9 w-full rounded-md" /> {/* Input field */}
</div>
</div>
<div className="ml-2 mt-auto">
<SkeletonButton className="h-9 w-12 rounded-md" /> {/* Buy button */}
</div>
</div>
<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>
{/* Download Expense Log */}
<div className="mt-4 flex">
<div className="mr-auto w-full">
<SkeletonText className="mb-4 h-4 w-40" /> {/* Download expense log label */}
<div className="mr-2 mt-1">
<SkeletonText className="h-9 w-full rounded-md" /> {/* Select dropdown */}
</div>
</div>
<div className="mt-auto">
<SkeletonButton className="h-9 w-20 rounded-md" /> {/* Download button */}
</div>
</div>
</div>
{/* Additional credits */}
<div className="mt-4">
<div>
<SkeletonText className="mb-2 h-4 w-40" /> {/* Additional credits label */}
</div>
<div>
<SkeletonText className="h-4 w-24" /> {/* Additional credits value */}
</div>
</div>
<div className="-mx-6 mb-6 mt-4">
<hr className="border-subtle mb-3 mt-3" />
</div>
{/* Buy credits form */}
<div className="flex">
<div className="mr-auto">
<div>
<SkeletonText className="mb-2 h-4 w-[268px]" /> {/* Buy credits label */}
</div>
<div>
<SkeletonText className="mt-2 h-8 w-[240px] rounded-md" /> {/* Input field */}
</div>
</div>
<div className="mt-auto ">
<SkeletonButton className="h-9 w-[120px] rounded-md" /> {/* Buy button */}
</div>
</div>
<div className="-mx-6 mb-6 mt-6">
<hr className="border-subtle mb-3 mt-3" />
</div>
{/* Download expense log */}
<div className="flex">
<div className="mr-auto mt-1">
<div>
<SkeletonText className="mb-2 h-4 w-[156px]" /> {/* Label */}
</div>
<div>
<SkeletonText className="mt-2 h-8 w-[156px] rounded-md" /> {/* Select */}
</div>
</div>
<div className="mt-auto ">
<SkeletonButton className="h-9 w-[109px] rounded-md" /> {/* Download button */}
</div>
</div>
</div>
{/* Credit Worth Section */}
<div className="text-subtle px-5 py-4">
<SkeletonText className="h-4 w-full" />
<SkeletonText className="mt-1 h-4 w-3/4" />
</div>
</div>
</SkeletonContainer>
+5 -4
View File
@@ -1,13 +1,13 @@
{
"name": "@calcom/web",
"version": "5.7.0",
"version": "5.7.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
"analyze:server": "BUNDLE_ANALYZE=server next build",
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "yarn copy-static && next dev --turbopack",
"dev": "turbo run copy-app-store-static && next dev --turbopack",
"dev:scan": "yarn dev & npx --yes react-scan@latest localhost:3000",
"dev:cron": "npx tsx cron-tester.ts",
"dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https",
@@ -16,8 +16,9 @@
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"sentry:release": "NODE_OPTIONS='--max-old-space-size=6144' node scripts/create-sentry-release.js",
"copy-static": "node scripts/copy-app-store-static.js",
"build": "yarn copy-static && next build && yarn sentry:release",
"copy-static": "turbo run copy-app-store-static",
"copy-app-store-static": "node scripts/copy-app-store-static.js",
"build": "turbo run copy-app-store-static && next build && yarn sentry:release",
"start": "next start",
"lint": "eslint . --ignore-path .gitignore",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
+69
View File
@@ -0,0 +1,69 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.describe("Admin Users Management", () => {
test("should add a new user successfully", async ({ page, users }) => {
const adminUser = await users.create({
role: "ADMIN",
});
await adminUser.apiLogin();
await page.goto("/settings/admin/users/add");
await page.waitForLoadState();
await page.fill('input[name="name"]', "Test User");
await page.fill('input[name="username"]', "testuser");
await page.fill('input[name="email"]', "testuser@example.com");
await page.fill('input[name="bio"]', "Test user bio");
await page.locator('[data-testid="timezone-select"] div').first().click();
const responsePromise = page.waitForResponse((response) =>
response.url().includes("/api/trpc/users/add")
);
await page.click('button[type="submit"]');
const response = await responsePromise;
expect(response.status()).toBe(200);
await page.waitForURL("/settings/admin/users");
});
test("should edit an existing user successfully", async ({ page, users }) => {
const adminUser = await users.create({
role: "ADMIN",
});
const userToEdit = await users.create({
name: "Edit User",
username: "edituser",
});
await adminUser.apiLogin();
await page.goto(`/settings/admin/users/${userToEdit.id}/edit`);
await page.waitForLoadState();
await expect(page.locator('input[name="name"]')).toHaveValue("Edit User");
await page.fill('input[name="name"]', "Updated User");
const responsePromise = page.waitForResponse((response) =>
response.url().includes("/api/trpc/users/update")
);
await page.click('button[type="submit"]');
const response = await responsePromise;
expect(response.status()).toBe(200);
await page.waitForURL("/settings/admin/users");
});
});
@@ -16,8 +16,8 @@ test.describe("Duplicate API Calls Prevention", () => {
const trpcCalls: string[] = [];
const apiV2Calls: string[] = [];
// Intercept tRPC getSchedule calls - pattern matches trpc.viewer.slots.getSchedule.useQuery()
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
// Intercept tRPC getSchedule calls - pattern matches /api/trpc/slots/getSchedule
await page.route("**/api/trpc/slots/getSchedule**", async (route) => {
trpcCalls.push(route.request().url());
await route.continue();
});
@@ -33,14 +33,10 @@ test.describe("Duplicate API Calls Prevention", () => {
const totalCalls = trpcCalls.length + apiV2Calls.length;
if (totalCalls === 0) {
console.log("No API calls detected - environment may have issues preventing page load");
expect(totalCalls).toBeGreaterThanOrEqual(0);
} else {
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
}
expect(totalCalls).toBeGreaterThan(0);
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
});
test("should detect when schedule endpoints are called multiple times for team events", async ({
@@ -65,7 +61,7 @@ test.describe("Duplicate API Calls Prevention", () => {
const trpcCalls: string[] = [];
const apiV2Calls: string[] = [];
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
await page.route("**/api/trpc/slots/getSchedule**", async (route) => {
trpcCalls.push(route.request().url());
await route.continue();
});
@@ -81,14 +77,10 @@ test.describe("Duplicate API Calls Prevention", () => {
const totalCalls = trpcCalls.length + apiV2Calls.length;
if (totalCalls === 0) {
console.log("No API calls detected - environment may have issues preventing page load");
expect(totalCalls).toBeGreaterThanOrEqual(0);
} else {
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
}
expect(totalCalls).toBeGreaterThan(0);
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
});
test("should detect when schedule endpoints are called multiple times for organization team events", async ({
@@ -117,7 +109,7 @@ test.describe("Duplicate API Calls Prevention", () => {
const trpcCalls: string[] = [];
const apiV2Calls: string[] = [];
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
await page.route("**/api/trpc/slots/getSchedule**", async (route) => {
trpcCalls.push(route.request().url());
await route.continue();
});
@@ -133,13 +125,9 @@ test.describe("Duplicate API Calls Prevention", () => {
const totalCalls = trpcCalls.length + apiV2Calls.length;
if (totalCalls === 0) {
console.log("No API calls detected - environment may have issues preventing page load");
expect(totalCalls).toBeGreaterThanOrEqual(0);
} else {
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
}
expect(totalCalls).toBeGreaterThan(0);
expect(totalCalls).toBeLessThanOrEqual(1);
expect(trpcCalls.length).toBeLessThanOrEqual(1);
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
});
});
+3
View File
@@ -111,6 +111,9 @@ export function createAppsFixture(page: Page) {
},
goToEventType: async (eventType: string) => {
await page.getByRole("link", { name: eventType }).click();
// fix the race condition
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
},
goToAppsTab: async () => {
await page.getByTestId("vertical-tab-apps").click();
+8
View File
@@ -584,6 +584,10 @@ export const createUsersFixture = (
get: () => store.users,
logout: async () => {
await page.goto("/auth/logout");
const logoutBtn = page.getByTestId("logout-btn");
await expect(logoutBtn).toHaveText("Go back to the login page");
await page.reload();
await expect(logoutBtn).toHaveText("Go back to the login page");
},
deleteAll: async () => {
const ids = store.users.map((u) => u.id);
@@ -680,6 +684,10 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
},
logout: async () => {
await page.goto("/auth/logout");
const logoutBtn = page.getByTestId("logout-btn");
await expect(logoutBtn).toHaveText("Go back to the login page");
await page.reload();
await expect(logoutBtn).toHaveText("Go back to the login page");
},
getFirstTeamMembership: async () => {
const memberships = await prisma.membership.findMany({
+79 -2
View File
@@ -1,10 +1,13 @@
import { expect } from "@playwright/test";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { randomString } from "@calcom/lib/random";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import { clearFilters, applySelectFilter } from "./filter-helpers";
import { test } from "./lib/fixtures";
import { createAllPermissionsArray, enablePBACForTeam } from "./lib/test-helpers/pbac";
test.describe.configure({ mode: "parallel" });
@@ -268,8 +271,82 @@ test.describe("Insights", async () => {
];
for (const title of expectedChartTitles) {
const chartCard = page.locator("[data-testid='panel-card'] h2").filter({ hasText: title });
const chartCard = page
.locator("[data-testid='panel-card'] h2")
.filter({ hasText: new RegExp(`^${title}$`) });
await expect(chartCard).toBeVisible();
}
});
test("should be able to access insights page with custom role lacking insights.read permission", async ({
page,
users,
}) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
});
const userOne = await users.create();
const userTwo = await users.create();
const { teamOne } = await createTeamsAndMembership(userOne.id, userTwo.id);
const orgMembership = await owner.getOrgMembership();
const orgId = orgMembership.team.id;
await enablePBACForTeam(orgId);
await enablePBACForTeam(teamOne.id);
const permissions = createAllPermissionsArray().filter(
({ resource, action }) => !(resource === "insights" && action === "read")
);
const customRole = await prisma.role.create({
data: {
id: `e2e_no_insights_${orgId}_${Date.now()}`,
name: "E2E Role Without Insights",
description: "E2E role for testing - has all permissions except insights.read",
color: "#dc2626",
teamId: orgId,
type: "CUSTOM",
permissions: {
create: permissions,
},
},
});
await prisma.membership.update({
where: {
userId_teamId: {
userId: userOne.id,
teamId: teamOne.id,
},
},
data: {
customRoleId: customRole.id,
},
});
const featuresRepository = new FeaturesRepository(prisma);
const isPBACEnabled = await featuresRepository.checkIfTeamHasFeature(orgId, "pbac");
expect(isPBACEnabled).toBe(true);
const permissionService = new PermissionCheckService();
const hasPermission = await permissionService.checkPermission({
userId: userOne.id,
teamId: teamOne.id,
permission: "insights.read",
fallbackRoles: [],
});
expect(hasPermission).toBe(false);
await userOne.apiLogin();
await page.goto("/insights");
// Verify the user can access the insights page
await page.locator('[data-testid^="insights-filters-"]').waitFor();
expect(page.url()).toContain("/insights");
});
});
@@ -0,0 +1,29 @@
import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
import { prisma } from "@calcom/prisma";
// Create array of all permissions from PERMISSION_REGISTRY
export const createAllPermissionsArray = () => {
const allPermissions: { resource: string; action: string }[] = [];
Object.entries(PERMISSION_REGISTRY).forEach(([resource, resourceConfig]) => {
if (resource === "*") {
return;
}
Object.entries(resourceConfig).forEach(([action, _details]) => {
allPermissions.push({ resource, action });
});
});
return allPermissions;
};
export const enablePBACForTeam = async (teamId: number) => {
await prisma.teamFeatures.create({
data: {
featureId: "pbac",
teamId: teamId,
assignedBy: "e2e",
assignedAt: new Date(),
},
});
};
+2 -2
View File
@@ -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(
+1 -1
View File
@@ -15,7 +15,7 @@ test.describe("Workflow Tab - Event Type", () => {
test("Creating a new workflow", async ({ workflowPage }) => {
const { createWorkflow, assertListCount } = workflowPage;
await createWorkflow({ name: "" });
await createWorkflow({ name: "test workflow" });
await assertListCount(3);
});
+7
View File
@@ -512,6 +512,10 @@
<symbol class="lucide lucide-paperclip" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="paperclip">
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" ></path>
</symbol>
<symbol class="lucide lucide-pause" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="pause">
<rect x="14" y="4" width="4" height="16" rx="1" ></rect>
<rect x="6" y="4" width="4" height="16" rx="1" ></rect>
</symbol>
<symbol class="lucide lucide-pencil" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="pencil">
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" ></path>
<path d="m15 5 4 4" ></path>
@@ -533,6 +537,9 @@
<symbol class="lucide lucide-phone" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="phone">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" ></path>
</symbol>
<symbol class="lucide lucide-play" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="play">
<polygon points="6 3 20 12 6 21 6 3" ></polygon>
</symbol>
<symbol class="lucide lucide-plus" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="plus">
<path d="M5 12h14" ></path>
<path d="M12 5v14" ></path>

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

+2 -1
View File
@@ -16,7 +16,8 @@
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display_override": ["minimal-ui"],
"display": "standalone",
"scope": "/",
"start_url": "/"
}
}
+85 -6
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم",
"verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني",
"verify_email_button": "تأكيد البريد الإلكتروني",
"cal_ai_assistant": "مساعد Cal AI",
"cal_ai_assistant": "المساعد",
"send_cal_video_transcription_emails": "إرسال رسائل النسخ النصي لفيديو Cal",
"description_send_cal_video_transcription_emails": "إرسال رسائل بريد إلكتروني تحتوي على النسخ النصي لفيديو Cal بعد انتهاء الاجتماع. (يتطلب خطة مدفوعة)",
"verify_email_change_description": "لقد طلبت تغيير البريد الإلكتروني المستخدم لحسابك في {{appName}}. الرجاء النقر على الزر أدناه لتأكيد بريدك الإلكتروني الجديد.",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "فشل التحقق من صحة سير العمل",
"workflow_validation_empty_fields": "خطوة واحدة أو أكثر من خطوات سير العمل تحتوي على محتوى رسالة فارغ",
"workflow_validation_unverified_contacts": "لم يتم التحقق من رقم هاتف واحد أو أكثر أو عناوين البريد الإلكتروني",
"supercharge_your_workflows_with_cal_ai": "عزز سير عملك مع Cal.ai",
"supercharge_your_workflows_with_cal_ai_description": "وكلاء ذكاء اصطناعي واقعيون يقومون بحجز الاجتماعات وإرسال التذكيرات ومتابعة عملائك.",
"phone_number_imported_successfully": "تم استيراد رقم الهاتف وربطه بالوكيل بنجاح",
"phone_number_deleted_successfully": "تم حذف رقم الهاتف بنجاح",
"delete_phone_number_confirmation": "هل أنت متأكد أنك تريد حذف رقم الهاتف هذا؟ لا يمكن التراجع عن هذا الإجراء.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "تم إلغاء اشتراك رقم الهاتف بنجاح",
"updating": "جاري التحديث",
"round_robin": "الترتيب الدوري",
"hi_how_are_you_doing": "مرحبًا، كيف حالك؟",
"round_robin_description": "نقل الاجتماعات بشكل دوري بين أعضاء الفريق المتعددين.",
"managed_event": "حدث تم إدارته",
"username_placeholder": "اسم المستخدم",
@@ -876,9 +879,9 @@
"are_you_sure_you_want_to_delete_workflow_step": "هل أنت متأكد من أنك تريد حذف خطوة مسار العمل هذه؟",
"do_you_still_want_to_unsubscribe": "هل ما زلت ترغب في إلغاء اشتراك رقم الهاتف من هذا الوكيل؟",
"the_action_will_disconnect_phone_number": "سيؤدي هذا الإجراء إلى فصل رقم الهاتف عن الوكيل. لن يتمكن الوكيل من إجراء مكالمات حتى يتم توصيل رقم هاتف جديد.",
"cal_ai_phone_numbers": "أرقام هواتف Cal AI",
"cal_ai_phone_numbers": "أرقام الهواتف",
"connect_phone_number": "توصيل رقم الهاتف",
"cal_ai_phone_numbers_description": "إدارة أرقام هواتف الخاصة بك",
"cal_ai_phone_numbers_description": "إدارة أرقام هواتفك",
"import_number": "استيراد رقم",
"this_action_will_also": "سيقوم هذا الإجراء أيضًا بـ:",
"import_phone_number": "استيراد رقم الهاتف",
@@ -886,7 +889,7 @@
"cancel_your_phone_number_subscription": "إلغاء اشتراك رقم هاتفك",
"delete_associated_phone_number": "حذف رقم الهاتف المرتبط",
"unauthorized_create_workflow": "أنت غير مصرح لك بإنشاء مسار العمل هذا",
"import_phone_number_description": "استيراد رقم هاتف Twilio الخاص بك لاستخدامه مع هاتف Cal AI",
"import_phone_number_description": "استيراد رقم هاتف Twilio الخاص بك لاستخدامه مع الهاتف",
"phone_number_cost": "${{price}}/شهريًا",
"buy_new_number": "شراء رقم جديد",
"buy_number_cost_x_per_month": "تكلفة شراء رقم هاتف هي ${{priceInDollars}} شهريًا. سيتم محاسبتك شهريًا على كل رقم هاتف نشط.",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "نعم، إلغاء الاشتراك",
"cancel_phone_number_subscription_confirmation": "هل أنت متأكد من رغبتك في إلغاء اشتراك رقم الهاتف هذا؟ لا يمكن التراجع عن هذا الإجراء وستفقد إمكانية الوصول إلى رقم الهاتف هذا.",
"add_members": "إضافة أعضاء...",
"add_members_no_ellipsis": "إضافة أعضاء",
"no_assigned_members": "لا يوجد أعضاء معينين",
"assigned_to": "تم التعيين إلى",
"you_must_be_logged_in_to": "يجب تسجيل الدخول إلى {{url}}",
@@ -1207,6 +1211,7 @@
"categories": "الفئات",
"pricing": "التسعير",
"learn_more": "معرفة المزيد",
"try_now": "جرب الآن",
"privacy_policy": "سياسة الخصوصية",
"terms_of_service": "شروط الخدمة",
"remove": "إزالة",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "إرسال رسالة عبر WhatsApp إلى أحد الحاضرين",
"workflows": "سير العمل",
"new_workflow_btn": "سير عمل جديد",
"how_would_you_like_to_start": "كيف ترغب في البدء؟",
"add_new_workflow": "إضافة سير عمل جديد",
"reschedule_event_trigger": "متى يتم إعادة جدولة الحدث",
"trigger": "مشغل",
@@ -1716,6 +1722,8 @@
"event_duration_info": "مدة الحدث",
"event_time_info": "وقت بدء الحدث",
"event_type_not_found": "لم يتم العثور على نوع الحدث",
"number_to_call_variable": "رقم للاتصال",
"number_to_call_info": "رقم هاتف المستخدم الذي تتصل به",
"location_variable": "الموقع",
"location_info": "موقع الحدث",
"additional_notes_variable": "ملاحظات إضافية",
@@ -1753,6 +1761,7 @@
"team_url": "رابط الفريق",
"team_members": "أعضاء الفريق",
"more": "المزيد",
"cal_ai_workflows": "سير عمل Cal.ai",
"and_count_more": "و {{count}} أكثر",
"more_page_footer": "نعتبر تطبيق الهاتف المحمول امتداداً لتطبيق الويب، لكن يرجى العودة لتطبيق الويب إذا كنت تقوم بأي إجراءات معقدة.",
"workflow_example_1": "إرسال رسائل قصيرة للحضور للتذكير قبل 24 ساعة من بدء الحدث",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "إرسال رسائل قصيرة للحضور للتذكير قبل ساعة من بدء الحدث",
"workflow_example_5": "إرسال رسالة قصيرة مخصصة للحضور عند إعادة جدولة الحدث",
"workflow_example_6": "إرسال بريد إلكتروني مخصص للمضيف عندما يتم حجز حدث جديد",
"send_sms_reminder": "إرسال تذكير عبر الرسائل القصيرة",
"send_sms_reminder_description": "قبل 24 ساعة من بدء الحدث",
"follow_up_with_no_shows": "متابعة المتغيبين",
"follow_up_with_no_shows_description": "بعد 30 دقيقة من انتهاء الحدث",
"remind_attendees_to_bring_id": "تذكير الحاضرين بإحضار الهوية",
"remind_attendees_to_bring_id_description": "قبل يوم واحد من بدء الحدث",
"email_to_remind_booking": "تذكير بالبريد الإلكتروني",
"email_to_remind_booking_description": "قبل ساعة واحدة من بدء الحدث",
"custom_sms_reminder": "تذكير مخصص عبر الرسائل القصيرة",
"custom_sms_reminder_description": "عند جدولة الحدث",
"custom_email_reminder": "تذكير مخصص بالبريد الإلكتروني",
"custom_email_reminder_description": "إعادة جدولة الحدث للمضيف",
"count_managed_to_limit": "تضمين عدد الحجوزات من أنواع الفعاليات المُدارة",
"welcome_to_cal_header": "مرحبًا بك في {{appName}}!",
"edit_form_later_subtitle": "ستتمكن من تعديل هذا لاحقًا.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "إظهار في صفحة الحجز",
"visit_cancelled_booking": "يمكنك زيارة صفحة الحجز الملغى",
"get_started_zapier_templates": "البدء في استخدام قوالب Zapier",
"standard_templates": "القوالب القياسية",
"cal_ai_templates": "قوالب Cal.ai",
"team_is_unpublished": "لم يُنشر {{team}}",
"org_is_unpublished_description": "رابط هذه المنظمة غير متاح حاليًا. يرجى الاتصال بمالك المنظمة أو طلب النشر منه.",
"team_is_unpublished_description": "رابط هذا {{entity}} غير متاح حاليًا. يرجى الاتصال بمالك {{entity}} أو طلب نشره منه.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "تعذر تنفيذ عملية الدفع من البطاقة.",
"insights": "رؤى",
"routing_forms": "نماذج التوجيه",
"testing_workflow_info_message": "عند اختبار سير العمل هذا، يجب أن تدرك أنه لا يمكن جدولة رسائل البريد الإلكتروني والرسائل القصيرة إلا قبل ساعة واحدة على الأقل",
"insights_no_data_found_for_filter": "لم يتم العثور على بيانات لعامل التصفية المحدد أو التواريخ المحددة.",
"acknowledge_booking_no_show_fee": "أقر بأنه إذا لم أحضر هذا الحدث فسيتم سحب رسوم عدم حضور من بطاقتي تبلغ {{amount, currency}}.",
"days": "أيام",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "الفريق: {{teamName}}",
"insights_user_filter": "المستخدم: {{userName}}",
"insights_subtitle": "عرض Insights الحجز عبر أحداثك",
"call_history": "سجل المكالمات",
"call_history_subtitle": "عرض سجل المكالمات عبر مكالمات Cal.ai الخاصة بك",
"location_options": "خيارات الموقع {{locationCount}}",
"channel_type": "نوع القناة",
"end_reason": "سبب الإنهاء",
"session_status": "حالة الجلسة",
"user_sentiment": "شعور المستخدم",
"time_header": "الوقت",
"from_header": "من",
"custom_plan": "الخطة المخصصة",
"email_embed": "البريد الإلكتروني المضمن",
"add_times_to_your_email": "حدّد بعض الفترات المتاحة وأدرجها في رسالتك الإلكترونية",
@@ -2752,6 +2782,8 @@
"account_already_linked": "الحساب مرتبط بالفعل",
"send_email": "إرسال بريد إلكتروني",
"cal_ai_phone_call_action": "الاتصال بالحاضر باستخدام وكيل Cal.ai الصوتي",
"call_to_confirm_booking": "اتصال لتأكيد الحجز",
"cal_ai_phone_call_action_description": "قبل ساعتين من بدء الحدث",
"cal_ai_agent_configuration": "إعدادات وكيل Cal.ai",
"choose_at_least_one_event_type_test_call": "يرجى اختيار نوع حدث واحد على الأقل لإجراء مكالمة اختبارية.",
"mark_as_no_show": "وضع علامة عدم الحضور",
@@ -3203,9 +3235,15 @@
"verify_email_change": "تأكيد تغيير البريد الإلكتروني",
"buy_credits": "شراء رصيد",
"credits": "الرصيد",
"view_and_manage_credits": "عرض وإدارة الرصيد",
"credits_used": "الرصيد المستخدم",
"total_credits_remaining": "الرصيد المتبقي الإجمالي",
"credits_per_tip_org": "تحصل على 1000 رصيد شهريًا لكل عضو في الفريق",
"credits_per_tip_teams": "تحصل على 750 رصيد شهريًا لكل عضو في الفريق",
"view_and_manage_credits": "عرض وإدارة الرصيد لإرسال رسائل SMS",
"view_and_manage_credits_description": "عرض وإدارة الرصيد لإرسال رسائل SMS. قيمة الرصيد الواحد هي 1¢ (دولار أمريكي). <0>معرفة المزيد</0>",
"credit_worth_description": "الرصيد الواحد يساوي 1 سنت (دولار أمريكي). <0>تعرف على المزيد</0>",
"buy_additional_credits": "شراء رصيد إضافي (0.01 دولار لكل رصيد)",
"view_additional_credits_expense_tip": "يمكنك عرض إنفاق الرصيد الإضافي في سجل النفقات الخاص بك",
"overview": "نظرة عامة",
"organization_slug_taken": "الاسم المختصر للمؤسسة مستخدم بالفعل",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "لا يمكنك إنشاء مؤسسة لأنك بالفعل جزء من مؤسسة",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "لقد نفد رصيد فريق Cal.com الخاص بك {{teamName}}. نتيجة لذلك، يتم الآن إرسال رسائل SMS عبر البريد الإلكتروني بدلاً من ذلك. لاستئناف إرسال SMS، يرجى شراء رصيد إضافي.",
"credit_limit_reached_message_user": "لقد نفدت رصيد حسابك على Cal.com. نتيجة لذلك، يتم الآن إرسال رسائل SMS عبر البريد الإلكتروني بدلاً من ذلك. لاستئناف إرسال رسائل SMS، يرجى شراء رصيد إضافي.",
"current_credit_balance": "الرصيد الحالي: {{balance}} رصيد",
"current_balance": "الرصيد الحالي:",
"notification_about_your_booking": "إشعار بخصوص حجزك",
"monthly_credits": "الرصيد الشهري",
"total_credits": "إجمالي الرصيد: {{totalCredits}}",
"remaining_credits": "الرصيد المتبقي: {{remainingCredits}}",
"remaining": "المتبقي",
"total": "الإجمالي",
"additional_credits": "رصيد إضافي",
"routing_form_next_in_queue": "{{count}} التالي في قائمة الانتظار",
"routing_form_select_members_to_email": "إرسال ردود البريد الإلكتروني إلى",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "عرض سير العمل الحالي وإعداداته",
"pbac_desc_update_workflows": "تعديل وتغيير إعدادات سير العمل",
"pbac_desc_delete_workflows": "إزالة سير العمل من النظام",
"pbac_resource_webhook": "ويب هوك",
"pbac_desc_create_webhooks": "إنشاء ويب هوكس",
"pbac_desc_view_webhooks": "عرض ويب هوكس",
"pbac_desc_update_webhooks": "تحديث الويب هوك",
"pbac_desc_delete_webhooks": "حذف الويب هوك",
"pbac_desc_manage_workflows": "وصول كامل لإدارة جميع سير العمل",
"pbac_desc_create_event_types": "إنشاء أنواع الأحداث",
"pbac_desc_view_event_types": "عرض أنواع الأحداث",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "اسم حدث المشغل (مثل BOOKING_CREATED، BOOKING_CANCELLED)",
"webhook_created_at": "وقت إنشاء الويب هوك",
"webhook_type": "رابط نوع الحدث",
"set_up_agent": "إعداد الوكيل",
"webhook_title": "اسم نوع الحدث",
"webhook_start_time": "وقت بدء الحدث",
"webhook_end_time": "وقت انتهاء الحدث",
@@ -3625,6 +3672,9 @@
"visit": "زيارة",
"location_custom_label_input_label": "تسمية مخصصة على صفحة الحجز",
"meeting_link": "رابط الاجتماع",
"session_outcome": "نتيجة الجلسة",
"call_created": "تم إنشاء المكالمة",
"voicemail": "البريد الصوتي",
"my_bookings": "حجوزاتي",
"phone": "الهاتف",
"free": "مجاني",
@@ -3632,6 +3682,8 @@
"user_name": "اسم المستخدم",
"expand_panel": "توسيع اللوحة",
"collapse_panel": "طي اللوحة",
"email_verification_required": "التحقق من البريد الإلكتروني مطلوب لهذا النوع من الأحداث",
"invalid_verification_code": "تم تقديم رمز تحقق غير صالح",
"you_have_one_team": "لديك فريق واحد",
"consider_consolidating_one_team_org": "فكر في إنشاء مؤسسة لتوحيد الفواتير وأدوات الإدارة والتحليلات عبر فريقك.",
"consider_consolidating_multi_team_org": "فكر في إنشاء مؤسسة لتوحيد الفواتير وأدوات الإدارة والتحليلات عبر فرقك.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "قبل وقت البدء المحدد",
"cancel_booking_acknowledge_no_show_fee": "أقر بأنه عند إلغاء الحجز قبل {{timeValue}} {{timeUnit}} من وقت البدء سيتم فرض رسوم التخلف عن الحضور بقيمة {{amount, currency}}",
"contact_organizer": "إذا كانت لديك أي أسئلة، يرجى التواصل مع المنظم.",
"booking_time_option": "وقت الحجز",
"booking_time_option_description": "عندما يتم جدولة الحجز (من البداية إلى النهاية)",
"created_at_option": "تم الإنشاء في",
"created_at_option_description": "عندما تم إنشاء الحجز في الأصل",
"call_details": "تفاصيل المكالمة",
"call_id": "معرف المكالمة",
"call_information": "معلومات المكالمة",
"sentiment": "المشاعر",
"disconnect_reason": "سبب الانقطاع",
"call_summary": "ملخص المكالمة",
"transcription": "النسخ",
"event_details": "تفاصيل الحدث",
"agent": "الوكيل",
"no_transcript_available": "لا يوجد نص متاح",
"testing_sms_workflow_info_message": "عند اختبار سير العمل هذا، انتبه إلى أنه يجب جدولة الرسائل النصية القصيرة قبل 15 دقيقة على الأقل",
"start_from_scratch_title": "البدء من الصفر",
"start_from_scratch_description": "إنشاء سير العمل الخاص بك من الصفر.",
"cal_ai_template_title": "قالب Cal.ai",
"cal_ai_template_description": "وكلاء الذكاء الاصطناعي الذين يحجزون الاجتماعات، ويرسلون التذكيرات، ويتابعون!",
"voice": "الصوت",
"select_voice": "اختر الصوت",
"select_voice_for_agent": "اختر صوتًا لوكيلك",
"choose_a_voice_for_your_agent": "اختر صوتًا لوكيلك",
"trait": "سمة",
"voice_id": "معرّف الصوت",
"use_voice": "استخدم الصوت",
"current_voice": "الصوت الحالي",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+84 -5
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "Ən yaxşı e-poçt və təqvim çatdırılmasını təmin etmək üçün e-poçt ünvanınızı təsdiqləyin",
"verify_email_email_header": "E-poçt ünvanınızı təsdiqləyin",
"verify_email_button": "E-poçtu təsdiqlə",
"cal_ai_assistant": "Köməkçisi",
"cal_ai_assistant": "Köməkçi",
"send_cal_video_transcription_emails": "Cal Video transkript e-poçtlarını göndər",
"description_send_cal_video_transcription_emails": "Görüş bitdikdən sonra Cal Video transkripsiyası olan e-poçtları göndərin. (Ödənişli plan tələb olunur)",
"verify_email_change_description": "Son zamanlarda {{appName}} hesabınıza daxil olmaq üçün istifadə etdiyiniz e-poçt ünvanını dəyişdirmək istədiyinizi bildirdiniz. Yeni e-poçt ünvanınızı təsdiqləmək üçün aşağıdakı düyməni basın.",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "İş axını doğrulaması uğursuz oldu",
"workflow_validation_empty_fields": "Bir və ya daha çox iş axını addımında boş mesaj məzmunu var",
"workflow_validation_unverified_contacts": "Bir və ya daha çox telefon nömrəsi və ya e-poçt ünvanı təsdiqlənməyib",
"supercharge_your_workflows_with_cal_ai": "Cal.ai ilə İş Axınlarınızı Gücləndirin",
"supercharge_your_workflows_with_cal_ai_description": "Görüşləri təyin edən, xatırlatmalar göndərən və müştərilərinizlə əlaqə saxlayan canlı AI agentləri.",
"phone_number_imported_successfully": "Telefon nömrəsi uğurla idxal edildi və agentə bağlandı",
"phone_number_deleted_successfully": "Telefon nömrəsi uğurla silindi",
"delete_phone_number_confirmation": "Bu telefon nömrəsini silmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "Telefon nömrəsi abunəliyi uğurla ləğv edildi",
"updating": "Yenilənir",
"round_robin": "Round Robin",
"hi_how_are_you_doing": "Salam, necəsən?",
"round_robin_description": "Görüşləri bir neçə komanda üzvü arasında dövr edin.",
"managed_event": "İdarə olunan Tədbir",
"username_placeholder": "istifadəçi adı",
@@ -878,7 +881,7 @@
"the_action_will_disconnect_phone_number": "Bu əməliyyat telefon nömrəsini agentdən ayıracaq. Yeni telefon nömrəsi qoşulana qədər agent zəng edə bilməyəcək.",
"cal_ai_phone_numbers": "Telefon Nömrələri",
"connect_phone_number": "Telefon Nömrəsini Qoş",
"cal_ai_phone_numbers_description": "Telefon Nömrələrinizi idarə edin",
"cal_ai_phone_numbers_description": "Telefon Nömrələrinizi İdarə edin",
"import_number": "Nömrəni İdxal et",
"this_action_will_also": "Bu əməliyyat həmçinin:",
"import_phone_number": "Telefon Nömrəsini İdxal et",
@@ -886,7 +889,7 @@
"cancel_your_phone_number_subscription": "Telefon nömrəsi abunəliyinizi ləğv edin",
"delete_associated_phone_number": "Əlaqəli telefon nömrəsini silin",
"unauthorized_create_workflow": "Bu iş axınını yaratmaq üçün səlahiyyətiniz yoxdur",
"import_phone_number_description": "Phone ilə istifadə etmək üçün Twilio telefon nömrənizi idxal edin",
"import_phone_number_description": "Telefon ilə istifadə etmək üçün Twilio telefon nömrənizi idxal edin",
"phone_number_cost": "${{price}}/ay",
"buy_new_number": "Yeni Nömrə Alın",
"buy_number_cost_x_per_month": "Telefon nömrəsi almaq ayda ${{priceInDollars}} məbləğində başa gəlir. Hər aktiv telefon nömrəsi üçün aylıq ödəniş edəcəksiniz.",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "Bəli, Abunəliyi Ləğv Et",
"cancel_phone_number_subscription_confirmation": "Bu telefon nömrəsi abunəliyini ləğv etmək istədiyinizə əminsiniz? Bu əməliyyat geri qaytarıla bilməz və bu telefon nömrəsinə girişinizi itirəcəksiniz.",
"add_members": "Üzvlər əlavə et...",
"add_members_no_ellipsis": "Üzvləri əlavə et",
"no_assigned_members": "Təyin edilmiş üzvlər yoxdur",
"assigned_to": "Təyin edilmiş",
"you_must_be_logged_in_to": "{{url}}-ə daxil olmalısınız",
@@ -1207,6 +1211,7 @@
"categories": "Kateqoriyalar",
"pricing": "Qiymətlər",
"learn_more": "Daha çox öyrən",
"try_now": "İndi cəhd edin",
"privacy_policy": "Məxfilik Siyasəti",
"terms_of_service": "Xidmət Şərtləri",
"remove": "Sil",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "iştirakçıya WhatsApp mesajı göndərin",
"workflows": "İş axınları",
"new_workflow_btn": "Yeni İş Axını",
"how_would_you_like_to_start": "Necə başlamaq istəyirsiniz?",
"add_new_workflow": "Yeni iş axını əlavə et",
"reschedule_event_trigger": "hadisə yenidən planlaşdırıldıqda",
"trigger": "Tətik",
@@ -1716,6 +1722,8 @@
"event_duration_info": "Tədbirin müddəti",
"event_time_info": "Tədbirin başlama vaxtı",
"event_type_not_found": "EventType tapılmadı",
"number_to_call_variable": "Zəng ediləcək nömrə",
"number_to_call_info": "Zəng etdiyiniz istifadəçinin telefon nömrəsi",
"location_variable": "Məkan",
"location_info": "Tədbirin yeri",
"additional_notes_variable": "Əlavə qeydlər",
@@ -1753,6 +1761,7 @@
"team_url": "Komanda URL",
"team_members": "Komanda üzvləri",
"more": "Daha çox",
"cal_ai_workflows": "Cal.ai İş Axınları",
"and_count_more": "və {{count}} daha",
"more_page_footer": "Mobil tətbiqi veb tətbiqinin bir uzantısı kimi görürük. Hər hansı mürəkkəb əməliyyatları yerinə yetirirsinizsə, lütfən, veb tətbiqinə qayıdın.",
"workflow_example_1": "Tədbir başlamazdan 24 saat əvvəl iştirakçıya SMS xatırlatma göndərin",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "Tədbir başlamazdan 1 saat əvvəl iştirakçıya e-poçt xatırlatma göndərin",
"workflow_example_5": "Tədbir təxirə salındıqda ev sahibinə xüsusi e-poçt göndərin",
"workflow_example_6": "Yeni tədbir sifariş edildikdə ev sahibinə xüsusi SMS göndərin",
"send_sms_reminder": "SMS xatırlatması göndər",
"send_sms_reminder_description": "Tədbir başlamazdan 24 saat əvvəl",
"follow_up_with_no_shows": "Gəlməyənlərlə əlaqə saxla",
"follow_up_with_no_shows_description": "Tədbir bitdikdən 30 dəqiqə sonra",
"remind_attendees_to_bring_id": "İştirakçılara şəxsiyyət vəsiqəsi gətirməyi xatırlat",
"remind_attendees_to_bring_id_description": "Tədbir başlamazdan 1 gün əvvəl",
"email_to_remind_booking": "E-poçt Xatırlatması",
"email_to_remind_booking_description": "Tədbir başlamazdan 1 saat əvvəl",
"custom_sms_reminder": "Fərdi SMS Xatırlatması",
"custom_sms_reminder_description": "Tədbir planlaşdırıldıqda",
"custom_email_reminder": "Fərdi E-poçt Xatırlatması",
"custom_email_reminder_description": "Tədbir ev sahibi üçün yenidən planlaşdırıldıqda",
"count_managed_to_limit": "İdarə olunan tədbir növlərindən rezervasiya saylarını daxil edin",
"welcome_to_cal_header": "{{appName}}-a xoş gəlmisiniz!",
"edit_form_later_subtitle": "Bunu sonra redaktə edə biləcəksiniz.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "Rezervasiya səhifəsində göstər",
"visit_cancelled_booking": "Ləğv edilmiş rezervasiya səhifəsinə baxa bilərsiniz",
"get_started_zapier_templates": "Zapier şablonları ilə başlayın",
"standard_templates": "Standart Şablonlar",
"cal_ai_templates": "Cal.ai Şablonları",
"team_is_unpublished": "{{team}} yayımlanmayıb",
"org_is_unpublished_description": "Bu təşkilat bağlantısı hazırda mövcud deyil. Zəhmət olmasa təşkilat sahibinə müraciət edin və ya onu yayımlamasını xahiş edin.",
"team_is_unpublished_description": "Bu komanda bağlantısı hazırda mövcud deyil. Zəhmət olmasa komanda sahibinə müraciət edin və ya onu yayımlamasını xahiş edin.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "Ödəniş üçün kartdan pul çıxıla bilmədi.",
"insights": "Görüşlər",
"routing_forms": "Marşrutlaşdırma Formları",
"testing_workflow_info_message": "Bu iş axınını sınaqdan keçirərkən, e-poçtların və SMS-lərin ən azı 1 saat əvvəl planlaşdırıla biləcəyini unutmayın",
"insights_no_data_found_for_filter": "Seçilmiş filtr və ya tarixlər üçün məlumat tapılmadı.",
"acknowledge_booking_no_show_fee": "Bu tədbirə qatılmasam, kartıma {{amount, currency}} gəlməmə haqqı tətbiq olunacağını qəbul edirəm.",
"days": "günlər",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "Komanda: {{teamName}}",
"insights_user_filter": "İstifadəçi: {{userName}}",
"insights_subtitle": "Tədbirləriniz üzrə rezervasiya məlumatlarını görün",
"call_history": "Zəng Tarixçəsi",
"call_history_subtitle": "Cal.ai zəngləriniz üzrə zəng tarixçəsinə baxın",
"location_options": "{{locationCount}} məkan seçimi",
"channel_type": "Kanal Növü",
"end_reason": "Bitmə Səbəbi",
"session_status": "Sessiya Statusu",
"user_sentiment": "İstifadəçi Münasibəti",
"time_header": "Vaxt",
"from_header": "Kimdən",
"custom_plan": "Xüsusi Plan",
"email_embed": "Email Əlavəsi",
"add_times_to_your_email": "Mövcud vaxtları seçin və Email-ə əlavə edin",
@@ -2752,6 +2782,8 @@
"account_already_linked": "Hesab artıq əlaqələndirilib",
"send_email": "E-poçt göndərin",
"cal_ai_phone_call_action": "Cal.ai Səs Agenti vasitəsilə iştirakçıya zəng edin",
"call_to_confirm_booking": "Rezervasiyanı təsdiqləmək üçün zəng edin",
"cal_ai_phone_call_action_description": "Tədbir başlamazdan 2 saat əvvəl",
"cal_ai_agent_configuration": "Cal.ai Agent Konfiqurasiyası",
"choose_at_least_one_event_type_test_call": "Test zəngi etmək üçün ən azı bir tədbir növü seçin.",
"mark_as_no_show": "Gəlməyən kimi işarələyin",
@@ -3203,9 +3235,15 @@
"verify_email_change": "E-poçt dəyişikliyini təsdiqlə",
"buy_credits": "Kredit alın",
"credits": "Kreditlər",
"view_and_manage_credits": "Kreditləri görüntüləyin və idarə edin",
"credits_used": "İstifadə edilmiş kreditlər",
"total_credits_remaining": "Ümumi qalan",
"credits_per_tip_org": "Hər ay, hər komanda üzvü üçün 1000 kredit əldə edirsiniz",
"credits_per_tip_teams": "Hər ay, hər komanda üzvü üçün 750 kredit əldə edirsiniz",
"view_and_manage_credits": "SMS mesajları göndərmək üçün kreditlərə baxın və idarə edin",
"view_and_manage_credits_description": "SMS mesajları göndərmək üçün kreditləri görüntüləyin və idarə edin. Bir kredit 1¢ (USD) dəyərindədir. <0>Daha ətraflı</0>",
"credit_worth_description": "Bir kredit 1¢ (USD) dəyərindədir. <0>Daha ətraflı öyrənin</0>",
"buy_additional_credits": "Əlavə kreditlər alın (kredit başına $0.01)",
"view_additional_credits_expense_tip": "Əlavə kredit xərclərini xərc jurnalınızda görə bilərsiniz",
"overview": "Ümumi baxış",
"organization_slug_taken": "Təşkilat slaqı artıq istifadə olunub",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Siz artıq bir təşkilatın üzvü olduğunuz üçün yeni təşkilat yarada bilməzsiniz",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "Cal.com {{teamName}} komandanızın kreditləri bitdi. Nəticədə, SMS mesajları indi e-poçt vasitəsilə göndərilir. SMS göndərməyə davam etmək üçün, zəhmət olmasa əlavə kreditlər alın.",
"credit_limit_reached_message_user": "Cal.com hesabınızın kreditləri bitib. Nəticə olaraq, SMS mesajları artıq e-poçt vasitəsilə göndərilir. SMS göndərməyə davam etmək üçün, zəhmət olmasa əlavə kreditlər alın.",
"current_credit_balance": "Cari balans: {{balance}} kredit",
"current_balance": "Cari balans:",
"notification_about_your_booking": "Rezervasiyanız haqqında bildiriş",
"monthly_credits": "Aylıq kreditlər",
"total_credits": "Ümumi kreditlər: {{totalCredits}}",
"remaining_credits": "Qalan kreditlər: {{remainingCredits}}",
"remaining": "Qalan",
"total": "Ümumi",
"additional_credits": "Əlavə kreditlər",
"routing_form_next_in_queue": "Növbədə {{count}} növbəti",
"routing_form_select_members_to_email": "E-poçt cavablarını göndərmək üçün üzvləri seçin",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "Mövcud iş axınlarına və onların konfiqurasiyalarına baxış",
"pbac_desc_update_workflows": "İş axını parametrlərini redaktə etmək və dəyişdirmək",
"pbac_desc_delete_workflows": "İş axınlarını sistemdən silmək",
"pbac_resource_webhook": "Webhook",
"pbac_desc_create_webhooks": "Webhook yaradın",
"pbac_desc_view_webhooks": "Webhook-lara baxın",
"pbac_desc_update_webhooks": "Webhook-ləri yeniləyin",
"pbac_desc_delete_webhooks": "Webhook-ləri silin",
"pbac_desc_manage_workflows": "Bütün iş axınlarına tam idarəetmə girişi",
"pbac_desc_create_event_types": "Tədbir növləri yaratmaq",
"pbac_desc_view_event_types": "Tədbir növlərinə baxmaq",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "Tətikləyici hadisənin adı (məs., BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "Webhook yaradılma vaxtı",
"webhook_type": "Tədbir növünün slaqı",
"set_up_agent": "Agent qurun",
"webhook_title": "Tədbir növünün adı",
"webhook_start_time": "Tədbirin başlama vaxtı",
"webhook_end_time": "Tədbirin bitmə vaxtı",
@@ -3625,6 +3672,9 @@
"visit": "Ziyarət et",
"location_custom_label_input_label": "Rezervasiya səhifəsində xüsusi etiket",
"meeting_link": "Görüş linki",
"session_outcome": "Sessiya nəticəsi",
"call_created": "Zəng yaradıldı",
"voicemail": "Səsli poçt",
"my_bookings": "Mənim Rezervasiyalarım",
"phone": "Telefon",
"free": "Pulsuz",
@@ -3632,6 +3682,8 @@
"user_name": "İstifadəçi Adı",
"expand_panel": "Paneli Genişlət",
"collapse_panel": "Paneli Yığ",
"email_verification_required": "Bu tədbir növü üçün e-poçt doğrulaması tələb olunur",
"invalid_verification_code": "Yanlış doğrulama kodu təqdim edilib",
"you_have_one_team": "Bir komandanız var",
"consider_consolidating_one_team_org": "Komandanız üzrə ödənişləri, admin alətlərini və analitikanı birləşdirmək üçün təşkilat qurmağı nəzərdən keçirin.",
"consider_consolidating_multi_team_org": "Komandalarınız üzrə ödənişləri, admin alətlərini və analitikanı birləşdirmək üçün təşkilat qurmağı nəzərdən keçirin.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "Planlaşdırılmış başlama vaxtından əvvəl",
"cancel_booking_acknowledge_no_show_fee": "Təsdiq edirəm ki, rezervasiyanı başlama vaxtından {{timeValue}} {{timeUnit}} ərzində ləğv etməklə mənə {{amount, currency}} məbləğində gəlməmə haqqı tutulacaq",
"contact_organizer": "Hər hansı sualınız varsa, təşkilatçı ilə əlaqə saxlayın.",
"booking_time_option": "Rezervasiya vaxtı",
"booking_time_option_description": "Rezervasiyanın planlaşdırıldığı vaxt (başlanğıcdan sona)",
"created_at_option": "Yaradılma tarixi",
"created_at_option_description": "Rezervasiyanın ilkin yaradıldığı vaxt",
"call_details": "Zəng detalları",
"call_id": "Zəng ID",
"call_information": "Zəng məlumatı",
"sentiment": "Əhval-ruhiyyə",
"disconnect_reason": "Bağlantının kəsilmə səbəbi",
"call_summary": "Zəng xülasəsi",
"transcription": "Transkripsiya",
"event_details": "Tədbir detalları",
"agent": "Agent",
"no_transcript_available": "Transkript mövcud deyil",
"testing_sms_workflow_info_message": "Bu iş axınını sınaqdan keçirərkən, SMS-lərin ən azı 15 dəqiqə əvvəlcədən planlaşdırılmalı olduğunu nəzərə alın",
"start_from_scratch_title": "Sıfırdan başlayın",
"start_from_scratch_description": "Öz iş axınınızı sıfırdan yaradın.",
"cal_ai_template_title": "Cal.ai şablonu",
"cal_ai_template_description": "Görüşləri təyin edən, xatırlatmalar göndərən və davam etdirən AI agentləri!",
"voice": "Səs",
"select_voice": "Səs seçin",
"select_voice_for_agent": "Agentiniz üçün səs seçin",
"choose_a_voice_for_your_agent": "Agentiniz üçün səs seçin",
"trait": "Xüsusiyyət",
"voice_id": "Səs ID",
"use_voice": "Səsdən istifadə edin",
"current_voice": "Cari səs",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+83 -4
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "Проверете имейл адреса си, за да гарантирате най-добрата доставка на имейли и календари",
"verify_email_email_header": "Потвърдете вашия имейл адрес",
"verify_email_button": "Потвърди имейл",
"cal_ai_assistant": "асистент",
"cal_ai_assistant": "Асистент",
"send_cal_video_transcription_emails": "Изпращане на имейли с транскрипция на Cal Video",
"description_send_cal_video_transcription_emails": "Изпращане на имейли с транскрипцията на Cal Video след края на срещата. (Изисква платен план)",
"verify_email_change_description": "Наскоро поискахте да промените имейл адреса, който използвате за вход във вашия {{appName}} акаунт. Моля, кликнете върху бутона по-долу, за да потвърдите новия си имейл адрес.",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "Валидацията на работния процес е неуспешна",
"workflow_validation_empty_fields": "Една или повече стъпки от работния процес имат празно съдържание на съобщението",
"workflow_validation_unverified_contacts": "Един или повече телефонни номера или имейл адреси не са потвърдени",
"supercharge_your_workflows_with_cal_ai": "Подсилете вашите работни процеси с Cal.ai",
"supercharge_your_workflows_with_cal_ai_description": "Реалистични AI агенти, които резервират срещи, изпращат напомняния и проследяват вашите клиенти.",
"phone_number_imported_successfully": "Телефонният номер е импортиран и свързан с агента успешно",
"phone_number_deleted_successfully": "Телефонният номер е изтрит успешно",
"delete_phone_number_confirmation": "Сигурни ли сте, че искате да изтриете този телефонен номер? Това действие не може да бъде отменено.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "Абонаментът за телефонен номер е отменен успешно",
"updating": "Обновяване",
"round_robin": "Round Robin",
"hi_how_are_you_doing": "Здравейте, как сте?",
"round_robin_description": "Разпределяйте срещи между множество членове на екипа.",
"managed_event": "Управлявано събитие",
"username_placeholder": "потребителско име",
@@ -876,7 +879,7 @@
"are_you_sure_you_want_to_delete_workflow_step": "Сигурни ли сте, че искате да изтриете тази стъпка от работния процес?",
"do_you_still_want_to_unsubscribe": "Все още ли искате да прекратите абонамента за този телефонен номер от този агент?",
"the_action_will_disconnect_phone_number": "Това действие ще прекъсне връзката на телефонния номер с агента. Агентът няма да може да осъществява обаждания, докато не бъде свързан нов телефонен номер.",
"cal_ai_phone_numbers": "телефонни номера",
"cal_ai_phone_numbers": "Телефонни номера",
"connect_phone_number": "Свързване на телефонен номер",
"cal_ai_phone_numbers_description": "Управлявайте вашите телефонни номера",
"import_number": "Импортиране на номер",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "Да, отказвам абонамента",
"cancel_phone_number_subscription_confirmation": "Сигурни ли сте, че искате да прекратите абонамента за този телефонен номер? Това действие не може да бъде отменено и ще загубите достъп до този телефонен номер.",
"add_members": "Добави членове...",
"add_members_no_ellipsis": "Добави членове",
"no_assigned_members": "Няма назначени членове",
"assigned_to": "Назначено на",
"you_must_be_logged_in_to": "Трябва да сте влезли в профила си, за да {{url}}",
@@ -1207,6 +1211,7 @@
"categories": "Категории",
"pricing": "Цени",
"learn_more": "Научи повече",
"try_now": "Пробвайте сега",
"privacy_policy": "Политика за поверителност",
"terms_of_service": "Общи условия",
"remove": "Премахни",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "изпращане на WhatsApp съобщение до участника",
"workflows": "Работни процеси",
"new_workflow_btn": "Нов работен процес",
"how_would_you_like_to_start": "Как бихте искали да започнете?",
"add_new_workflow": "Добавяне на нов работен процес",
"reschedule_event_trigger": "когато събитието е пренасрочено",
"trigger": "Тригер",
@@ -1716,6 +1722,8 @@
"event_duration_info": "Продължителността на събитието",
"event_time_info": "Началният час на събитието",
"event_type_not_found": "Типът събитие не е намерен",
"number_to_call_variable": "Номер за обаждане",
"number_to_call_info": "Телефонният номер на потребителя, на когото се обаждате",
"location_variable": "Локация",
"location_info": "Локацията на събитието",
"additional_notes_variable": "Допълнителни бележки",
@@ -1753,6 +1761,7 @@
"team_url": "URL на екипа",
"team_members": "Членове на екипа",
"more": "Още",
"cal_ai_workflows": "Cal.ai работни процеси",
"and_count_more": "и още {{count}}",
"more_page_footer": "Разглеждаме мобилното приложение като разширение на уеб приложението. Ако извършвате сложни действия, моля, върнете се към уеб приложението.",
"workflow_example_1": "Изпрати SMS напомняне 24 часа преди началото на събитието до участника",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "Изпрати имейл напомняне 1 час преди началото на събитията до участника",
"workflow_example_5": "Изпрати персонализиран имейл до домакина, когато събитието е пренасрочено",
"workflow_example_6": "Изпрати персонализиран SMS до домакина, когато е резервирано ново събитие",
"send_sms_reminder": "Изпращане на SMS напомняне",
"send_sms_reminder_description": "24 часа преди началото на събитието",
"follow_up_with_no_shows": "Проследяване на неявилите се",
"follow_up_with_no_shows_description": "30 минути след края на събитието",
"remind_attendees_to_bring_id": "Напомняне на участниците да носят лична карта",
"remind_attendees_to_bring_id_description": "1 ден преди началото на събитието",
"email_to_remind_booking": "Имейл напомняне",
"email_to_remind_booking_description": "1 час преди началото на събитието",
"custom_sms_reminder": "Персонализирано SMS напомняне",
"custom_sms_reminder_description": "Когато събитието е планирано",
"custom_email_reminder": "Персонализирано имейл напомняне",
"custom_email_reminder_description": "Събитието е пренасрочено за домакина",
"count_managed_to_limit": "Включване на броя резервации от управляваните типове събития",
"welcome_to_cal_header": "Добре дошли в {{appName}}!",
"edit_form_later_subtitle": "Ще можете да редактирате това по-късно.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "Покажи на страницата за резервации",
"visit_cancelled_booking": "Можете да посетите страницата с отменената резервация",
"get_started_zapier_templates": "Започнете със Zapier шаблони",
"standard_templates": "Стандартни шаблони",
"cal_ai_templates": "Cal.ai шаблони",
"team_is_unpublished": "{{team}} не е публикуван",
"org_is_unpublished_description": "Тази връзка към организацията в момента не е достъпна. Моля, свържете се със собственика на организацията или го помолете да я публикува.",
"team_is_unpublished_description": "Тази връзка към екипа в момента не е достъпна. Моля, свържете се със собственика на екипа или го помолете да я публикува.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "Не можа да се извърши плащане с картата.",
"insights": "Анализи",
"routing_forms": "Формуляри за маршрутизиране",
"testing_workflow_info_message": "Когато тествате този работен процес, имайте предвид, че имейлите и SMS съобщенията могат да бъдат планирани само поне 1 час предварително",
"insights_no_data_found_for_filter": "Не са намерени данни за избрания филтър или избраните дати.",
"acknowledge_booking_no_show_fee": "Потвърждавам, че ако не присъствам на това събитие, ще бъде начислена такса за неявяване в размер на {{amount, currency}} на моята карта.",
"days": "дни",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "Екип: {{teamName}}",
"insights_user_filter": "Потребител: {{userName}}",
"insights_subtitle": "Преглед на статистики за резервации за всички ваши събития",
"call_history": "История на обажданията",
"call_history_subtitle": "Преглед на историята на обажданията през вашите Cal.ai разговори",
"location_options": "{{locationCount}} опции за локация",
"channel_type": "Тип канал",
"end_reason": "Причина за край",
"session_status": "Статус на сесията",
"user_sentiment": "Настроение на потребителя",
"time_header": "Време",
"from_header": "От",
"custom_plan": "Персонализиран план",
"email_embed": "Вграждане в имейл",
"add_times_to_your_email": "Изберете няколко свободни часа и ги вградете във вашия имейл",
@@ -2752,6 +2782,8 @@
"account_already_linked": "Профилът вече е свързан",
"send_email": "Изпрати имейл",
"cal_ai_phone_call_action": "Обаждане до участника чрез Cal.ai гласов агент",
"call_to_confirm_booking": "Обаждане за потвърждаване на резервация",
"cal_ai_phone_call_action_description": "2 часа преди началото на събитието",
"cal_ai_agent_configuration": "Конфигурация на Cal.ai агент",
"choose_at_least_one_event_type_test_call": "Моля, изберете поне един тип събитие, за да направите тестово обаждане.",
"mark_as_no_show": "Маркирай като неявил се",
@@ -3203,9 +3235,15 @@
"verify_email_change": "Потвърди промяната на имейл",
"buy_credits": "Купете кредити",
"credits": "Кредити",
"view_and_manage_credits": "Преглед и управление на кредити",
"credits_used": "Използвани кредити",
"total_credits_remaining": "Общо оставащи",
"credits_per_tip_org": "Получавате 1000 кредита на месец за всеки член на екипа",
"credits_per_tip_teams": "Получавате 750 кредита на месец за всеки член на екипа",
"view_and_manage_credits": "Преглед и управление на кредити за изпращане на SMS съобщения",
"view_and_manage_credits_description": "Преглед и управление на кредити за изпращане на SMS съобщения. Един кредит струва 1¢ (USD). <0>Научете повече</0>",
"credit_worth_description": "Един кредит струва 1¢ (USD). <0>Научете повече</0>",
"buy_additional_credits": "Купете допълнителни кредити ($0.01 на кредит)",
"view_additional_credits_expense_tip": "Можете да видите допълнителните разходи за кредити в дневника на разходите си",
"overview": "Общ преглед",
"organization_slug_taken": "Слъгът на организацията вече е зает",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Не можете да създадете организация, тъй като вече сте част от организация",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "Вашият Cal.com екип {{teamName}} е изчерпал кредитите си. В резултат на това SMS съобщенията сега се изпращат чрез имейл. За да възобновите изпращането на SMS, моля, закупете допълнителни кредити.",
"credit_limit_reached_message_user": "Кредитите на вашия Cal.com акаунт са изчерпани. В резултат на това SMS съобщенията сега се изпращат чрез имейл. За да възобновите изпращането на SMS, моля, закупете допълнителни кредити.",
"current_credit_balance": "Текущ баланс: {{balance}} кредита",
"current_balance": "Текущ баланс:",
"notification_about_your_booking": "Известие за вашата резервация",
"monthly_credits": "Месечни кредити",
"total_credits": "Общо кредити: {{totalCredits}}",
"remaining_credits": "Оставащи кредити: {{remainingCredits}}",
"remaining": "Оставащи",
"total": "Общо",
"additional_credits": "Допълнителни кредити",
"routing_form_next_in_queue": "{{count}} следващи на опашката",
"routing_form_select_members_to_email": "Изпращане на имейл отговори до",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "Преглед на съществуващите работни процеси и техните конфигурации",
"pbac_desc_update_workflows": "Редактиране и модифициране на настройките на работните процеси",
"pbac_desc_delete_workflows": "Премахване на работни процеси от системата",
"pbac_resource_webhook": "Уебхук",
"pbac_desc_create_webhooks": "Създаване на уебхуци",
"pbac_desc_view_webhooks": "Преглед на уебхуци",
"pbac_desc_update_webhooks": "Актуализиране на уебхукове",
"pbac_desc_delete_webhooks": "Изтриване на уебхукове",
"pbac_desc_manage_workflows": "Пълен достъп за управление на всички работни процеси",
"pbac_desc_create_event_types": "Създаване на типове събития",
"pbac_desc_view_event_types": "Преглед на типове събития",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "Името на задействащото събитие (напр. BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "Времето на уебхука",
"webhook_type": "Слъгът на типа събитие",
"set_up_agent": "Настройка на агент",
"webhook_title": "Името на типа събитие",
"webhook_start_time": "Началното време на събитието",
"webhook_end_time": "Крайното време на събитието",
@@ -3625,6 +3672,9 @@
"visit": "Посети",
"location_custom_label_input_label": "Персонализиран етикет на страницата за резервация",
"meeting_link": "Линк за среща",
"session_outcome": "Резултат от сесията",
"call_created": "Създадено обаждане",
"voicemail": "Гласова поща",
"my_bookings": "Моите резервации",
"phone": "Телефон",
"free": "Безплатно",
@@ -3632,6 +3682,8 @@
"user_name": "Име на потребителя",
"expand_panel": "Разгъни панела",
"collapse_panel": "Свий панела",
"email_verification_required": "Изисква се имейл верификация за този тип събитие",
"invalid_verification_code": "Предоставен е невалиден код за верификация",
"you_have_one_team": "Имате един екип",
"consider_consolidating_one_team_org": "Помислете за създаване на организация, за да обедините таксуването, административните инструменти и анализите за вашия екип.",
"consider_consolidating_multi_team_org": "Помислете за създаване на организация, за да обедините таксуването, административните инструменти и анализите за вашите екипи.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "Преди планираното начално време",
"cancel_booking_acknowledge_no_show_fee": "Потвърждавам, че при отмяна на резервацията в рамките на {{timeValue}} {{timeUnit}} преди началния час ще ми бъде начислена такса за неявяване в размер на {{amount, currency}}",
"contact_organizer": "Ако имате въпроси, моля, свържете се с организатора.",
"booking_time_option": "Време на резервация",
"booking_time_option_description": "Когато резервацията е планирана (от начало до край)",
"created_at_option": "Създадено на",
"created_at_option_description": "Когато резервацията е първоначално създадена",
"call_details": "Детайли на обаждането",
"call_id": "ID на обаждането",
"call_information": "Информация за обаждането",
"sentiment": "Настроение",
"disconnect_reason": "Причина за прекъсване",
"call_summary": "Обобщение на обаждането",
"transcription": "Транскрипция",
"event_details": "Детайли на събитието",
"agent": "Агент",
"no_transcript_available": "Няма наличен транскрипт",
"testing_sms_workflow_info_message": "Когато тествате този работен процес, имайте предвид, че SMS трябва да бъдат планирани поне 15 минути предварително",
"start_from_scratch_title": "Започнете от нулата",
"start_from_scratch_description": "Създайте свой собствен работен процес от нулата.",
"cal_ai_template_title": "Cal.ai шаблон",
"cal_ai_template_description": "AI агенти, които резервират срещи, изпращат напомняния и проследяват!",
"voice": "Глас",
"select_voice": "Избери глас",
"select_voice_for_agent": "Избери глас за твоя агент",
"choose_a_voice_for_your_agent": "Избери глас за твоя агент",
"trait": "Характеристика",
"voice_id": "ID на глас",
"use_voice": "Използвай глас",
"current_voice": "Текущ глас",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+84 -5
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "সেরা ইমেল এবং ক্যালেন্ডার ডেলিভারিবিলিটি নিশ্চিত করতে আপনার ইমেল ঠিকানা যাচাই করুন। ",
"verify_email_email_header": "আপনার ইমেল ঠিকানা যাচাই করুন",
"verify_email_button": "ইমেল যাচাই করুন",
"cal_ai_assistant": "ক্যাল এআই সহকারী",
"cal_ai_assistant": "সহকারী",
"send_cal_video_transcription_emails": "ক্যাল ভিডিও ট্রান্সক্রিপশন ইমেল পাঠান",
"description_send_cal_video_transcription_emails": "মিটিং শেষ হওয়ার পর ক্যাল ভিডিওর ট্রান্সক্রিপশন সহ ইমেল পাঠান। (একটি পেইড প্ল্যান প্রয়োজন)",
"verify_email_change_description": "আপনি সম্প্রতি আপনার লগ ইন করতে ব্যবহার করা ইমেল ঠিকানা পরিবর্তন করার জন্য অনুরোধ করেছেন {{appName}} অ্যাকাউন্ট। ",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "ওয়ার্কফ্লো যাচাইকরণ ব্যর্থ হয়েছে",
"workflow_validation_empty_fields": "এক বা একাধিক ওয়ার্কফ্লো ধাপে খালি বার্তা বিষয়বস্তু রয়েছে",
"workflow_validation_unverified_contacts": "এক বা একাধিক ফোন নম্বর বা ইমেল ঠিকানা যাচাই করা হয়নি",
"supercharge_your_workflows_with_cal_ai": "Cal.ai দিয়ে আপনার ওয়ার্কফ্লো সুপারচার্জ করুন",
"supercharge_your_workflows_with_cal_ai_description": "জীবন্ত এআই এজেন্ট যারা মিটিং বুক করে, রিমাইন্ডার পাঠায়, এবং আপনার গ্রাহকদের সাথে ফলোআপ করে।",
"phone_number_imported_successfully": "ফোন নম্বর সফলভাবে আমদানি করা হয়েছে এবং এজেন্টের সাথে সংযুক্ত করা হয়েছে",
"phone_number_deleted_successfully": "ফোন নম্বর সফলভাবে মুছে ফেলা হয়েছে",
"delete_phone_number_confirmation": "আপনি কি নিশ্চিত যে আপনি এই ফোন নম্বরটি মুছতে চান? এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "ফোন নম্বর সাবস্ক্রিপশন সফলভাবে বাতিল করা হয়েছে",
"updating": "আপডেট করা হচ্ছে",
"round_robin": "রাউন্ড রবিন",
"hi_how_are_you_doing": "হাই, আপনি কেমন আছেন?",
"round_robin_description": "একাধিক দলের সদস্যের মধ্যে চক্র সভা।",
"managed_event": "পরিচালিত ইভেন্ট",
"username_placeholder": "ব্যবহারকারীর নাম",
@@ -876,7 +879,7 @@
"are_you_sure_you_want_to_delete_workflow_step": "আপনি কি নিশ্চিত যে আপনি এই ওয়ার্কফ্লো ধাপটি মুছতে চান?",
"do_you_still_want_to_unsubscribe": "আপনি কি এখনও এই এজেন্ট থেকে ফোন নম্বরটি আনসাবস্ক্রাইব করতে চান?",
"the_action_will_disconnect_phone_number": "এই কাজটি এজেন্ট থেকে ফোন নম্বরটি বিচ্ছিন্ন করবে। নতুন ফোন নম্বর সংযোগ না করা পর্যন্ত এজেন্ট কল করতে সক্ষম হবে না।",
"cal_ai_phone_numbers": "ফোন নম্বর",
"cal_ai_phone_numbers": "ফোন নম্বরসমূহ",
"connect_phone_number": "ফোন নম্বর সংযোগ করুন",
"cal_ai_phone_numbers_description": "আপনার ফোন নম্বরগুলি পরিচালনা করুন",
"import_number": "নম্বর আমদানি করুন",
@@ -886,7 +889,7 @@
"cancel_your_phone_number_subscription": "আপনার ফোন নম্বর সাবস্ক্রিপশন বাতিল করুন",
"delete_associated_phone_number": "সংযুক্ত ফোন নম্বরটি মুছুন",
"unauthorized_create_workflow": "আপনি এই ওয়ার্কফ্লো তৈরি করার জন্য অনুমোদিত নন",
"import_phone_number_description": "ফোনের সাথে ব্যবহার করার জন্য আপনার Twilio ফোন নম্বর আমদানি করুন",
"import_phone_number_description": "ফোনের সাথে ব্যবহার করার জন্য আপনার Twilio ফোন নম্বর ইম্পোর্ট করুন",
"phone_number_cost": "${{price}}/মাস",
"buy_new_number": "নতুন নম্বর কিনুন",
"buy_number_cost_x_per_month": "একটি ফোন নম্বর কেনার খরচ মাসে ${{priceInDollars}}। প্রতিটি সক্রিয় ফোন নম্বরের জন্য আপনাকে মাসিক চার্জ করা হবে।",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "হ্যাঁ, সাবস্ক্রিপশন বাতিল করুন",
"cancel_phone_number_subscription_confirmation": "আপনি কি নিশ্চিত যে আপনি এই ফোন নম্বর সাবস্ক্রিপশন বাতিল করতে চান? এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না এবং আপনি এই ফোন নম্বরে অ্যাক্সেস হারাবেন।",
"add_members": "সদস্য যোগ করুন ...",
"add_members_no_ellipsis": "সদস্য যোগ করুন",
"no_assigned_members": "কোনও নির্ধারিত সদস্য নেই",
"assigned_to": "নির্ধারিত",
"you_must_be_logged_in_to": "আপনি অবশ্যই লগ ইন করতে হবে {{url}}",
@@ -1207,6 +1211,7 @@
"categories": "বিভাগ",
"pricing": "মূল্য নির্ধারণ",
"learn_more": "আরও শিখুন",
"try_now": "এখনই চেষ্টা করুন",
"privacy_policy": "গোপনীয়তা নীতি",
"terms_of_service": "পরিষেবার শর্তাদি",
"remove": "সরান",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "অংশগ্রহণকারীদের কাছে হোয়াটসঅ্যাপ বার্তা প্রেরণ করুন",
"workflows": "কর্মপ্রবাহ",
"new_workflow_btn": "নতুন কর্মপ্রবাহ",
"how_would_you_like_to_start": "আপনি কীভাবে শুরু করতে চান?",
"add_new_workflow": "একটি নতুন ওয়ার্কফ্লো যুক্ত করুন",
"reschedule_event_trigger": "যখন ইভেন্টটি পুনরায় নির্ধারণ করা হয়",
"trigger": "ট্রিগার",
@@ -1716,6 +1722,8 @@
"event_duration_info": "ইভেন্টের সময়কাল",
"event_time_info": "ইভেন্ট শুরু সময়",
"event_type_not_found": "ইভেন্টটাইপ পাওয়া যায় নি",
"number_to_call_variable": "কল করার নম্বর",
"number_to_call_info": "আপনি যে ব্যবহারকারীকে কল করছেন তার ফোন নম্বর",
"location_variable": "অবস্থান",
"location_info": "ইভেন্টের অবস্থান",
"additional_notes_variable": "অতিরিক্ত নোট",
@@ -1753,6 +1761,7 @@
"team_url": "টিম ইউআরএল",
"team_members": "দলের সদস্য",
"more": "আরও",
"cal_ai_workflows": "Cal.ai ওয়ার্কফ্লো",
"and_count_more": "এবং আরও {{count}}টি",
"more_page_footer": "আমরা মোবাইল অ্যাপ্লিকেশনটিকে ওয়েব অ্যাপ্লিকেশনটির এক্সটেনশন হিসাবে দেখি। ",
"workflow_example_1": "ইভেন্টটি অংশ নিতে শুরু করার 24 ঘন্টা আগে এসএমএস অনুস্মারক প্রেরণ করুন",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "ইভেন্টগুলি উপস্থিত হতে শুরু করার 1 ঘন্টা আগে ইমেল অনুস্মারক প্রেরণ করুন",
"workflow_example_5": "ইভেন্টটি হোস্টে পুনরায় নির্ধারণ করা হলে কাস্টম ইমেল প্রেরণ করুন",
"workflow_example_6": "হোস্ট করার জন্য নতুন ইভেন্ট বুক করা হলে কাস্টম এসএমএস প্রেরণ করুন",
"send_sms_reminder": "এসএমএস রিমাইন্ডার পাঠান",
"send_sms_reminder_description": "ইভেন্ট শুরু হওয়ার ২৪ ঘন্টা আগে",
"follow_up_with_no_shows": "অনুপস্থিতদের সাথে ফলোআপ করুন",
"follow_up_with_no_shows_description": "ইভেন্ট শেষ হওয়ার ৩০ মিনিট পরে",
"remind_attendees_to_bring_id": "অংশগ্রহণকারীদের আইডি আনতে মনে করিয়ে দিন",
"remind_attendees_to_bring_id_description": "ইভেন্ট শুরু হওয়ার ১ দিন আগে",
"email_to_remind_booking": "ইমেইল রিমাইন্ডার",
"email_to_remind_booking_description": "ইভেন্ট শুরু হওয়ার ১ ঘন্টা আগে",
"custom_sms_reminder": "কাস্টম এসএমএস রিমাইন্ডার",
"custom_sms_reminder_description": "যখন ইভেন্ট শিডিউল করা হয়",
"custom_email_reminder": "কাস্টম ইমেইল রিমাইন্ডার",
"custom_email_reminder_description": "ইভেন্ট হোস্টের জন্য পুনঃনির্ধারিত করা হয়েছে",
"count_managed_to_limit": "পরিচালিত ইভেন্টের প্রকারগুলি থেকে বুকিং গণনা অন্তর্ভুক্ত করুন",
"welcome_to_cal_header": "স্বাগতম {{appName}}আর!",
"edit_form_later_subtitle": "আপনি পরে এটি সম্পাদনা করতে পারবেন।",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "বুকিং পৃষ্ঠায় দেখান",
"visit_cancelled_booking": "আপনি বাতিল হওয়া বুকিং পৃষ্ঠাটি দেখতে পারেন",
"get_started_zapier_templates": "জ্যাপিয়ার টেম্পলেটগুলি দিয়ে শুরু করুন",
"standard_templates": "স্ট্যান্ডার্ড টেমপ্লেট",
"cal_ai_templates": "Cal.ai টেমপ্লেট",
"team_is_unpublished": "{{team}} অপ্রকাশিত",
"org_is_unpublished_description": "এই সংস্থার লিঙ্কটি বর্তমানে উপলভ্য নয়। ",
"team_is_unpublished_description": "এই দলের লিঙ্কটি বর্তমানে উপলভ্য নয়। ",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "অর্থ প্রদানের জন্য কার্ড চার্জ করতে পারেনি।",
"insights": "অন্তর্দৃষ্টি",
"routing_forms": "রাউটিং ফর্ম",
"testing_workflow_info_message": "এই কর্মপ্রবাহটি পরীক্ষা করার সময়, সচেতন হন যে ইমেলগুলি এবং এসএমএস কেবলমাত্র কমপক্ষে 1 ঘন্টা আগে নির্ধারিত হতে পারে",
"insights_no_data_found_for_filter": "নির্বাচিত ফিল্টার বা নির্বাচিত তারিখগুলির জন্য কোনও ডেটা পাওয়া যায় নি।",
"acknowledge_booking_no_show_fee": "আমি স্বীকার করি যে আমি যদি এই ইভেন্টে অংশ না নিই তবে ক {{amount, currency}} আমার কার্ডে কোনও শো ফি প্রয়োগ করা হবে না।",
"days": "দিন",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "দল: {{teamName}}",
"insights_user_filter": "ব্যবহারকারী: {{userName}}",
"insights_subtitle": "আপনার ইভেন্টগুলি জুড়ে বুকিং অন্তর্দৃষ্টি দেখুন",
"call_history": "কল ইতিহাস",
"call_history_subtitle": "আপনার Cal.ai কলগুলির কল ইতিহাস দেখুন",
"location_options": "{{locationCount}} অবস্থান বিকল্প",
"channel_type": "চ্যানেল টাইপ",
"end_reason": "শেষ হওয়ার কারণ",
"session_status": "সেশন স্ট্যাটাস",
"user_sentiment": "ব্যবহারকারীর অনুভূতি",
"time_header": "সময়",
"from_header": "থেকে",
"custom_plan": "কাস্টম পরিকল্পনা",
"email_embed": "ইমেল এম্বেড",
"add_times_to_your_email": "কয়েকটি উপলভ্য সময় নির্বাচন করুন এবং সেগুলি আপনার ইমেলটিতে এম্বেড করুন",
@@ -2752,6 +2782,8 @@
"account_already_linked": "অ্যাকাউন্ট ইতিমধ্যে লিঙ্কযুক্ত",
"send_email": "ইমেল প্রেরণ",
"cal_ai_phone_call_action": "Cal.ai ভয়েস এজেন্ট ব্যবহার করে অংশগ্রহণকারীকে কল করুন",
"call_to_confirm_booking": "বুকিং নিশ্চিত করতে কল করুন",
"cal_ai_phone_call_action_description": "ইভেন্ট শুরু হওয়ার ২ ঘন্টা আগে",
"cal_ai_agent_configuration": "Cal.ai এজেন্ট কনফিগারেশন",
"choose_at_least_one_event_type_test_call": "একটি টেস্ট কল করতে অনুগ্রহ করে কমপক্ষে একটি ইভেন্ট টাইপ বেছে নিন।",
"mark_as_no_show": "নো-শো হিসাবে চিহ্নিত করুন",
@@ -3203,9 +3235,15 @@
"verify_email_change": "ইমেইল পরিবর্তন যাচাই করুন",
"buy_credits": "ক্রেডিট কিনুন",
"credits": "ক্রেডিট",
"view_and_manage_credits": "ক্রেডিট দেখুন এবং পরিচালনা করুন",
"credits_used": "ব্যবহৃত ক্রেডিট",
"total_credits_remaining": "মোট অবশিষ্ট",
"credits_per_tip_org": "আপনি প্রতি মাসে, প্রতি টিম সদস্যের জন্য ১০০০ ক্রেডিট পান",
"credits_per_tip_teams": "আপনি প্রতি মাসে, প্রতি টিম সদস্যের জন্য ৭৫০ ক্রেডিট পান",
"view_and_manage_credits": "এসএমএস বার্তা পাঠানোর জন্য ক্রেডিট দেখুন এবং পরিচালনা করুন",
"view_and_manage_credits_description": "এসএমএস বার্তা পাঠানোর জন্য ক্রেডিট দেখুন এবং পরিচালনা করুন। একটি ক্রেডিটের মূল্য 1¢ (USD)। <0>আরও জানুন</0>",
"credit_worth_description": "একটি ক্রেডিটের মূল্য ১¢ (USD)। <0>আরও জানুন</0>",
"buy_additional_credits": "অতিরিক্ত ক্রেডিট কিনুন ($0.01 প্রতি ক্রেডিট)",
"view_additional_credits_expense_tip": "আপনি আপনার ব্যয় লগে অতিরিক্ত ক্রেডিট খরচ দেখতে পারেন",
"overview": "সংক্ষিপ্ত বিবরণ",
"organization_slug_taken": "সংগঠনের স্লাগ ইতিমধ্যে নেওয়া হয়েছে",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "আপনি একটি সংগঠন তৈরি করতে পারবেন না কারণ আপনি ইতিমধ্যে একটি সংগঠনের অংশ",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "আপনার Cal.com টিম {{teamName}} এর ক্রেডিট শেষ হয়ে গেছে। ফলস্বরূপ, এসএমএস বার্তাগুলি এখন ইমেইলের মাধ্যমে পাঠানো হচ্ছে। এসএমএস পাঠানো পুনরায় শুরু করতে, অনুগ্রহ করে অতিরিক্ত ক্রেডিট কিনুন।",
"credit_limit_reached_message_user": "আপনার Cal.com অ্যাকাউন্টের ক্রেডিট শেষ হয়ে গেছে। ফলস্বরূপ, এসএমএস বার্তাগুলি এখন ইমেইলের মাধ্যমে পাঠানো হচ্ছে। এসএমএস পাঠানো পুনরায় শুরু করতে, অনুগ্রহ করে অতিরিক্ত ক্রেডিট কিনুন।",
"current_credit_balance": "বর্তমান ব্যালেন্স: {{balance}} ক্রেডিট",
"current_balance": "বর্তমান ব্যালেন্স:",
"notification_about_your_booking": "আপনার বুকিং সম্পর্কে বিজ্ঞপ্তি",
"monthly_credits": "মাসিক ক্রেডিট",
"total_credits": "মোট ক্রেডিট: {{totalCredits}}",
"remaining_credits": "অবশিষ্ট ক্রেডিট: {{remainingCredits}}",
"remaining": "অবশিষ্ট",
"total": "মোট",
"additional_credits": "অতিরিক্ত ক্রেডিট",
"routing_form_next_in_queue": "সারিতে পরবর্তী {{count}}টি",
"routing_form_select_members_to_email": "ইমেইল প্রতিক্রিয়া পাঠানোর জন্য সদস্য নির্বাচন করুন",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "বিদ্যমান কর্মপ্রবাহ এবং তাদের কনফিগারেশন দেখুন",
"pbac_desc_update_workflows": "কর্মপ্রবাহ সেটিংস সম্পাদনা ও পরিবর্তন করুন",
"pbac_desc_delete_workflows": "সিস্টেম থেকে কর্মপ্রবাহ সরান",
"pbac_resource_webhook": "ওয়েবহুক",
"pbac_desc_create_webhooks": "ওয়েবহুক তৈরি করুন",
"pbac_desc_view_webhooks": "ওয়েবহুক দেখুন",
"pbac_desc_update_webhooks": "ওয়েবহুক আপডেট করুন",
"pbac_desc_delete_webhooks": "ওয়েবহুক মুছুন",
"pbac_desc_manage_workflows": "সমস্ত কর্মপ্রবাহের জন্য সম্পূর্ণ ব্যবস্থাপনা অ্যাক্সেস",
"pbac_desc_create_event_types": "ইভেন্টের ধরণ তৈরি করুন",
"pbac_desc_view_event_types": "ইভেন্টের ধরণ দেখুন",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "ট্রিগার ইভেন্টের নাম (যেমন, BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "ওয়েবহুকের সময়",
"webhook_type": "ইভেন্ট টাইপ স্লাগ",
"set_up_agent": "এজেন্ট সেট আপ করুন",
"webhook_title": "ইভেন্ট টাইপের নাম",
"webhook_start_time": "ইভেন্টের শুরু সময়",
"webhook_end_time": "ইভেন্টের শেষ সময়",
@@ -3625,6 +3672,9 @@
"visit": "দেখুন",
"location_custom_label_input_label": "বুকিং পৃষ্ঠায় কাস্টম লেবেল",
"meeting_link": "সভার লিঙ্ক",
"session_outcome": "সেশনের ফলাফল",
"call_created": "কল তৈরি হয়েছে",
"voicemail": "ভয়েসমেইল",
"my_bookings": "আমার বুকিং",
"phone": "ফোন",
"free": "বিনামূল্যে",
@@ -3632,6 +3682,8 @@
"user_name": "ব্যবহারকারীর নাম",
"expand_panel": "প্যানেল বিস্তৃত করুন",
"collapse_panel": "প্যানেল সংকুচিত করুন",
"email_verification_required": "এই ইভেন্ট টাইপের জন্য ইমেইল যাচাইকরণ প্রয়োজন",
"invalid_verification_code": "অবৈধ যাচাইকরণ কোড প্রদান করা হয়েছে",
"you_have_one_team": "আপনার একটি দল আছে",
"consider_consolidating_one_team_org": "আপনার দলের মধ্যে বিলিং, অ্যাডমিন টুলস এবং অ্যানালিটিক্স একত্রিত করতে একটি সংগঠন সেট আপ করার কথা বিবেচনা করুন।",
"consider_consolidating_multi_team_org": "আপনার দলগুলির মধ্যে বিলিং, অ্যাডমিন টুলস এবং অ্যানালিটিক্স একত্রিত করতে একটি সংগঠন সেট আপ করার কথা বিবেচনা করুন।",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "নির্ধারিত শুরুর সময়ের আগে",
"cancel_booking_acknowledge_no_show_fee": "আমি স্বীকার করছি যে শুরুর সময়ের {{timeValue}} {{timeUnit}} এর মধ্যে বুকিং বাতিল করলে আমাকে {{amount, currency}} অনুপস্থিতি ফি চার্জ করা হবে",
"contact_organizer": "আপনার কোন প্রশ্ন থাকলে, অনুগ্রহ করে আয়োজকের সাথে যোগাযোগ করুন।",
"booking_time_option": "বুকিং সময়",
"booking_time_option_description": "যখন বুকিং নির্ধারিত হয় (শুরু থেকে শেষ)",
"created_at_option": "তৈরি হয়েছে",
"created_at_option_description": "যখন বুকিং প্রথম তৈরি করা হয়েছিল",
"call_details": "কলের বিবরণ",
"call_id": "কল আইডি",
"call_information": "কলের তথ্য",
"sentiment": "অনুভূতি",
"disconnect_reason": "সংযোগ বিচ্ছেদের কারণ",
"call_summary": "কলের সারাংশ",
"transcription": "ট্রান্সক্রিপশন",
"event_details": "ইভেন্টের বিবরণ",
"agent": "এজেন্ট",
"no_transcript_available": "কোন ট্রান্সক্রিপ্ট উপলব্ধ নেই",
"testing_sms_workflow_info_message": "এই ওয়ার্কফ্লো পরীক্ষা করার সময়, মনে রাখবেন যে এসএমএস অবশ্যই কমপক্ষে ১৫ মিনিট আগে নির্ধারিত করতে হবে",
"start_from_scratch_title": "শূন্য থেকে শুরু করুন",
"start_from_scratch_description": "শূন্য থেকে আপনার নিজের ওয়ার্কফ্লো তৈরি করুন।",
"cal_ai_template_title": "Cal.ai টেমপ্লেট",
"cal_ai_template_description": "এআই এজেন্ট যারা মিটিং বুক করে, রিমাইন্ডার পাঠায়, এবং ফলো আপ করে!",
"voice": "ভয়েস",
"select_voice": "ভয়েস নির্বাচন করুন",
"select_voice_for_agent": "আপনার এজেন্টের জন্য একটি ভয়েস নির্বাচন করুন",
"choose_a_voice_for_your_agent": "আপনার এজেন্টের জন্য একটি ভয়েস বেছে নিন",
"trait": "বৈশিষ্ট্য",
"voice_id": "ভয়েস আইডি",
"use_voice": "ভয়েস ব্যবহার করুন",
"current_voice": "বর্তমান ভয়েস",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑ুষান্ত"
}
+84 -5
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "Verifica la teva adreça de correu per garantir la millor entrega de correus i calendaris",
"verify_email_email_header": "Verifica la teva adreça de correu",
"verify_email_button": "Verifica el correu electrònic",
"cal_ai_assistant": "Assistent IA de Cal",
"cal_ai_assistant": "Assistent",
"send_cal_video_transcription_emails": "Envia correus amb la transcripció del vídeo",
"description_send_cal_video_transcription_emails": "Envia correus amb la transcripció del vídeo de Cal després que finalitzi la reunió. (Requereix un pla de pagament)",
"verify_email_change_description": "Has sol·licitat recentment canviar l'adreça de correu que utilitzes per iniciar sessió al teu compte de {{appName}}. Si us plau, fes clic al botó següent per confirmar la nova adreça.",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "La validació del flux de treball ha fallat",
"workflow_validation_empty_fields": "Un o més passos del flux de treball tenen contingut de missatge buit",
"workflow_validation_unverified_contacts": "Un o més números de telèfon o adreces de correu electrònic no estan verificats",
"supercharge_your_workflows_with_cal_ai": "Potencia els teus fluxos de treball amb Cal.ai",
"supercharge_your_workflows_with_cal_ai_description": "Agents d'IA realistes que programen reunions, envien recordatoris i fan seguiment amb els teus clients.",
"phone_number_imported_successfully": "Número de telèfon importat i vinculat a l'agent correctament",
"phone_number_deleted_successfully": "Número de telèfon eliminat correctament",
"delete_phone_number_confirmation": "Estàs segur que vols eliminar aquest número de telèfon? Aquesta acció no es pot desfer.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "Subscripció del número de telèfon cancel·lada correctament",
"updating": "Actualitzant",
"round_robin": "Round Robin",
"hi_how_are_you_doing": "Hola, com estàs?",
"round_robin_description": "Alterna les reunions entre diversos membres de l'equip.",
"managed_event": "Esdeveniment gestionat",
"username_placeholder": "nom d'usuari",
@@ -876,9 +879,9 @@
"are_you_sure_you_want_to_delete_workflow_step": "Estàs segur que vols eliminar aquest pas del flux de treball?",
"do_you_still_want_to_unsubscribe": "Encara vols donar de baixa el número de telèfon d'aquest agent?",
"the_action_will_disconnect_phone_number": "Aquesta acció desconnectarà el número de telèfon de l'agent. L'agent no podrà fer trucades fins que es connecti un nou número de telèfon.",
"cal_ai_phone_numbers": "Números de telèfon de Cal AI",
"cal_ai_phone_numbers": "Números de telèfon",
"connect_phone_number": "Connecta un número de telèfon",
"cal_ai_phone_numbers_description": "Gestiona els teus números de telèfon de Cal AI",
"cal_ai_phone_numbers_description": "Gestiona els teus números de telèfon",
"import_number": "Importa un número",
"this_action_will_also": "Aquesta acció també:",
"import_phone_number": "Importa un número de telèfon",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "Sí, cancel·lar la subscripció",
"cancel_phone_number_subscription_confirmation": "Estàs segur que vols cancel·lar aquesta subscripció de número de telèfon? Aquesta acció no es pot desfer i perdràs l'accés a aquest número de telèfon.",
"add_members": "Afegir membres...",
"add_members_no_ellipsis": "Afegir membres",
"no_assigned_members": "No hi ha membres assignats",
"assigned_to": "Assignat a",
"you_must_be_logged_in_to": "Has d'iniciar sessió per {{url}}",
@@ -1207,6 +1211,7 @@
"categories": "Categories",
"pricing": "Preus",
"learn_more": "Més informació",
"try_now": "Prova-ho ara",
"privacy_policy": "Política de privacitat",
"terms_of_service": "Condicions del servei",
"remove": "Elimina",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "enviar missatge de WhatsApp a l'assistent",
"workflows": "Fluxos de treball",
"new_workflow_btn": "Nou flux de treball",
"how_would_you_like_to_start": "Com vols començar?",
"add_new_workflow": "Afegir un nou flux de treball",
"reschedule_event_trigger": "quan es canvia l'hora de l'esdeveniment",
"trigger": "Activador",
@@ -1716,6 +1722,8 @@
"event_duration_info": "La durada de l'esdeveniment",
"event_time_info": "L'hora d'inici de l'esdeveniment",
"event_type_not_found": "Tipus d'esdeveniment no trobat",
"number_to_call_variable": "Número per trucar",
"number_to_call_info": "El número de telèfon de l'usuari al qual estàs trucant",
"location_variable": "Ubicació",
"location_info": "La ubicació de l'esdeveniment",
"additional_notes_variable": "Notes addicionals",
@@ -1753,6 +1761,7 @@
"team_url": "URL de l'equip",
"team_members": "Membres de l'equip",
"more": "Més",
"cal_ai_workflows": "Fluxos de treball de Cal.ai",
"and_count_more": "i {{count}} més",
"more_page_footer": "Considerem l'aplicació mòbil com una extensió de l'aplicació web. Si has de realitzar accions complexes, si us plau, torna a l'aplicació web.",
"workflow_example_1": "Envia un recordatori per SMS 24 hores abans que comenci l'esdeveniment al participant",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "Envia un recordatori per correu 1 hora abans que comenci l'esdeveniment al participant",
"workflow_example_5": "Envia un correu personalitzat a l'amfitrió quan es reprogrami l'esdeveniment",
"workflow_example_6": "Envia un SMS personalitzat a l'amfitrió quan es reservi un nou esdeveniment",
"send_sms_reminder": "Enviar recordatori per SMS",
"send_sms_reminder_description": "24 hores abans de l'inici de l'esdeveniment",
"follow_up_with_no_shows": "Seguiment dels que no s'han presentat",
"follow_up_with_no_shows_description": "30 minuts després de finalitzar l'esdeveniment",
"remind_attendees_to_bring_id": "Recordar als assistents que portin identificació",
"remind_attendees_to_bring_id_description": "1 dia abans de l'inici de l'esdeveniment",
"email_to_remind_booking": "Recordatori per correu electrònic",
"email_to_remind_booking_description": "1 hora abans de l'inici de l'esdeveniment",
"custom_sms_reminder": "Recordatori SMS personalitzat",
"custom_sms_reminder_description": "Quan es programa l'esdeveniment",
"custom_email_reminder": "Recordatori per correu electrònic personalitzat",
"custom_email_reminder_description": "L'esdeveniment es reprograma per a l'amfitrió",
"count_managed_to_limit": "Inclou el recompte de reserves dels tipus d'esdeveniments gestionats",
"welcome_to_cal_header": "Benvingut/da a {{appName}}!",
"edit_form_later_subtitle": "Podràs editar-ho més tard.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "Mostra a la pàgina de reserva",
"visit_cancelled_booking": "Pots visitar la pàgina de reserva cancel·lada",
"get_started_zapier_templates": "Comença amb les plantilles de Zapier",
"standard_templates": "Plantilles estàndard",
"cal_ai_templates": "Plantilles de Cal.ai",
"team_is_unpublished": "{{team}} no està publicat",
"org_is_unpublished_description": "Aquest enllaç d'organització no està disponible actualment. Si us plau, contacta amb el propietari de l'organització o demana-li que el publiqui.",
"team_is_unpublished_description": "Aquest enllaç d'equip no està disponible actualment. Si us plau, contacta amb el propietari de l'equip o demana-li que el publiqui.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "No s'ha pogut carregar la targeta per al pagament.",
"insights": "Estadístiques",
"routing_forms": "Formularis de redirecció",
"testing_workflow_info_message": "Quan provis aquest flux de treball, tingues en compte que els correus electrònics i SMS només es poden programar amb almenys 1 hora d'antelació",
"insights_no_data_found_for_filter": "No s'han trobat dades per al filtre seleccionat o les dates seleccionades.",
"acknowledge_booking_no_show_fee": "Reconec que si no assisteixo a aquest esdeveniment, s'aplicarà una penalització de {{amount, currency}} a la meva targeta.",
"days": "dies",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "Equip: {{teamName}}",
"insights_user_filter": "Usuari: {{userName}}",
"insights_subtitle": "Visualitzeu les estadístiques de reserves dels vostres esdeveniments",
"call_history": "Historial de trucades",
"call_history_subtitle": "Visualitza l'historial de trucades de les teves trucades de Cal.ai",
"location_options": "{{locationCount}} opcions d'ubicació",
"channel_type": "Tipus de canal",
"end_reason": "Motiu de finalització",
"session_status": "Estat de la sessió",
"user_sentiment": "Sentiment de l'usuari",
"time_header": "Hora",
"from_header": "De",
"custom_plan": "Pla personalitzat",
"email_embed": "Incrustació al correu",
"add_times_to_your_email": "Seleccioneu alguns horaris disponibles i incrusteu-los al vostre correu",
@@ -2752,6 +2782,8 @@
"account_already_linked": "El compte ja està vinculat",
"send_email": "Envia un correu electrònic",
"cal_ai_phone_call_action": "Trucar a l'assistent utilitzant l'agent de veu Cal.ai",
"call_to_confirm_booking": "Trucada per confirmar la reserva",
"cal_ai_phone_call_action_description": "2 hores abans de l'inici de l'esdeveniment",
"cal_ai_agent_configuration": "Configuració de l'agent Cal.ai",
"choose_at_least_one_event_type_test_call": "Si us plau, escull almenys un tipus d'esdeveniment per fer una trucada de prova.",
"mark_as_no_show": "Marca com a no presentat",
@@ -3203,9 +3235,15 @@
"verify_email_change": "Verifica el canvi de correu electrònic",
"buy_credits": "Compra crèdits",
"credits": "Crèdits",
"view_and_manage_credits": "Visualitza i gestiona els crèdits",
"credits_used": "Crèdits utilitzats",
"total_credits_remaining": "Total restant",
"credits_per_tip_org": "Reps 1000 crèdits al mes per cada membre de l'equip",
"credits_per_tip_teams": "Reps 750 crèdits al mes per cada membre de l'equip",
"view_and_manage_credits": "Visualitza i gestiona els crèdits per enviar missatges SMS",
"view_and_manage_credits_description": "Visualitza i gestiona els crèdits per enviar missatges SMS. Un crèdit val 1¢ (USD). <0>Més informació</0>",
"credit_worth_description": "Un crèdit val 1¢ (USD). <0>Més informació</0>",
"buy_additional_credits": "Compra crèdits addicionals (0,01 $ per crèdit)",
"view_additional_credits_expense_tip": "Pots veure la despesa de crèdits addicionals al teu registre de despeses",
"overview": "Resum",
"organization_slug_taken": "L'slug de l'organització ja està en ús",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "No podeu crear una organització perquè ja formeu part d'una",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "El teu equip de Cal.com {{teamName}} s'ha quedat sense crèdits. Com a resultat, els missatges SMS ara s'estan enviant per correu electrònic. Per reprendre l'enviament d'SMS, si us plau, compra crèdits addicionals.",
"credit_limit_reached_message_user": "El teu compte de Cal.com s'ha quedat sense crèdits. Com a resultat, els missatges SMS ara s'envien per correu electrònic. Per reprendre l'enviament d'SMS, si us plau, compra crèdits addicionals.",
"current_credit_balance": "Saldo actual: {{balance}} crèdits",
"current_balance": "Saldo actual:",
"notification_about_your_booking": "Notificació sobre la teva reserva",
"monthly_credits": "Crèdits mensuals",
"total_credits": "Total de crèdits: {{totalCredits}}",
"remaining_credits": "Crèdits restants: {{remainingCredits}}",
"remaining": "Restant",
"total": "Total",
"additional_credits": "Crèdits addicionals",
"routing_form_next_in_queue": "{{count}} següent a la cua",
"routing_form_select_members_to_email": "Enviar respostes per correu electrònic a",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "Veure fluxos de treball existents i les seves configuracions",
"pbac_desc_update_workflows": "Editar i modificar la configuració dels fluxos de treball",
"pbac_desc_delete_workflows": "Eliminar fluxos de treball del sistema",
"pbac_resource_webhook": "Webhook",
"pbac_desc_create_webhooks": "Crea webhooks",
"pbac_desc_view_webhooks": "Visualitza webhooks",
"pbac_desc_update_webhooks": "Actualitza els webhooks",
"pbac_desc_delete_webhooks": "Elimina els webhooks",
"pbac_desc_manage_workflows": "Accés complet de gestió a tots els fluxos de treball",
"pbac_desc_create_event_types": "Crear tipus d'esdeveniments",
"pbac_desc_view_event_types": "Veure tipus d'esdeveniments",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "El nom de l'esdeveniment desencadenant (p. ex., BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "L'hora del webhook",
"webhook_type": "L'slug del tipus d'esdeveniment",
"set_up_agent": "Configura l'agent",
"webhook_title": "El nom del tipus d'esdeveniment",
"webhook_start_time": "L'hora d'inici de l'esdeveniment",
"webhook_end_time": "L'hora de finalització de l'esdeveniment",
@@ -3625,6 +3672,9 @@
"visit": "Visita",
"location_custom_label_input_label": "Etiqueta personalitzada a la pàgina de reserva",
"meeting_link": "Enllaç de la reunió",
"session_outcome": "Resultat de la sessió",
"call_created": "Trucada creada",
"voicemail": "Bústia de veu",
"my_bookings": "Les meves reserves",
"phone": "Telèfon",
"free": "Gratuït",
@@ -3632,6 +3682,8 @@
"user_name": "Nom d'usuari",
"expand_panel": "Expandeix el panell",
"collapse_panel": "Contrau el panell",
"email_verification_required": "Es requereix verificació de correu electrònic per a aquest tipus d'esdeveniment",
"invalid_verification_code": "El codi de verificació proporcionat no és vàlid",
"you_have_one_team": "Tens un equip",
"consider_consolidating_one_team_org": "Considera configurar una organització per unificar la facturació, les eines d'administració i les analítiques del teu equip.",
"consider_consolidating_multi_team_org": "Considera configurar una organització per unificar la facturació, les eines d'administració i les analítiques dels teus equips.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "Abans de l'hora d'inici programada",
"cancel_booking_acknowledge_no_show_fee": "Reconec que en cancel·lar la reserva dins dels {{timeValue}} {{timeUnit}} abans de l'hora d'inici se'm cobrarà la tarifa per no assistència de {{amount, currency}}",
"contact_organizer": "Si teniu alguna pregunta, poseu-vos en contacte amb l'organitzador.",
"booking_time_option": "Hora de reserva",
"booking_time_option_description": "Quan es programa la reserva (inici a fi)",
"created_at_option": "Creat el",
"created_at_option_description": "Quan es va crear originalment la reserva",
"call_details": "Detalls de la trucada",
"call_id": "ID de trucada",
"call_information": "Informació de la trucada",
"sentiment": "Sentiment",
"disconnect_reason": "Motiu de desconnexió",
"call_summary": "Resum de la trucada",
"transcription": "Transcripció",
"event_details": "Detalls de l'esdeveniment",
"agent": "Agent",
"no_transcript_available": "No hi ha cap transcripció disponible",
"testing_sms_workflow_info_message": "Quan proveu aquest flux de treball, tingueu en compte que els SMS s'han de programar amb almenys 15 minuts d'antelació",
"start_from_scratch_title": "Comença des de zero",
"start_from_scratch_description": "Crea el teu propi flux de treball des de zero.",
"cal_ai_template_title": "Plantilla Cal.ai",
"cal_ai_template_description": "Agents d'IA que programen reunions, envien recordatoris i fan seguiment!",
"voice": "Veu",
"select_voice": "Selecciona la veu",
"select_voice_for_agent": "Selecciona una veu per al teu agent",
"choose_a_voice_for_your_agent": "Tria una veu per al teu agent",
"trait": "Característica",
"voice_id": "ID de veu",
"use_voice": "Utilitza la veu",
"current_voice": "Veu actual",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+85 -6
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "Ověřte svoji e-mailovou adresu, abyste zaručili co nejlepší doručitelnost e-mailů a kalendáře",
"verify_email_email_header": "Ověřte svoji e-mailovou adresu",
"verify_email_button": "Ověřit e-mail",
"cal_ai_assistant": "asistent",
"cal_ai_assistant": "Asistent",
"send_cal_video_transcription_emails": "Posílat e-maily s přepisem Cal videa",
"description_send_cal_video_transcription_emails": "Posílat e-maily s přepisem Cal videa po skončení schůzky. (Vyžaduje placený tarif)",
"verify_email_change_description": "Nedávno jste požádali o změnu e-mailové adresy, kterou používáte k přihlášení do svého účtu {{appName}}. Klikněte prosím na tlačítko níže, abyste potvrdili svou novou e-mailovou adresu.",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "Validace workflow selhala",
"workflow_validation_empty_fields": "Jeden nebo více kroků workflow má prázdný obsah zprávy",
"workflow_validation_unverified_contacts": "Jedno nebo více telefonních čísel nebo e-mailových adres není ověřeno",
"supercharge_your_workflows_with_cal_ai": "Posilte své pracovní postupy s Cal.ai",
"supercharge_your_workflows_with_cal_ai_description": "Realistické AI agenty, které plánují schůzky, posílají připomínky a komunikují s vašimi zákazníky.",
"phone_number_imported_successfully": "Telefonní číslo bylo úspěšně importováno a propojeno s agentem",
"phone_number_deleted_successfully": "Telefonní číslo bylo úspěšně smazáno",
"delete_phone_number_confirmation": "Opravdu chcete smazat toto telefonní číslo? Tuto akci nelze vrátit zpět.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "Předplatné telefonního čísla bylo úspěšně zrušeno",
"updating": "Aktualizuji",
"round_robin": "Plánování Round Robin",
"hi_how_are_you_doing": "Ahoj, jak se máte?",
"round_robin_description": "Schůzky v řadě mezi několika členy týmu.",
"managed_event": "Spravovaná událost",
"username_placeholder": "uživatelské jméno",
@@ -876,9 +879,9 @@
"are_you_sure_you_want_to_delete_workflow_step": "Opravdu chcete odstranit tento krok pracovního postupu?",
"do_you_still_want_to_unsubscribe": "Opravdu chcete odhlásit telefonní číslo od tohoto agenta?",
"the_action_will_disconnect_phone_number": "Tato akce odpojí telefonní číslo od agenta. Agent nebude moci uskutečňovat hovory, dokud nebude připojeno nové telefonní číslo.",
"cal_ai_phone_numbers": "Telefonní čísla Cal AI",
"cal_ai_phone_numbers": "Telefonní čísla",
"connect_phone_number": "Připojit telefonní číslo",
"cal_ai_phone_numbers_description": "Spravujte svá telefonní čísla Cal AI",
"cal_ai_phone_numbers_description": "Spravujte svá telefonní čísla",
"import_number": "Importovat číslo",
"this_action_will_also": "Tato akce také:",
"import_phone_number": "Importovat telefonní číslo",
@@ -886,7 +889,7 @@
"cancel_your_phone_number_subscription": "Zrušit předplatné telefonního čísla",
"delete_associated_phone_number": "Odstranit přidružené telefonní číslo",
"unauthorized_create_workflow": "Nemáte oprávnění vytvořit tento pracovní postup",
"import_phone_number_description": "Importujte své telefonní číslo Twilio pro použití s Phone",
"import_phone_number_description": "Importujte své telefonní číslo z Twilio pro použití s telefonem",
"phone_number_cost": "${{price}}/měsíc",
"buy_new_number": "Koupit nové číslo",
"buy_number_cost_x_per_month": "Nákup telefonního čísla stojí ${{priceInDollars}} měsíčně. Za každé aktivní telefonní číslo vám bude účtován měsíční poplatek.",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "Ano, zrušit předplatné",
"cancel_phone_number_subscription_confirmation": "Opravdu chcete zrušit toto předplatné telefonního čísla? Tuto akci nelze vrátit zpět a ztratíte přístup k tomuto telefonnímu číslu.",
"add_members": "Přidat členy...",
"add_members_no_ellipsis": "Přidat členy",
"no_assigned_members": "Žádní přiřazení členové",
"assigned_to": "Přiřazeno k",
"you_must_be_logged_in_to": "Musíte být přihlášeni k {{url}}",
@@ -1207,6 +1211,7 @@
"categories": "Kategorie",
"pricing": "Ceník",
"learn_more": "Zjistit více",
"try_now": "Vyzkoušet nyní",
"privacy_policy": "Zásady ochrany soukromí",
"terms_of_service": "Podmínky používání",
"remove": "Odebrat",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "odeslat Whatsapp účastníkovi",
"workflows": "Pracovní postupy",
"new_workflow_btn": "Nový pracovní postup",
"how_would_you_like_to_start": "Jak chcete začít?",
"add_new_workflow": "Přidat nový pracovní postup",
"reschedule_event_trigger": "pokud je událost přeložena na jiný termín",
"trigger": "Spouštěč",
@@ -1716,6 +1722,8 @@
"event_duration_info": "Délka události",
"event_time_info": "Čas začátku události",
"event_type_not_found": "Typ události nebyl nalezen",
"number_to_call_variable": "Číslo k zavolání",
"number_to_call_info": "Telefonní číslo uživatele, kterému voláte",
"location_variable": "Místo",
"location_info": "Místo události",
"additional_notes_variable": "Doplňující poznámky",
@@ -1753,6 +1761,7 @@
"team_url": "Adresa URL týmu",
"team_members": "Členové týmu",
"more": "Více",
"cal_ai_workflows": "Cal.ai pracovní postupy",
"and_count_more": "a ještě {{count}}",
"more_page_footer": "Mobilní aplikaci považujeme za rozšíření webové aplikace. Pokud provádíte složitější akce, proveďte je prosím ve webové aplikaci.",
"workflow_example_1": "Odeslat účastníkovi SMS upomínku 24 hodin před začátkem akce",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "Odeslat účastníkovi upomínku 1 hodinu před začátkem akce",
"workflow_example_5": "Odeslat hostiteli vlastní e-mail, pokud je událost přeložena na jiný termín",
"workflow_example_6": "Odeslat hostiteli vlastní SMS, pokud je rezervována nová událost",
"send_sms_reminder": "Poslat SMS připomínku",
"send_sms_reminder_description": "24 hodin před začátkem události",
"follow_up_with_no_shows": "Následná komunikace s nepřítomnými",
"follow_up_with_no_shows_description": "30 minut po skončení události",
"remind_attendees_to_bring_id": "Připomenout účastníkům, aby si vzali průkaz totožnosti",
"remind_attendees_to_bring_id_description": "1 den před začátkem události",
"email_to_remind_booking": "E-mailová připomínka",
"email_to_remind_booking_description": "1 hodinu před začátkem události",
"custom_sms_reminder": "Vlastní SMS připomínka",
"custom_sms_reminder_description": "Když je událost naplánována",
"custom_email_reminder": "Vlastní e-mailová připomínka",
"custom_email_reminder_description": "Událost je přeplánována pro hostitele",
"count_managed_to_limit": "Zahrnout počty rezervací ze spravovaných typů událostí",
"welcome_to_cal_header": "Vítá vás {{appName}}!",
"edit_form_later_subtitle": "Toto budete moci později upravit.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "Zobrazit na stránce rezervace",
"visit_cancelled_booking": "Můžete navštívit stránku zrušené rezervace",
"get_started_zapier_templates": "Začněte používat šablony Zapier",
"standard_templates": "Standardní šablony",
"cal_ai_templates": "Cal.ai šablony",
"team_is_unpublished": "Tým {{team}} není zveřejněn",
"org_is_unpublished_description": "Tento odkaz organizace není v současné době k dispozici. Kontaktujte prosím vlastníka organizace nebo ho požádejte o jeho zveřejnění.",
"team_is_unpublished_description": "Tento odkaz subjektu ({{entity}}) není v současné době k dispozici. Kontaktujte prosím vlastníka subjektu ({{entity}}) nebo ho požádejte o jeho zveřejnění.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "Nepodařilo se strhnout platbu z karty.",
"insights": "Přehledy",
"routing_forms": "Směrovací formuláře",
"testing_workflow_info_message": "Při testování tohoto pracovního postupu mějte na paměti, že e-maily a SMS lze naplánovat pouze nejméně 1 hodinu předem.",
"insights_no_data_found_for_filter": "Pro vybraný filtr nebo vybraná data nebyla nalezena žádná data.",
"acknowledge_booking_no_show_fee": "Beru na vědomí, že pokud se této události nezúčastním, bude z mé karty stržen poplatek za nedostavení se ve výši {{amount, currency}}.",
"days": "dny",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "Tým: {{teamName}}",
"insights_user_filter": "Uživatel: {{userName}}",
"insights_subtitle": "Zobrazte si Insight rezervací napříč vašimi událostmi",
"call_history": "Historie hovorů",
"call_history_subtitle": "Zobrazte historii hovorů napříč vašimi Cal.ai hovory",
"location_options": "Možná místa konání: {{locationCount}}",
"channel_type": "Typ kanálu",
"end_reason": "Důvod ukončení",
"session_status": "Stav relace",
"user_sentiment": "Sentiment uživatele",
"time_header": "Čas",
"from_header": "Od",
"custom_plan": "Vlastní plán",
"email_embed": "Vložení do e-mailu",
"add_times_to_your_email": "Vyberte několik dostupných časů a vložte je do e-mailu",
@@ -2752,6 +2782,8 @@
"account_already_linked": "Účet je již propojen",
"send_email": "Odeslat e-mail",
"cal_ai_phone_call_action": "Zavolat účastníkovi pomocí hlasového agenta Cal.ai",
"call_to_confirm_booking": "Hovor pro potvrzení rezervace",
"cal_ai_phone_call_action_description": "2 hodiny před začátkem události",
"cal_ai_agent_configuration": "Konfigurace agenta Cal.ai",
"choose_at_least_one_event_type_test_call": "Vyberte prosím alespoň jeden typ události pro testovací hovor.",
"mark_as_no_show": "Označit jako nepřítomný",
@@ -3203,9 +3235,15 @@
"verify_email_change": "Ověřit změnu e-mailu",
"buy_credits": "Koupit kredity",
"credits": "Kredity",
"view_and_manage_credits": "Zobrazit a spravovat kredity",
"credits_used": "Použité kredity",
"total_credits_remaining": "Celkem zbývá",
"credits_per_tip_org": "Získáváte 1000 kreditů měsíčně na každého člena týmu",
"credits_per_tip_teams": "Získáváte 750 kreditů měsíčně na každého člena týmu",
"view_and_manage_credits": "Zobrazit a spravovat kredity pro odesílání SMS zpráv",
"view_and_manage_credits_description": "Zobrazte a spravujte kredity pro odesílání SMS zpráv. Jeden kredit má hodnotu 1¢ (USD). <0>Zjistit více</0>",
"credit_worth_description": "Jeden kredit má hodnotu 1¢ (USD). <0>Zjistit více</0>",
"buy_additional_credits": "Koupit další kredity (0,01 $ za kredit)",
"view_additional_credits_expense_tip": "Dodatečné čerpání kreditů můžete zobrazit v protokolu výdajů",
"overview": "Přehled",
"organization_slug_taken": "Slug organizace je již obsazen",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Nemůžete vytvořit organizaci, protože již jste součástí jiné organizace",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "Vašemu týmu Cal.com {{teamName}} došly kredity. V důsledku toho jsou nyní SMS zprávy odesílány prostřednictvím e-mailu. Pro obnovení odesílání SMS prosím zakupte další kredity.",
"credit_limit_reached_message_user": "Vašemu účtu Cal.com došly kredity. V důsledku toho jsou nyní SMS zprávy zasílány e-mailem. Pro obnovení zasílání SMS si prosím zakupte další kredity.",
"current_credit_balance": "Aktuální zůstatek: {{balance}} kreditů",
"current_balance": "Aktuální zůstatek:",
"notification_about_your_booking": "Oznámení o vaší rezervaci",
"monthly_credits": "Měsíční kredity",
"total_credits": "Celkem kreditů: {{totalCredits}}",
"remaining_credits": "Zbývající kredity: {{remainingCredits}}",
"remaining": "Zbývá",
"total": "Celkem",
"additional_credits": "Dodatečné kredity",
"routing_form_next_in_queue": "{{count}} další ve frontě",
"routing_form_select_members_to_email": "Odeslat e-mailové odpovědi na",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "Zobrazit existující pracovní postupy a jejich konfigurace",
"pbac_desc_update_workflows": "Upravovat a měnit nastavení pracovních postupů",
"pbac_desc_delete_workflows": "Odstraňovat pracovní postupy ze systému",
"pbac_resource_webhook": "Webhook",
"pbac_desc_create_webhooks": "Vytvářet webhooky",
"pbac_desc_view_webhooks": "Zobrazit webhooky",
"pbac_desc_update_webhooks": "Aktualizovat webhooky",
"pbac_desc_delete_webhooks": "Smazat webhooky",
"pbac_desc_manage_workflows": "Plný přístup ke správě všech pracovních postupů",
"pbac_desc_create_event_types": "Vytvářet typy událostí",
"pbac_desc_view_event_types": "Zobrazit typy událostí",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "Název spouštěcí události (např. BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "Čas vytvoření webhooku",
"webhook_type": "Slug typu události",
"set_up_agent": "Nastavit agenta",
"webhook_title": "Název typu události",
"webhook_start_time": "Čas začátku události",
"webhook_end_time": "Čas ukončení události",
@@ -3625,6 +3672,9 @@
"visit": "Navštívit",
"location_custom_label_input_label": "Vlastní popisek na stránce rezervace",
"meeting_link": "Odkaz na schůzku",
"session_outcome": "Výsledek relace",
"call_created": "Hovor vytvořen",
"voicemail": "Hlasová schránka",
"my_bookings": "Moje rezervace",
"phone": "Telefon",
"free": "Zdarma",
@@ -3632,6 +3682,8 @@
"user_name": "Jméno uživatele",
"expand_panel": "Rozbalit panel",
"collapse_panel": "Sbalit panel",
"email_verification_required": "Pro tento typ události je vyžadováno ověření e-mailu",
"invalid_verification_code": "Byl poskytnut neplatný ověřovací kód",
"you_have_one_team": "Máte jeden tým",
"consider_consolidating_one_team_org": "Zvažte založení organizace pro sjednocení fakturace, administrativních nástrojů a analytiky napříč vaším týmem.",
"consider_consolidating_multi_team_org": "Zvažte založení organizace pro sjednocení fakturace, administrativních nástrojů a analytiky napříč vašimi týmy.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "Před plánovaným časem zahájení",
"cancel_booking_acknowledge_no_show_fee": "Beru na vědomí, že zrušením rezervace v době kratší než {{timeValue}} {{timeUnit}} před začátkem mi bude účtován poplatek za nedostavení se ve výši {{amount, currency}}",
"contact_organizer": "Pokud máte jakékoli dotazy, kontaktujte prosím organizátora.",
"booking_time_option": "Čas rezervace",
"booking_time_option_description": "Kdy je rezervace naplánována (od začátku do konce)",
"created_at_option": "Vytvořeno",
"created_at_option_description": "Kdy byla rezervace původně vytvořena",
"call_details": "Podrobnosti hovoru",
"call_id": "ID hovoru",
"call_information": "Informace o hovoru",
"sentiment": "Sentiment",
"disconnect_reason": "Důvod odpojení",
"call_summary": "Shrnutí hovoru",
"transcription": "Transkripce",
"event_details": "Podrobnosti události",
"agent": "Agent",
"no_transcript_available": "Žádný přepis není k dispozici",
"testing_sms_workflow_info_message": "Při testování tohoto workflow mějte na paměti, že SMS musí být naplánovány nejméně 15 minut předem",
"start_from_scratch_title": "Začít od nuly",
"start_from_scratch_description": "Vytvořte si vlastní workflow od začátku.",
"cal_ai_template_title": "Šablona Cal.ai",
"cal_ai_template_description": "AI agenti, kteří rezervují schůzky, posílají připomenutí a následně komunikují!",
"voice": "Hlas",
"select_voice": "Vybrat hlas",
"select_voice_for_agent": "Vyberte hlas pro vašeho agenta",
"choose_a_voice_for_your_agent": "Zvolte hlas pro vašeho agenta",
"trait": "Vlastnost",
"voice_id": "ID hlasu",
"use_voice": "Použít hlas",
"current_voice": "Aktuální hlas",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+84 -5
View File
@@ -819,6 +819,8 @@
"workflow_validation_failed": "Workflow-validering mislykkedes",
"workflow_validation_empty_fields": "Et eller flere workflow-trin har tomt meddelelsesindhold",
"workflow_validation_unverified_contacts": "Et eller flere telefonnumre eller e-mailadresser er ikke verificeret",
"supercharge_your_workflows_with_cal_ai": "Boost dine workflows med Cal.ai",
"supercharge_your_workflows_with_cal_ai_description": "Livagtige AI-agenter, der booker møder, sender påmindelser og følger op med dine kunder.",
"phone_number_imported_successfully": "Telefonnummer importeret og knyttet til agenten med succes",
"phone_number_deleted_successfully": "Telefonnummer slettet med succes",
"delete_phone_number_confirmation": "Er du sikker på, at du vil slette dette telefonnummer? Denne handling kan ikke fortrydes.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "Telefonnummerabonnement annulleret med succes",
"updating": "Opdaterer",
"round_robin": "Round Robin",
"hi_how_are_you_doing": "Hej, hvordan har du det?",
"round_robin_description": "Cyklusmøder mellem flere teammedlemmer.",
"managed_event": "Administreret begivenhed",
"username_placeholder": "brugernavn",
@@ -876,9 +879,9 @@
"are_you_sure_you_want_to_delete_workflow_step": "Er du sikker på, at du vil slette dette workflow-trin?",
"do_you_still_want_to_unsubscribe": "Vil du stadig afmelde telefonnummeret fra denne agent?",
"the_action_will_disconnect_phone_number": "Denne handling vil afbryde telefonnummeret fra agenten. Agenten vil ikke kunne foretage opkald, før et nyt telefonnummer er forbundet.",
"cal_ai_phone_numbers": "Cal AI-telefonnumre",
"cal_ai_phone_numbers": "Telefonnumre",
"connect_phone_number": "Forbind telefonnummer",
"cal_ai_phone_numbers_description": "Administrer dine Cal AI-telefonnumre",
"cal_ai_phone_numbers_description": "Administrer dine telefonnumre",
"import_number": "Importer nummer",
"this_action_will_also": "Denne handling vil også:",
"import_phone_number": "Importer telefonnummer",
@@ -886,7 +889,7 @@
"cancel_your_phone_number_subscription": "Annuller dit telefonnummerabonnement",
"delete_associated_phone_number": "Slet det tilknyttede telefonnummer",
"unauthorized_create_workflow": "Du har ikke tilladelse til at oprette dette workflow",
"import_phone_number_description": "Importer dit Twilio-telefonnummer til brug med Cal AI-telefon",
"import_phone_number_description": "Importer dit Twilio-telefonnummer til brug med Phone",
"phone_number_cost": "${{price}}/måned",
"buy_new_number": "Køb nyt nummer",
"buy_number_cost_x_per_month": "At købe et telefonnummer koster ${{priceInDollars}} om måneden. Du vil blive opkrævet månedligt for hvert aktive telefonnummer.",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "Ja, annuller abonnement",
"cancel_phone_number_subscription_confirmation": "Er du sikker på, at du vil annullere dette telefonnummerabonnement? Denne handling kan ikke fortrydes, og du vil miste adgangen til dette telefonnummer.",
"add_members": "Tilføj medlemmer...",
"add_members_no_ellipsis": "Tilføj medlemmer",
"no_assigned_members": "Ingen tildelte medlemmer",
"assigned_to": "Tildelt til",
"you_must_be_logged_in_to": "Du skal være logget ind på {{url}}",
@@ -1207,6 +1211,7 @@
"categories": "Kategorier",
"pricing": "Priser",
"learn_more": "Få flere oplysninger",
"try_now": "Prøv nu",
"privacy_policy": "Privatlivspolitik",
"terms_of_service": "Vilkår og betingelser",
"remove": "Fjern",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "send WhatsApp-besked til deltager",
"workflows": "Workflows",
"new_workflow_btn": "Nyt Workflow",
"how_would_you_like_to_start": "Hvordan vil du gerne starte?",
"add_new_workflow": "Tilføj et nyt workflow",
"reschedule_event_trigger": "når begivenheden bliver omlagt",
"trigger": "Trigger",
@@ -1716,6 +1722,8 @@
"event_duration_info": "Begivenhedens varighed",
"event_time_info": "Starttidspunkt for begivenheden",
"event_type_not_found": "Begivenhedstype ikke fundet",
"number_to_call_variable": "Nummer at ringe til",
"number_to_call_info": "Telefonnummeret på den bruger, du ringer til",
"location_variable": "Placering",
"location_info": "Begivenhedens placering",
"additional_notes_variable": "Yderligere bemærkninger",
@@ -1753,6 +1761,7 @@
"team_url": "Team URL",
"team_members": "Teammedlemmer",
"more": "Flere",
"cal_ai_workflows": "Cal.ai Workflows",
"and_count_more": "og {{count}} mere",
"more_page_footer": "Vi ser mobilapplikationen som en udvidelse af webapplikationen. Hvis du udfører komplicerede handlinger, henvises tilbage til webapplikationen.",
"workflow_example_1": "Send sms-påmindelse til deltageren 24 timer før begivenheden starter",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "Send e-mail påmindelse til deltager 1 time før begivenheder starter",
"workflow_example_5": "Send brugerdefineret e-mail til vært når begivenheden omlægges",
"workflow_example_6": "Send brugerdefineret SMS til vært når ny begivenhed bookes",
"send_sms_reminder": "Send SMS-påmindelse",
"send_sms_reminder_description": "24 timer før begivenheden starter",
"follow_up_with_no_shows": "Følg op med udeblivelser",
"follow_up_with_no_shows_description": "30 minutter efter begivenheden slutter",
"remind_attendees_to_bring_id": "Mind deltagere om at medbringe ID",
"remind_attendees_to_bring_id_description": "1 dag før begivenheden starter",
"email_to_remind_booking": "E-mail-påmindelse",
"email_to_remind_booking_description": "1 time før begivenheden starter",
"custom_sms_reminder": "Brugerdefineret SMS-påmindelse",
"custom_sms_reminder_description": "Når begivenheden er planlagt",
"custom_email_reminder": "Brugerdefineret e-mail-påmindelse",
"custom_email_reminder_description": "Begivenheden er omlagt til værten",
"count_managed_to_limit": "Inkluder bookingantal fra administrerede begivenhedstyper",
"welcome_to_cal_header": "Velkommen til {{appName}}!",
"edit_form_later_subtitle": "Du vil kunne redigere dette senere.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "Vis på bookingsiden",
"visit_cancelled_booking": "Du kan besøge siden for den aflyste booking",
"get_started_zapier_templates": "Kom i gang med Zapier skabeloner",
"standard_templates": "Standard skabeloner",
"cal_ai_templates": "Cal.ai-skabeloner",
"team_is_unpublished": "{{team}} er upubliceret",
"org_is_unpublished_description": "Dette organisationslink er i øjeblikket ikke tilgængeligt. Kontakt venligst organisationsindehaveren eller bed dem om at offentliggøre det.",
"team_is_unpublished_description": "Dette teamlink er i øjeblikket ikke tilgængeligt. Kontakt venligst teamindehaveren eller bed dem om at offentliggøre det.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "Kunne ikke trække betaling fra kortet.",
"insights": "Indsigter",
"routing_forms": "Ruteformularer",
"testing_workflow_info_message": "Når du tester denne arbejdsgang, skal du være opmærksom på, at e-mails og SMS'er kun kan planlægges mindst 1 time i forvejen",
"insights_no_data_found_for_filter": "Ingen data fundet for det valgte filter eller de valgte datoer.",
"acknowledge_booking_no_show_fee": "Jeg anerkender, at hvis jeg ikke deltager i denne begivenhed, vil et gebyr for udeblivelse på {{amount, currency}} blive opkrævet på mit kort.",
"days": "dage",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Bruger: {{userName}}",
"insights_subtitle": "Se bookingindsigter på tværs af dine begivenheder",
"call_history": "Opkaldshistorik",
"call_history_subtitle": "Se opkaldshistorik for dine Cal.ai-opkald",
"location_options": "{{locationCount}} placeringsmuligheder",
"channel_type": "Kanaltype",
"end_reason": "Afslutningsårsag",
"session_status": "Sessionsstatus",
"user_sentiment": "Brugerens holdning",
"time_header": "Tid",
"from_header": "Fra",
"custom_plan": "Tilpasset plan",
"email_embed": "E-mail indlejring",
"add_times_to_your_email": "Vælg nogle tilgængelige tidspunkter og indlejre dem i din e-mail",
@@ -2752,6 +2782,8 @@
"account_already_linked": "Kontoen er allerede forbundet",
"send_email": "Send e-mail",
"cal_ai_phone_call_action": "Ring til deltager ved hjælp af Cal.ai Voice Agent",
"call_to_confirm_booking": "Opkald for at bekræfte booking",
"cal_ai_phone_call_action_description": "2 timer før begivenheden starter",
"cal_ai_agent_configuration": "Cal.ai-agentkonfiguration",
"choose_at_least_one_event_type_test_call": "Vælg venligst mindst én begivenhedstype for at foretage et testopkald.",
"mark_as_no_show": "Markér som udeblevet",
@@ -3203,9 +3235,15 @@
"verify_email_change": "Bekræft ændring af e-mail",
"buy_credits": "Køb credits",
"credits": "Credits",
"view_and_manage_credits": "Se og administrer credits",
"credits_used": "Brugte credits",
"total_credits_remaining": "Samlet tilbageværende",
"credits_per_tip_org": "Du modtager 1000 credits om måneden pr. teammedlem",
"credits_per_tip_teams": "Du modtager 750 credits om måneden pr. teammedlem",
"view_and_manage_credits": "Se og administrer credits til at sende SMS-beskeder",
"view_and_manage_credits_description": "Se og administrer credits til at sende SMS-beskeder. Én credit er 1¢ (USD). <0>Læs mere</0>",
"credit_worth_description": "Én credit er 1¢ (USD) værd. <0>Læs mere</0>",
"buy_additional_credits": "Køb ekstra credits ($0,01 pr. credit)",
"view_additional_credits_expense_tip": "Du kan se yderligere kreditforbrug i din udgiftslog",
"overview": "Oversigt",
"organization_slug_taken": "Organisationens slug er allerede taget",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Du kan ikke oprette en organisation, da du allerede er en del af en organisation",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "Dit Cal.com-team {{teamName}} er løbet tør for credits. Som følge heraf bliver SMS-beskeder nu sendt via e-mail i stedet. For at genoptage afsendelse af SMS skal du købe flere credits.",
"credit_limit_reached_message_user": "Din Cal.com-konto er løbet tør for credits. Som følge heraf bliver SMS-beskeder nu sendt via e-mail i stedet. For at genoptage afsendelse af SMS'er skal du købe flere credits.",
"current_credit_balance": "Nuværende saldo: {{balance}} credits",
"current_balance": "Nuværende saldo:",
"notification_about_your_booking": "Notifikation om din booking",
"monthly_credits": "Månedlige credits",
"total_credits": "Samlede credits: {{totalCredits}}",
"remaining_credits": "Resterende credits: {{remainingCredits}}",
"remaining": "Tilbageværende",
"total": "Samlet",
"additional_credits": "Ekstra credits",
"routing_form_next_in_queue": "{{count}} næste i køen",
"routing_form_select_members_to_email": "Send e-mail-svar til",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "Se eksisterende workflows og deres konfigurationer",
"pbac_desc_update_workflows": "Rediger og tilpas workflowindstillinger",
"pbac_desc_delete_workflows": "Fjern workflows fra systemet",
"pbac_resource_webhook": "Webhook",
"pbac_desc_create_webhooks": "Opret webhooks",
"pbac_desc_view_webhooks": "Se webhooks",
"pbac_desc_update_webhooks": "Opdater webhooks",
"pbac_desc_delete_webhooks": "Slet webhooks",
"pbac_desc_manage_workflows": "Fuld administrationsadgang til alle workflows",
"pbac_desc_create_event_types": "Opret begivenhedstyper",
"pbac_desc_view_event_types": "Vis begivenhedstyper",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "Navnet på triggerbegivenheden (f.eks. BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "Tidspunktet for webhooken",
"webhook_type": "Slug for begivenhedstypen",
"set_up_agent": "Opsæt agent",
"webhook_title": "Navnet på begivenhedstypen",
"webhook_start_time": "Begivenhedens starttidspunkt",
"webhook_end_time": "Begivenhedens sluttidspunkt",
@@ -3625,6 +3672,9 @@
"visit": "Besøg",
"location_custom_label_input_label": "Brugerdefineret etiket på bookingsiden",
"meeting_link": "Mødelink",
"session_outcome": "Sessionens resultat",
"call_created": "Opkald oprettet",
"voicemail": "Telefonsvarer",
"my_bookings": "Mine bookinger",
"phone": "Telefon",
"free": "Gratis",
@@ -3632,6 +3682,8 @@
"user_name": "Brugers navn",
"expand_panel": "Udvid panel",
"collapse_panel": "Skjul panel",
"email_verification_required": "Emailbekræftelse er påkrævet for denne type begivenhed",
"invalid_verification_code": "Ugyldig bekræftelseskode angivet",
"you_have_one_team": "Du har ét team",
"consider_consolidating_one_team_org": "Overvej at oprette en organisation for at samle fakturering, adminværktøjer og analyser på tværs af dit team.",
"consider_consolidating_multi_team_org": "Overvej at oprette en organisation for at samle fakturering, adminværktøjer og analyser på tværs af dine teams.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "Før den planlagte starttid",
"cancel_booking_acknowledge_no_show_fee": "Jeg anerkender, at ved at aflyse bookingen inden for {{timeValue}} {{timeUnit}} før starttiden, vil jeg blive opkrævet gebyret for udeblivelse på {{amount, currency}}",
"contact_organizer": "Hvis du har spørgsmål, bedes du kontakte arrangøren.",
"booking_time_option": "Bookingtid",
"booking_time_option_description": "Når bookingen er planlagt (start til slut)",
"created_at_option": "Oprettet den",
"created_at_option_description": "Når bookingen oprindeligt blev oprettet",
"call_details": "Opkaldsdetaljer",
"call_id": "Opkalds-ID",
"call_information": "Opkaldsinformation",
"sentiment": "Stemning",
"disconnect_reason": "Afbrydelsesårsag",
"call_summary": "Opkaldsoversigt",
"transcription": "Transskription",
"event_details": "Begivenhedsdetaljer",
"agent": "Agent",
"no_transcript_available": "Ingen transskription tilgængelig",
"testing_sms_workflow_info_message": "Når du tester denne arbejdsgang, skal du være opmærksom på, at SMS'er skal planlægges mindst 15 minutter i forvejen",
"start_from_scratch_title": "Start fra bunden",
"start_from_scratch_description": "Opret din egen arbejdsgang fra bunden.",
"cal_ai_template_title": "Cal.ai-skabelon",
"cal_ai_template_description": "AI-agenter, der booker møder, sender påmindelser og følger op!",
"voice": "Stemme",
"select_voice": "Vælg stemme",
"select_voice_for_agent": "Vælg en stemme til din agent",
"choose_a_voice_for_your_agent": "Vælg en stemme til din agent",
"trait": "Egenskab",
"voice_id": "Stemme-ID",
"use_voice": "Brug stemme",
"current_voice": "Nuværende stemme",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+84 -5
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die Zustellbarkeit von E-Mails und Kalenderbenachrichtigungen zu gewährleisten",
"verify_email_email_header": "Bestätigen Sie Ihre E-Mail-Adresse",
"verify_email_button": "E-Mail bestätigen",
"cal_ai_assistant": "Cal KI-Assistent",
"cal_ai_assistant": "Assistent",
"send_cal_video_transcription_emails": "Cal Video-Transkriptions-E-Mails senden",
"description_send_cal_video_transcription_emails": "E-Mails mit der Transkription des Cal Videos nach Beendigung des Meetings versenden. (Erfordert einen kostenpflichtigen Plan)",
"verify_email_change_description": "Sie haben vor kurzem angefordert, die E-Mail-Adresse zu ändern, die Sie verwenden, um sich bei Ihrem {{appName}} Konto anzumelden. Bitte klicken Sie auf die Schaltfläche unten, um Ihre neue E-Mail-Adresse zu bestätigen.",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "Workflow-Validierung fehlgeschlagen",
"workflow_validation_empty_fields": "Ein oder mehrere Workflow-Schritte haben leere Nachrichteninhalte",
"workflow_validation_unverified_contacts": "Eine oder mehrere Telefonnummern oder E-Mail-Adressen sind nicht verifiziert",
"supercharge_your_workflows_with_cal_ai": "Optimiere deine Arbeitsabläufe mit Cal.ai",
"supercharge_your_workflows_with_cal_ai_description": "Lebensechte KI-Agenten, die Meetings buchen, Erinnerungen senden und mit deinen Kunden Nachfassen.",
"phone_number_imported_successfully": "Telefonnummer erfolgreich importiert und mit Agent verknüpft",
"phone_number_deleted_successfully": "Telefonnummer erfolgreich gelöscht",
"delete_phone_number_confirmation": "Sind Sie sicher, dass Sie diese Telefonnummer löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "Telefonnummer-Abonnement erfolgreich gekündigt",
"updating": "Aktualisierung läuft",
"round_robin": "Round Robin",
"hi_how_are_you_doing": "Hallo, wie geht es dir?",
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
"managed_event": "Verwaltetes Ereignis",
"username_placeholder": "Benutzername",
@@ -878,7 +881,7 @@
"the_action_will_disconnect_phone_number": "Diese Aktion wird die Telefonnummer vom Agent trennen. Der Agent kann keine Anrufe tätigen, bis eine neue Telefonnummer verbunden wird.",
"cal_ai_phone_numbers": "Telefonnummern",
"connect_phone_number": "Telefonnummer verbinden",
"cal_ai_phone_numbers_description": "Verwalten Sie Ihre Telefonnummern",
"cal_ai_phone_numbers_description": "Verwalte deine Telefonnummern",
"import_number": "Nummer importieren",
"this_action_will_also": "Diese Aktion wird auch:",
"import_phone_number": "Telefonnummer importieren",
@@ -886,7 +889,7 @@
"cancel_your_phone_number_subscription": "Ihr Telefonnummer-Abonnement kündigen",
"delete_associated_phone_number": "Die zugehörige Telefonnummer löschen",
"unauthorized_create_workflow": "Sie sind nicht berechtigt, diesen Workflow zu erstellen",
"import_phone_number_description": "Importieren Sie Ihre Twilio-Telefonnummer zur Verwendung mit Phone",
"import_phone_number_description": "Importiere deine Twilio-Telefonnummer zur Verwendung mit Telefon",
"phone_number_cost": "${{price}}/Monat",
"buy_new_number": "Neue Nummer kaufen",
"buy_number_cost_x_per_month": "Der Kauf einer Telefonnummer kostet ${{priceInDollars}} pro Monat. Ihnen wird monatlich für jede aktive Telefonnummer eine Gebühr berechnet.",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "Ja, Abonnement kündigen",
"cancel_phone_number_subscription_confirmation": "Sind Sie sicher, dass Sie dieses Telefonnummer-Abonnement kündigen möchten? Diese Aktion kann nicht rückgängig gemacht werden und Sie verlieren den Zugriff auf diese Telefonnummer.",
"add_members": "Mitglieder hinzufügen...",
"add_members_no_ellipsis": "Mitglieder hinzufügen",
"no_assigned_members": "Keine zugeordneten Mitglieder",
"assigned_to": "Zugeordnet zu",
"you_must_be_logged_in_to": "Sie müssen bei {{url}} angemeldet sein",
@@ -1207,6 +1211,7 @@
"categories": "Kategorien",
"pricing": "Preise",
"learn_more": "Mehr erfahren",
"try_now": "Jetzt ausprobieren",
"privacy_policy": "Datenschutzerklärung",
"terms_of_service": "Nutzungsbedingungen",
"remove": "Entfernen",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "WhatsApp-Nachricht an Teilnehmer senden",
"workflows": "Workflows",
"new_workflow_btn": "Neuer Workflow",
"how_would_you_like_to_start": "Wie möchtest du beginnen?",
"add_new_workflow": "Einen neuen Workflow hinzufügen",
"reschedule_event_trigger": "wenn das Ereignis neu geplant wird",
"trigger": "Auslöser",
@@ -1716,6 +1722,8 @@
"event_duration_info": "Die Dauer des Ereignisses",
"event_time_info": "Die Startzeit des Termins",
"event_type_not_found": "Ereignistyp nicht gefunden",
"number_to_call_variable": "Anzurufende Nummer",
"number_to_call_info": "Die Telefonnummer des Benutzers, den du anrufst",
"location_variable": "Ort",
"location_info": "Der Ort des Events",
"additional_notes_variable": "Zusätzliche Notizen",
@@ -1753,6 +1761,7 @@
"team_url": "Team-URL",
"team_members": "Teammitglieder",
"more": "Mehr",
"cal_ai_workflows": "Cal.ai Arbeitsabläufe",
"and_count_more": "und {{count}} mehr",
"more_page_footer": "Wir betrachten die mobile Anwendung als Erweiterung der Web-Anwendung. Wenn Sie komplizierte Aktionen durchführen, verwenden Sie bitte die Web-Anwendung.",
"workflow_example_1": "Eine Erinnerung per Mail an die Teilnehmer verschicken, 24 Stunden bevor das Event startet",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "Eine Erinnerung per SMS an die Teilnehmer verschicken, 24 Stunden bevor das Event startet",
"workflow_example_5": "Benutzerdefinierte E-Mail senden, wenn der Termin zum Moderieren neu geplant wird",
"workflow_example_6": "Benutzerdefinierte SMS senden, wenn der Termin zum Moderieren gebucht wird",
"send_sms_reminder": "SMS-Erinnerung senden",
"send_sms_reminder_description": "24 Stunden vor Terminbeginn",
"follow_up_with_no_shows": "Nachfassen bei Nichterscheinen",
"follow_up_with_no_shows_description": "30 Minuten nach Terminende",
"remind_attendees_to_bring_id": "Teilnehmer erinnern, Ausweis mitzubringen",
"remind_attendees_to_bring_id_description": "1 Tag vor Terminbeginn",
"email_to_remind_booking": "E-Mail-Erinnerung",
"email_to_remind_booking_description": "1 Stunde vor Terminbeginn",
"custom_sms_reminder": "Benutzerdefinierte SMS-Erinnerung",
"custom_sms_reminder_description": "Wenn Termin geplant wird",
"custom_email_reminder": "Benutzerdefinierte E-Mail-Erinnerung",
"custom_email_reminder_description": "Termin wird für Gastgeber umgeplant",
"count_managed_to_limit": "Buchungszahlen von verwalteten Ereignistypen einbeziehen",
"welcome_to_cal_header": "Willkommen bei {{appName}}!",
"edit_form_later_subtitle": "Du kannst dies später bearbeiten.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "Auf der Buchungsseite anzeigen",
"visit_cancelled_booking": "Sie können die Seite der stornierten Buchung besuchen",
"get_started_zapier_templates": "Legen Sie mit Zapier-Vorlagen los",
"standard_templates": "Standardvorlagen",
"cal_ai_templates": "Cal.ai-Vorlagen",
"team_is_unpublished": "{{team}} ist unveröffentlicht",
"org_is_unpublished_description": "Dieser Organisations-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den Organisations-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
"team_is_unpublished_description": "Dieser {{entity}}-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den {{entity}}-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "Karte konnte nicht für die Zahlung belastet werden.",
"insights": "Erkenntnisse",
"routing_forms": "Weiterleitungsformulare",
"testing_workflow_info_message": "Bitte beachten Sie beim Testen dieses Workflows, dass E-Mails und SMS spätestens 1 Stunde im Voraus geplant werden können",
"insights_no_data_found_for_filter": "Keine Daten für den ausgewählten Filter oder das ausgewählte Datum gefunden.",
"acknowledge_booking_no_show_fee": "Ich bestätige, dass meiner Karte eine Gebühr von {{amount, currency}} berechnet wird, sofern ich nicht an diesem Ereignis teilnehme.",
"days": "Tage",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Benutzer: {{userName}}",
"insights_subtitle": "Erfahren Sie mehr über Ihre Termine und Ihr Team",
"call_history": "Anrufverlauf",
"call_history_subtitle": "Anrufverlauf aller Cal.ai-Anrufe anzeigen",
"location_options": "{{locationCount}} Veranstaltungsort-Optionen",
"channel_type": "Kanaltyp",
"end_reason": "Beendigungsgrund",
"session_status": "Sitzungsstatus",
"user_sentiment": "Nutzer-Stimmung",
"time_header": "Zeit",
"from_header": "Von",
"custom_plan": "Maßgeschneiderter Tarif",
"email_embed": "E-Mail Einbettung",
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
@@ -2752,6 +2782,8 @@
"account_already_linked": "Konto ist bereits verknüpft",
"send_email": "E-Mail senden",
"cal_ai_phone_call_action": "Teilnehmer mit Cal.ai Sprachassistent anrufen",
"call_to_confirm_booking": "Anruf zur Buchungsbestätigung",
"cal_ai_phone_call_action_description": "2 Std. vor Terminbeginn",
"cal_ai_agent_configuration": "Cal.ai Agent-Konfiguration",
"choose_at_least_one_event_type_test_call": "Bitte wählen Sie mindestens einen Ereignistyp aus, um einen Testanruf zu tätigen.",
"mark_as_no_show": "Als No-Show markieren",
@@ -3203,9 +3235,15 @@
"verify_email_change": "E-Mail-Änderung bestätigen",
"buy_credits": "Credits kaufen",
"credits": "Credits",
"view_and_manage_credits": "Credits anzeigen und verwalten",
"credits_used": "Verbrauchte Credits",
"total_credits_remaining": "Verbleibende Gesamtanzahl",
"credits_per_tip_org": "Sie erhalten 1000 Credits pro Monat pro Teammitglied",
"credits_per_tip_teams": "Sie erhalten 750 Credits pro Monat pro Teammitglied",
"view_and_manage_credits": "Credits für den Versand von SMS-Nachrichten anzeigen und verwalten",
"view_and_manage_credits_description": "Credits für das Senden von SMS-Nachrichten anzeigen und verwalten. Ein Credit entspricht 1¢ (USD). <0>Mehr erfahren</0>",
"credit_worth_description": "Ein Credit entspricht 1¢ (USD). <0>Mehr erfahren</0>",
"buy_additional_credits": "Zusätzliche Credits kaufen (0,01 $ pro Credit)",
"view_additional_credits_expense_tip": "Zusätzliche Credit-Ausgaben können Sie in Ihrem Ausgabenprotokoll einsehen",
"overview": "Übersicht",
"organization_slug_taken": "Der Organisations-Slug ist bereits vergeben",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Sie können keine Organisation erstellen, da Sie bereits Teil einer Organisation sind",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "Ihrem Cal.com-Team {{teamName}} sind die Credits ausgegangen. Infolgedessen werden SMS-Nachrichten jetzt per E-Mail gesendet. Um wieder SMS zu senden, kaufen Sie bitte zusätzliche Credits.",
"credit_limit_reached_message_user": "Dein Cal.com-Konto hat keine Credits mehr. Infolgedessen werden SMS-Nachrichten jetzt stattdessen per E-Mail gesendet. Um das Senden von SMS fortzusetzen, kaufe bitte zusätzliche Credits.",
"current_credit_balance": "Aktuelles Guthaben: {{balance}} Credits",
"current_balance": "Aktuelles Guthaben:",
"notification_about_your_booking": "Benachrichtigung über deine Buchung",
"monthly_credits": "Monatliche Credits",
"total_credits": "Gesamte Credits: {{totalCredits}}",
"remaining_credits": "Verbleibende Credits: {{remainingCredits}}",
"remaining": "Verbleibend",
"total": "Gesamt",
"additional_credits": "Zusätzliche Credits",
"routing_form_next_in_queue": "{{count}} nächste in der Warteschlange",
"routing_form_select_members_to_email": "E-Mail-Antworten senden an",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "Bestehende Workflows und ihre Konfigurationen anzeigen",
"pbac_desc_update_workflows": "Workflow-Einstellungen bearbeiten und ändern",
"pbac_desc_delete_workflows": "Workflows aus dem System entfernen",
"pbac_resource_webhook": "Webhook",
"pbac_desc_create_webhooks": "Webhooks erstellen",
"pbac_desc_view_webhooks": "Webhooks anzeigen",
"pbac_desc_update_webhooks": "Webhooks aktualisieren",
"pbac_desc_delete_webhooks": "Webhooks löschen",
"pbac_desc_manage_workflows": "Vollständiger Verwaltungszugriff auf alle Workflows",
"pbac_desc_create_event_types": "Ereignistypen erstellen",
"pbac_desc_view_event_types": "Ereignistypen anzeigen",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "Der Name des Auslöseereignisses (z.B. BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "Der Zeitpunkt des Webhooks",
"webhook_type": "Der Ereignistyp-Slug",
"set_up_agent": "Agent einrichten",
"webhook_title": "Der Name des Ereignistyps",
"webhook_start_time": "Die Startzeit des Ereignisses",
"webhook_end_time": "Die Endzeit des Ereignisses",
@@ -3625,6 +3672,9 @@
"visit": "Besuchen",
"location_custom_label_input_label": "Benutzerdefinierte Bezeichnung auf der Buchungsseite",
"meeting_link": "Meeting-Link",
"session_outcome": "Sitzungsergebnis",
"call_created": "Anruf erstellt",
"voicemail": "Sprachnachricht",
"my_bookings": "Meine Buchungen",
"phone": "Telefon",
"free": "Kostenlos",
@@ -3632,6 +3682,8 @@
"user_name": "Benutzername",
"expand_panel": "Panel erweitern",
"collapse_panel": "Panel einklappen",
"email_verification_required": "E-Mail-Verifizierung ist für diesen Ereignistyp erforderlich",
"invalid_verification_code": "Ungültiger Verifizierungscode angegeben",
"you_have_one_team": "Sie haben ein Team",
"consider_consolidating_one_team_org": "Erwägen Sie die Einrichtung einer Organisation, um Abrechnung, Administrationstools und Analysen für Ihr Team zu vereinheitlichen.",
"consider_consolidating_multi_team_org": "Erwägen Sie die Einrichtung einer Organisation, um Abrechnung, Administrationstools und Analysen für Ihre Teams zu vereinheitlichen.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "Vor dem geplanten Startzeitpunkt",
"cancel_booking_acknowledge_no_show_fee": "Ich bestätige, dass mir bei einer Stornierung innerhalb von {{timeValue}} {{timeUnit}} vor dem Startzeitpunkt die Ausfallgebühr von {{amount, currency}} berechnet wird",
"contact_organizer": "Bei Fragen wenden Sie sich bitte an den Organisator.",
"booking_time_option": "Buchungszeit",
"booking_time_option_description": "Wann die Buchung geplant ist (Anfang bis Ende)",
"created_at_option": "Erstellt am",
"created_at_option_description": "Wann die Buchung ursprünglich erstellt wurde",
"call_details": "Anrufdetails",
"call_id": "Anruf-ID",
"call_information": "Anrufinformationen",
"sentiment": "Stimmung",
"disconnect_reason": "Trennungsgrund",
"call_summary": "Anrufzusammenfassung",
"transcription": "Transkription",
"event_details": "Ereignisdetails",
"agent": "Agent",
"no_transcript_available": "Kein Transkript verfügbar",
"testing_sms_workflow_info_message": "Beachten Sie beim Testen dieses Workflows, dass SMS mindestens 15 Minuten im Voraus geplant werden müssen",
"start_from_scratch_title": "Von Grund auf neu beginnen",
"start_from_scratch_description": "Erstellen Sie Ihren eigenen Workflow von Grund auf.",
"cal_ai_template_title": "Cal.ai-Vorlage",
"cal_ai_template_description": "KI-Agenten, die Meetings buchen, Erinnerungen senden und nachfassen!",
"voice": "Stimme",
"select_voice": "Stimme auswählen",
"select_voice_for_agent": "Wähle eine Stimme für deinen Agenten",
"choose_a_voice_for_your_agent": "Wähle eine Stimme für deinen Agenten",
"trait": "Eigenschaft",
"voice_id": "Stimm-ID",
"use_voice": "Stimme verwenden",
"current_voice": "Aktuelle Stimme",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+85 -6
View File
@@ -23,7 +23,7 @@
"verify_email_banner_body": "Επαληθεύστε τη διεύθυνση email σας για να εξασφαλίσετε την καλύτερη δυνατή παράδοση email και ημερολογίου",
"verify_email_email_header": "Επιβεβαιώστε τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας",
"verify_email_button": "Επαλήθευση email",
"cal_ai_assistant": "Βοηθός Cal AI",
"cal_ai_assistant": "Βοηθός",
"send_cal_video_transcription_emails": "Αποστολή email απομαγνητοφώνησης Cal Video",
"description_send_cal_video_transcription_emails": "Αποστολή email με την απομαγνητοφώνηση του Cal Video μετά το τέλος της συνάντησης. (Απαιτείται συνδρομητικό πακέτο)",
"verify_email_change_description": "Πρόσφατα ζητήσατε να αλλάξετε τη διεύθυνση email που χρησιμοποιείτε για να συνδεθείτε στον λογαριασμό σας στο {{appName}}. Παρακαλούμε κάντε κλικ στο παρακάτω κουμπί για να επιβεβαιώσετε τη νέα διεύθυνση email σας.",
@@ -819,6 +819,8 @@
"workflow_validation_failed": "Η επικύρωση της ροής εργασίας απέτυχε",
"workflow_validation_empty_fields": "Ένα ή περισσότερα βήματα της ροής εργασίας έχουν κενό περιεχόμενο μηνύματος",
"workflow_validation_unverified_contacts": "Ένας ή περισσότεροι αριθμοί τηλεφώνου ή διευθύνσεις email δεν έχουν επαληθευτεί",
"supercharge_your_workflows_with_cal_ai": "Ενισχύστε τις ροές εργασίας σας με το Cal.ai",
"supercharge_your_workflows_with_cal_ai_description": "Ρεαλιστικοί AI πράκτορες που κλείνουν συναντήσεις, στέλνουν υπενθυμίσεις και επικοινωνούν με τους πελάτες σας.",
"phone_number_imported_successfully": "Ο αριθμός τηλεφώνου εισήχθη και συνδέθηκε με τον πράκτορα με επιτυχία",
"phone_number_deleted_successfully": "Ο αριθμός τηλεφώνου διαγράφηκε με επιτυχία",
"delete_phone_number_confirmation": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον αριθμό τηλεφώνου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
@@ -846,6 +848,7 @@
"phone_number_subscription_cancelled_successfully": "Η συνδρομή του αριθμού τηλεφώνου ακυρώθηκε με επιτυχία",
"updating": "Ενημέρωση",
"round_robin": "Round Robin",
"hi_how_are_you_doing": "Γεια, πώς είστε;",
"round_robin_description": "Κυκλική εναλλαγή συναντήσεων μεταξύ πολλών μελών της ομάδας.",
"managed_event": "Διαχειριζόμενη εκδήλωση",
"username_placeholder": "όνομα χρήστη",
@@ -876,9 +879,9 @@
"are_you_sure_you_want_to_delete_workflow_step": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το βήμα ροής εργασίας;",
"do_you_still_want_to_unsubscribe": "Θέλετε ακόμα να καταργήσετε την εγγραφή του αριθμού τηλεφώνου από αυτόν τον πράκτορα;",
"the_action_will_disconnect_phone_number": "Αυτή η ενέργεια θα αποσυνδέσει τον αριθμό τηλεφώνου από τον πράκτορα. Ο πράκτορας δεν θα μπορεί να πραγματοποιεί κλήσεις μέχρι να συνδεθεί ένας νέος αριθμός τηλεφώνου.",
"cal_ai_phone_numbers": "Αριθμοί τηλεφώνου Cal AI",
"cal_ai_phone_numbers": "Αριθμοί Τηλεφώνου",
"connect_phone_number": "Σύνδεση αριθμού τηλεφώνου",
"cal_ai_phone_numbers_description": "Διαχειριστείτε τους αριθμούς τηλεφώνου Cal AI",
"cal_ai_phone_numbers_description": "Διαχειριστείτε τους αριθμούς τηλεφώνου σας",
"import_number": "Εισαγωγή αριθμού",
"this_action_will_also": "Αυτή η ενέργεια επίσης θα:",
"import_phone_number": "Εισαγωγή αριθμού τηλεφώνου",
@@ -886,7 +889,7 @@
"cancel_your_phone_number_subscription": "Ακύρωση της συνδρομής του αριθμού τηλεφώνου σας",
"delete_associated_phone_number": "Διαγραφή του συσχετισμένου αριθμού τηλεφώνου",
"unauthorized_create_workflow": "Δεν έχετε εξουσιοδότηση για τη δημιουργία αυτής της ροής εργασίας",
"import_phone_number_description": "Εισαγάγετε τον αριθμό τηλεφώνου Twilio για χρήση με το Phone",
"import_phone_number_description": "Εισαγάγετε τον αριθμό τηλεφώνου Twilio για χρήση με το Τηλέφωνο",
"phone_number_cost": "${{price}}/μήνα",
"buy_new_number": "Αγορά νέου αριθμού",
"buy_number_cost_x_per_month": "Η αγορά ενός αριθμού τηλεφώνου κοστίζει ${{priceInDollars}} ανά μήνα. Θα χρεώνεστε μηνιαία για κάθε ενεργό αριθμό τηλεφώνου.",
@@ -897,6 +900,7 @@
"yes_cancel_subscription": "Ναι, ακύρωση συνδρομής",
"cancel_phone_number_subscription_confirmation": "Είστε βέβαιοι ότι θέλετε να ακυρώσετε αυτή τη συνδρομή αριθμού τηλεφώνου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί και θα χάσετε την πρόσβαση σε αυτόν τον αριθμό τηλεφώνου.",
"add_members": "Προσθήκη μελών...",
"add_members_no_ellipsis": "Προσθήκη μελών",
"no_assigned_members": "Δεν υπάρχουν ανατεθειμένα μέλη",
"assigned_to": "Ανατέθηκε σε",
"you_must_be_logged_in_to": "Πρέπει να είστε συνδεδεμένοι στο {{url}}",
@@ -1207,6 +1211,7 @@
"categories": "Κατηγορίες",
"pricing": "Τιμολόγηση",
"learn_more": "Μάθετε περισσότερα",
"try_now": "Δοκιμάστε τώρα",
"privacy_policy": "Πολιτική απορρήτου",
"terms_of_service": "Όροι χρήσης",
"remove": "Αφαίρεση",
@@ -1440,6 +1445,7 @@
"whatsapp_attendee_action": "αποστολή μηνύματος WhatsApp στον συμμετέχοντα",
"workflows": "Ροές εργασίας",
"new_workflow_btn": "Νέα ροή εργασίας",
"how_would_you_like_to_start": "Πώς θα θέλατε να ξεκινήσετε;",
"add_new_workflow": "Προσθήκη νέας ροής εργασίας",
"reschedule_event_trigger": "όταν επαναπρογραμματίζεται το συμβάν",
"trigger": "Έναυσμα",
@@ -1716,6 +1722,8 @@
"event_duration_info": "Η διάρκεια της εκδήλωσης",
"event_time_info": "Η ώρα έναρξης της εκδήλωσης",
"event_type_not_found": "Ο τύπος εκδήλωσης δεν βρέθηκε",
"number_to_call_variable": "Αριθμός κλήσης",
"number_to_call_info": "Ο αριθμός τηλεφώνου του χρήστη που καλείτε",
"location_variable": "Τοποθεσία",
"location_info": "Η τοποθεσία της εκδήλωσης",
"additional_notes_variable": "Πρόσθετες σημειώσεις",
@@ -1753,6 +1761,7 @@
"team_url": "URL ομάδας",
"team_members": "Μέλη ομάδας",
"more": "Περισσότερα",
"cal_ai_workflows": "Ροές εργασίας Cal.ai",
"and_count_more": "και {{count}} ακόμη",
"more_page_footer": "Θεωρούμε την εφαρμογή για κινητά ως επέκταση της διαδικτυακής εφαρμογής. Εάν εκτελείτε περίπλοκες ενέργειες, παρακαλούμε ανατρέξτε στη διαδικτυακή εφαρμογή.",
"workflow_example_1": "Αποστολή υπενθύμισης SMS 24 ώρες πριν την έναρξη της εκδήλωσης στον συμμετέχοντα",
@@ -1761,6 +1770,18 @@
"workflow_example_4": "Αποστολή υπενθύμισης email 1 ώρα πριν την έναρξη της εκδήλωσης στον συμμετέχοντα",
"workflow_example_5": "Αποστολή προσαρμοσμένου email στον διοργανωτή όταν η εκδήλωση επαναπρογραμματιστεί",
"workflow_example_6": "Αποστολή προσαρμοσμένου SMS στον διοργανωτή όταν γίνει κράτηση νέας εκδήλωσης",
"send_sms_reminder": "Αποστολή υπενθύμισης SMS",
"send_sms_reminder_description": "24 ώρες πριν την έναρξη του συμβάντος",
"follow_up_with_no_shows": "Επικοινωνία με όσους δεν εμφανίστηκαν",
"follow_up_with_no_shows_description": "30 λεπτά μετά το τέλος του συμβάντος",
"remind_attendees_to_bring_id": "Υπενθύμιση στους συμμετέχοντες να φέρουν ταυτότητα",
"remind_attendees_to_bring_id_description": "1 ημέρα πριν την έναρξη του συμβάντος",
"email_to_remind_booking": "Υπενθύμιση μέσω Email",
"email_to_remind_booking_description": "1 ώρα πριν την έναρξη του συμβάντος",
"custom_sms_reminder": "Προσαρμοσμένη υπενθύμιση SMS",
"custom_sms_reminder_description": "Όταν προγραμματίζεται το συμβάν",
"custom_email_reminder": "Προσαρμοσμένη υπενθύμιση Email",
"custom_email_reminder_description": "Το συμβάν επαναπρογραμματίζεται για τον διοργανωτή",
"count_managed_to_limit": "Συμπερίληψη αριθμού κρατήσεων από διαχειριζόμενους τύπους εκδηλώσεων",
"welcome_to_cal_header": "Καλώς ήρθατε στο {{appName}}!",
"edit_form_later_subtitle": "Θα μπορείτε να το επεξεργαστείτε αργότερα.",
@@ -2184,6 +2205,8 @@
"show_on_booking_page": "Εμφάνιση στη σελίδα κράτησης",
"visit_cancelled_booking": "Μπορείτε να επισκεφθείτε τη σελίδα ακυρωμένων κρατήσεων",
"get_started_zapier_templates": "Ξεκινήστε με τα πρότυπα του Zapier",
"standard_templates": "Τυπικά Πρότυπα",
"cal_ai_templates": "Πρότυπα Cal.ai",
"team_is_unpublished": "Η ομάδα {{team}} δεν έχει δημοσιευτεί",
"org_is_unpublished_description": "Αυτός ο σύνδεσμος οργανισμού δεν είναι διαθέσιμος αυτή τη στιγμή. Παρακαλούμε επικοινωνήστε με τον ιδιοκτήτη του οργανισμού ή ζητήστε του να τον δημοσιεύσει.",
"team_is_unpublished_description": "Αυτός ο σύνδεσμος ομάδας δεν είναι διαθέσιμος αυτή τη στιγμή. Παρακαλούμε επικοινωνήστε με τον ιδιοκτήτη της ομάδας ή ζητήστε του να τον δημοσιεύσει.",
@@ -2341,7 +2364,6 @@
"could_not_charge_card": "Δεν ήταν δυνατή η χρέωση της κάρτας για την πληρωμή.",
"insights": "Αναλύσεις",
"routing_forms": "Φόρμες δρομολόγησης",
"testing_workflow_info_message": "Κατά τη δοκιμή αυτής της ροής εργασίας, λάβετε υπόψη ότι τα email και τα SMS μπορούν να προγραμματιστούν τουλάχιστον 1 ώρα νωρίτερα",
"insights_no_data_found_for_filter": "Δεν βρέθηκαν δεδομένα για το επιλεγμένο φίλτρο ή τις επιλεγμένες ημερομηνίες.",
"acknowledge_booking_no_show_fee": "Αναγνωρίζω ότι εάν δεν παρευρεθώ σε αυτήν την εκδήλωση, θα χρεωθεί στην κάρτα μου τέλος μη προσέλευσης ύψους {{amount, currency}}.",
"days": "ημέρες",
@@ -2464,7 +2486,15 @@
"insights_team_filter": "Ομάδα: {{teamName}}",
"insights_user_filter": "Χρήστης: {{userName}}",
"insights_subtitle": "Δείτε στατιστικά κρατήσεων για τις εκδηλώσεις σας",
"call_history": "Ιστορικό Κλήσεων",
"call_history_subtitle": "Προβολή ιστορικού κλήσεων σε όλες τις κλήσεις Cal.ai",
"location_options": "{{locationCount}} επιλογές τοποθεσίας",
"channel_type": "Τύπος Καναλιού",
"end_reason": "Αιτία Τερματισμού",
"session_status": "Κατάσταση Συνεδρίας",
"user_sentiment": "Διάθεση Χρήστη",
"time_header": "Ώρα",
"from_header": "Από",
"custom_plan": "Προσαρμοσμένο πλάνο",
"email_embed": "Ενσωμάτωση στο email",
"add_times_to_your_email": "Επιλέξτε μερικές διαθέσιμες ώρες και ενσωματώστε τις στο email σας",
@@ -2752,6 +2782,8 @@
"account_already_linked": "Ο λογαριασμός είναι ήδη συνδεδεμένος",
"send_email": "Αποστολή email",
"cal_ai_phone_call_action": "Κλήση συμμετέχοντα χρησιμοποιώντας τον Φωνητικό Πράκτορα Cal.ai",
"call_to_confirm_booking": "Κλήση για επιβεβαίωση κράτησης",
"cal_ai_phone_call_action_description": "2 ώρες πριν την έναρξη του συμβάντος",
"cal_ai_agent_configuration": "Διαμόρφωση πράκτορα Cal.ai",
"choose_at_least_one_event_type_test_call": "Παρακαλώ επιλέξτε τουλάχιστον έναν τύπο εκδήλωσης για να κάνετε μια δοκιμαστική κλήση.",
"mark_as_no_show": "Σήμανση ως μη εμφάνιση",
@@ -3203,9 +3235,15 @@
"verify_email_change": "Επαλήθευση αλλαγής email",
"buy_credits": "Αγορά μονάδων",
"credits": "Μονάδες",
"view_and_manage_credits": "Προβολή και διαχείριση μονάδων",
"credits_used": "Πιστώσεις που χρησιμοποιήθηκαν",
"total_credits_remaining": "Συνολικό υπόλοιπο",
"credits_per_tip_org": "Λαμβάνετε 1000 πιστώσεις ανά μήνα, ανά μέλος της ομάδας",
"credits_per_tip_teams": "Λαμβάνετε 750 πιστώσεις ανά μήνα, ανά μέλος της ομάδας",
"view_and_manage_credits": "Προβολή και διαχείριση πιστώσεων για αποστολή μηνυμάτων SMS",
"view_and_manage_credits_description": "Προβολή και διαχείριση μονάδων για αποστολή μηνυμάτων SMS. Μία μονάδα αξίζει 1¢ (USD). <0>Μάθετε περισσότερα</0>",
"credit_worth_description": "Μία πίστωση αξίζει 1¢ (USD). <0>Μάθετε περισσότερα</0>",
"buy_additional_credits": "Αγορά επιπλέον μονάδων ($0.01 ανά μονάδα)",
"view_additional_credits_expense_tip": "Μπορείτε να δείτε τις επιπλέον δαπάνες πιστώσεων στο αρχείο εξόδων σας",
"overview": "Επισκόπηση",
"organization_slug_taken": "Το αναγνωριστικό του οργανισμού χρησιμοποιείται ήδη",
"you_cannot_create_an_organization_as_you_are_already_part_of_an_organization": "Δεν μπορείτε να δημιουργήσετε έναν οργανισμό καθώς είστε ήδη μέλος ενός οργανισμού",
@@ -3386,10 +3424,13 @@
"credit_limit_reached_message": "Η ομάδα σας {{teamName}} στο Cal.com έχει εξαντλήσει τις μονάδες της. Ως αποτέλεσμα, τα μηνύματα SMS αποστέλλονται τώρα μέσω email. Για να συνεχίσετε την αποστολή SMS, παρακαλούμε αγοράστε επιπλέον μονάδες.",
"credit_limit_reached_message_user": "Ο λογαριασμός σας στο Cal.com έχει εξαντλήσει τις μονάδες του. Ως αποτέλεσμα, τα μηνύματα SMS αποστέλλονται τώρα μέσω email. Για να συνεχίσετε την αποστολή SMS, παρακαλούμε αγοράστε επιπλέον μονάδες.",
"current_credit_balance": "Τρέχον υπόλοιπο: {{balance}} μονάδες",
"current_balance": "Τρέχον υπόλοιπο:",
"notification_about_your_booking": "Ειδοποίηση σχετικά με την κράτησή σας",
"monthly_credits": "Μηνιαίες μονάδες",
"total_credits": "Συνολικές μονάδες: {{totalCredits}}",
"remaining_credits": "Υπολειπόμενες μονάδες: {{remainingCredits}}",
"remaining": "Υπόλοιπο",
"total": "Σύνολο",
"additional_credits": "Επιπλέον μονάδες",
"routing_form_next_in_queue": "{{count}} επόμενα στην ουρά",
"routing_form_select_members_to_email": "Αποστολή απαντήσεων email σε",
@@ -3467,6 +3508,11 @@
"pbac_desc_view_workflows": "Προβολή υπαρχουσών ροών εργασίας και των διαμορφώσεών τους",
"pbac_desc_update_workflows": "Επεξεργασία και τροποποίηση ρυθμίσεων ροών εργασίας",
"pbac_desc_delete_workflows": "Αφαίρεση ροών εργασίας από το σύστημα",
"pbac_resource_webhook": "Webhook",
"pbac_desc_create_webhooks": "Δημιουργία webhooks",
"pbac_desc_view_webhooks": "Προβολή webhooks",
"pbac_desc_update_webhooks": "Ενημέρωση webhooks",
"pbac_desc_delete_webhooks": "Διαγραφή webhooks",
"pbac_desc_manage_workflows": "Πλήρης πρόσβαση διαχείρισης σε όλες τις ροές εργασίας",
"pbac_desc_create_event_types": "Δημιουργία τύπων εκδηλώσεων",
"pbac_desc_view_event_types": "Προβολή τύπων εκδηλώσεων",
@@ -3594,6 +3640,7 @@
"webhook_trigger_event": "Το όνομα του συμβάντος ενεργοποίησης (π.χ., BOOKING_CREATED, BOOKING_CANCELLED)",
"webhook_created_at": "Η ώρα του webhook",
"webhook_type": "Το slug του τύπου εκδήλωσης",
"set_up_agent": "Ρύθμιση Πράκτορα",
"webhook_title": "Το όνομα του τύπου εκδήλωσης",
"webhook_start_time": "Η ώρα έναρξης της εκδήλωσης",
"webhook_end_time": "Η ώρα λήξης της εκδήλωσης",
@@ -3625,6 +3672,9 @@
"visit": "Επίσκεψη",
"location_custom_label_input_label": "Προσαρμοσμένη ετικέτα στη σελίδα κράτησης",
"meeting_link": "Σύνδεσμος συνάντησης",
"session_outcome": "Αποτέλεσμα Συνεδρίας",
"call_created": "Δημιουργήθηκε Κλήση",
"voicemail": "Φωνητικό Μήνυμα",
"my_bookings": "Οι κρατήσεις μου",
"phone": "Τηλέφωνο",
"free": "Δωρεάν",
@@ -3632,6 +3682,8 @@
"user_name": "Όνομα χρήστη",
"expand_panel": "Ανάπτυξη πάνελ",
"collapse_panel": "Σύμπτυξη πάνελ",
"email_verification_required": "Απαιτείται επαλήθευση email για αυτόν τον τύπο εκδήλωσης",
"invalid_verification_code": "Παρέχεται μη έγκυρος κωδικός επαλήθευσης",
"you_have_one_team": "Έχετε μία ομάδα",
"consider_consolidating_one_team_org": "Σκεφτείτε να δημιουργήσετε έναν οργανισμό για να ενοποιήσετε τη χρέωση, τα εργαλεία διαχείρισης και τα αναλυτικά στοιχεία σε όλη την ομάδα σας.",
"consider_consolidating_multi_team_org": "Σκεφτείτε να δημιουργήσετε έναν οργανισμό για να ενοποιήσετε τη χρέωση, τα εργαλεία διαχείρισης και τα αναλυτικά στοιχεία σε όλες τις ομάδες σας.",
@@ -3652,5 +3704,32 @@
"before_scheduled_start_time": "Πριν την προγραμματισμένη ώρα έναρξης",
"cancel_booking_acknowledge_no_show_fee": "Αναγνωρίζω ότι με την ακύρωση της κράτησης εντός {{timeValue}} {{timeUnit}} από την ώρα έναρξης θα χρεωθώ το τέλος μη εμφάνισης ύψους {{amount, currency}}",
"contact_organizer": "Εάν έχετε οποιεσδήποτε ερωτήσεις, παρακαλώ επικοινωνήστε με τον διοργανωτή.",
"booking_time_option": "Χρόνος κράτησης",
"booking_time_option_description": "Όταν προγραμματίζεται η κράτηση (αρχή έως τέλος)",
"created_at_option": "Δημιουργήθηκε στις",
"created_at_option_description": "Όταν δημιουργήθηκε αρχικά η κράτηση",
"call_details": "Λεπτομέρειες Κλήσης",
"call_id": "Αναγνωριστικό Κλήσης",
"call_information": "Πληροφορίες Κλήσης",
"sentiment": "Συναίσθημα",
"disconnect_reason": "Αιτία Αποσύνδεσης",
"call_summary": "Περίληψη Κλήσης",
"transcription": "Απομαγνητοφώνηση",
"event_details": "Λεπτομέρειες Εκδήλωσης",
"agent": "Πράκτορας",
"no_transcript_available": "Δεν υπάρχει διαθέσιμη απομαγνητοφώνηση",
"testing_sms_workflow_info_message": "Κατά τη δοκιμή αυτής της ροής εργασίας, λάβετε υπόψη ότι τα SMS πρέπει να προγραμματιστούν τουλάχιστον 15 λεπτά νωρίτερα",
"start_from_scratch_title": "Ξεκινήστε από το μηδέν",
"start_from_scratch_description": "Δημιουργήστε τη δική σας ροή εργασίας από το μηδέν.",
"cal_ai_template_title": "Πρότυπο Cal.ai",
"cal_ai_template_description": "AI πράκτορες που κλείνουν συναντήσεις, στέλνουν υπενθυμίσεις και κάνουν follow up!",
"voice": "Φωνή",
"select_voice": "Επιλογή φωνής",
"select_voice_for_agent": "Επιλέξτε φωνή για τον πράκτορά σας",
"choose_a_voice_for_your_agent": "Επιλέξτε μια φωνή για τον πράκτορά σας",
"trait": "Χαρακτηριστικό",
"voice_id": "Αναγνωριστικό φωνής",
"use_voice": "Χρήση φωνής",
"current_voice": "Τρέχουσα φωνή",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

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