Merge branch 'main' of github.com:calcom/cal.com into refactor-event-types-type-id-10419-cal-2264-cal-2296

This commit is contained in:
Alan 2023-09-10 22:43:36 -07:00
commit 4df1064f4c
51 changed files with 242 additions and 113 deletions

View File

@ -70,6 +70,7 @@ const schemaEventTypeCreateParams = z
recurringEvent: recurringEventInputSchema.optional(), recurringEvent: recurringEventInputSchema.optional(),
seatsPerTimeSlot: z.number().optional(), seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(), seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(), bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(), scheduleId: z.number().optional(),
}) })
@ -89,6 +90,7 @@ const schemaEventTypeEditParams = z
length: z.number().int().optional(), length: z.number().int().optional(),
seatsPerTimeSlot: z.number().optional(), seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(), seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(), bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(), scheduleId: z.number().optional(),
}) })
@ -129,6 +131,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
metadata: true, metadata: true,
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingFields: true, bookingFields: true,
bookingLimits: true, bookingLimits: true,
durationLimits: true, durationLimits: true,

View File

@ -94,6 +94,9 @@ import { defaultResponder } from "@calcom/lib/server";
* seatsShowAttendees: * seatsShowAttendees:
* type: boolean * type: boolean
* description: 'Share Attendee information in seats' * description: 'Share Attendee information in seats'
* seatsShowAvailabilityCount:
* type: boolean
* description: 'Show the number of available seats'
* smsReminderNumber: * smsReminderNumber:
* type: number * type: number
* description: 'SMS reminder number' * description: 'SMS reminder number'

View File

@ -146,6 +146,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
* seatsShowAttendees: * seatsShowAttendees:
* type: boolean * type: boolean
* description: 'Share Attendee information in seats' * description: 'Share Attendee information in seats'
* seatsShowAvailabilityCount:
* type: boolean
* description: 'Show the number of available seats'
* locations: * locations:
* type: array * type: array
* description: A list of all available locations for the event type * description: A list of all available locations for the event type

View File

@ -377,6 +377,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultChecked={!!eventType.seatsShowAttendees} defaultChecked={!!eventType.seatsShowAttendees}
/> />
</div> </div>
<div className="mt-2">
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)}
defaultChecked={!!eventType.seatsShowAvailabilityCount}
/>
</div>
</div> </div>
)} )}
/> />

View File

@ -14,7 +14,6 @@ import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic"; import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider"; import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks"; import { useFlags } from "@calcom/features/flags/hooks";
import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui"; import { MetaProvider } from "@calcom/ui";
import useIsBookingPage from "@lib/hooks/useIsBookingPage"; import useIsBookingPage from "@lib/hooks/useIsBookingPage";
@ -222,19 +221,7 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
function useOrgBrandingValues() { function useOrgBrandingValues() {
const session = useSession(); const session = useSession();
return session?.data?.user.org;
const res = trpc.viewer.organizations.getBrand.useQuery(undefined, {
// Only fetch if we have a session to avoid flooding logs with errors
enabled: session.status === "authenticated",
});
if (res.status === "loading") {
return undefined;
}
if (res.status === "error") return null;
return res.data;
} }
function OrgBrandProvider({ children }: { children: React.ReactNode }) { function OrgBrandProvider({ children }: { children: React.ReactNode }) {

View File

@ -917,6 +917,7 @@ const getEventTypesFromDB = async (id: number) => {
metadata: true, metadata: true,
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
periodStartDate: true, periodStartDate: true,
periodEndDate: true, periodEndDate: true,
}, },
@ -940,6 +941,7 @@ const handleSeatsEventTypeOnBooking = async (
eventType: { eventType: {
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
seatsShowAttendees: boolean | null; seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
[x: string | number | symbol]: unknown; [x: string | number | symbol]: unknown;
}, },
bookingInfo: Partial< bookingInfo: Partial<

View File

@ -113,6 +113,7 @@ export type FormValues = {
periodDates: { startDate: Date; endDate: Date }; periodDates: { startDate: Date; endDate: Date };
seatsPerTimeSlot: number | null; seatsPerTimeSlot: number | null;
seatsShowAttendees: boolean | null; seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
seatsPerTimeSlotEnabled: boolean; seatsPerTimeSlotEnabled: boolean;
minimumBookingNotice: number; minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number; minimumBookingNoticeInDurationType: number;
@ -360,6 +361,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime, afterBufferTime,
seatsPerTimeSlot, seatsPerTimeSlot,
seatsShowAttendees, seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits, bookingLimits,
durationLimits, durationLimits,
recurringEvent, recurringEvent,
@ -426,6 +428,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits, durationLimits,
seatsPerTimeSlot, seatsPerTimeSlot,
seatsShowAttendees, seatsShowAttendees,
seatsShowAvailabilityCount,
metadata, metadata,
customInputs, customInputs,
children, children,
@ -460,6 +463,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime, afterBufferTime,
seatsPerTimeSlot, seatsPerTimeSlot,
seatsShowAttendees, seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits, bookingLimits,
durationLimits, durationLimits,
recurringEvent, recurringEvent,
@ -516,6 +520,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits, durationLimits,
seatsPerTimeSlot, seatsPerTimeSlot,
seatsShowAttendees, seatsShowAttendees,
seatsShowAvailabilityCount,
metadata, metadata,
customInputs, customInputs,
}); });

View File

@ -292,9 +292,9 @@
"add_another_calendar": "Add another calendar", "add_another_calendar": "Add another calendar",
"other": "Other", "other": "Other",
"email_sign_in_subject": "Your sign-in link for {{appName}}", "email_sign_in_subject": "Your sign-in link for {{appName}}",
"emailed_you_and_attendees": "We emailed you and the other attendees a calendar invitation with all the details.", "emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.",
"emailed_you_and_attendees_recurring": "We emailed you and the other attendees a calendar invitation for the first of these recurring events.", "emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
"emailed_you_and_any_other_attendees": "You and any other attendees have been emailed with this information.", "emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.",
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.", "needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
"needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.", "needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.",
"user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.", "user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.",
@ -974,6 +974,8 @@
"offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.", "offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.",
"seats_available_one": "Seat available", "seats_available_one": "Seat available",
"seats_available_other": "Seats available", "seats_available_other": "Seats available",
"seats_nearly_full": "Seats almost full",
"seats_half_full": "Seats filling fast",
"number_of_seats": "Number of seats per booking", "number_of_seats": "Number of seats per booking",
"enter_number_of_seats": "Enter number of seats", "enter_number_of_seats": "Enter number of seats",
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.", "you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
@ -1100,7 +1102,7 @@
"reschedule_optional": "Reason for rescheduling (optional)", "reschedule_optional": "Reason for rescheduling (optional)",
"reschedule_placeholder": "Let others know why you need to reschedule", "reschedule_placeholder": "Let others know why you need to reschedule",
"event_cancelled": "This event is canceled", "event_cancelled": "This event is canceled",
"emailed_information_about_cancelled_event": "We emailed you and the other attendees to let them know.", "emailed_information_about_cancelled_event": "We sent an email to everyone to let them know.",
"this_input_will_shown_booking_this_event": "This input will be shown when booking this event", "this_input_will_shown_booking_this_event": "This input will be shown when booking this event",
"meeting_url_in_confirmation_email": "Meeting url is in the confirmation email", "meeting_url_in_confirmation_email": "Meeting url is in the confirmation email",
"url_start_with_https": "URL needs to start with http:// or https://", "url_start_with_https": "URL needs to start with http:// or https://",
@ -1455,6 +1457,7 @@
"add_limit": "Add Limit", "add_limit": "Add Limit",
"team_name_required": "Team name required", "team_name_required": "Team name required",
"show_attendees": "Share attendee information between guests", "show_attendees": "Share attendee information between guests",
"show_available_seats_count": "Show the number of available seats",
"how_booking_questions_as_variables": "How to use booking questions as variables?", "how_booking_questions_as_variables": "How to use booking questions as variables?",
"format": "Format", "format": "Format",
"uppercase_for_letters": "Use uppercase for all letters", "uppercase_for_letters": "Use uppercase for all letters",

View File

@ -1888,7 +1888,7 @@
"organization_name": "Nombre de la organización", "organization_name": "Nombre de la organización",
"organization_url": "URL de la organización", "organization_url": "URL de la organización",
"organization_verify_header": "Verifique el correo electrónico de su organización", "organization_verify_header": "Verifique el correo electrónico de su organización",
"organization_verify_email_body": "Utilice el código a continuación para verificar su dirección de correo electrónico para seguir configurando su organización.", "organization_verify_email_body": "Utilice el siguiente código para verificar su dirección de correo electrónico y continuar con la configuración de su organización.",
"additional_url_parameters": "Parámetros adicionales de URL", "additional_url_parameters": "Parámetros adicionales de URL",
"about_your_organization": "Acerca de su organización", "about_your_organization": "Acerca de su organización",
"about_your_organization_description": "Las organizaciones son entornos compartidos donde puede crear varios equipos con miembros, tipos de eventos, aplicaciones, flujos de trabajo compartidos y más.", "about_your_organization_description": "Las organizaciones son entornos compartidos donde puede crear varios equipos con miembros, tipos de eventos, aplicaciones, flujos de trabajo compartidos y más.",

View File

@ -974,6 +974,8 @@
"offer_seats_description": "Proposez des places de réservation. Cela désactive automatiquement les réservations d'invités et d'opt-in.", "offer_seats_description": "Proposez des places de réservation. Cela désactive automatiquement les réservations d'invités et d'opt-in.",
"seats_available_one": "Place disponible", "seats_available_one": "Place disponible",
"seats_available_other": "Places disponibles", "seats_available_other": "Places disponibles",
"seats_nearly_full": "Places presque toutes occupées",
"seats_half_full": "Les places partent vite",
"number_of_seats": "Nombre de places par réservation", "number_of_seats": "Nombre de places par réservation",
"enter_number_of_seats": "Saisir le nombre de sièges", "enter_number_of_seats": "Saisir le nombre de sièges",
"you_can_manage_your_schedules": "Vous pouvez gérer vos disponibilités sur la page Disponibilité.", "you_can_manage_your_schedules": "Vous pouvez gérer vos disponibilités sur la page Disponibilité.",
@ -1455,6 +1457,7 @@
"add_limit": "Ajouter une limite", "add_limit": "Ajouter une limite",
"team_name_required": "Nom d'équipe requis", "team_name_required": "Nom d'équipe requis",
"show_attendees": "Partagez les informations des participants entre les invités", "show_attendees": "Partagez les informations des participants entre les invités",
"show_available_seats_count": "Afficher le nombre de places disponibles",
"how_booking_questions_as_variables": "Comment utiliser les questions de réservation comme variables ?", "how_booking_questions_as_variables": "Comment utiliser les questions de réservation comme variables ?",
"format": "Format", "format": "Format",
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.", "uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",

View File

@ -210,6 +210,8 @@
"done": "Učinjeno", "done": "Učinjeno",
"all_done": "Završeno!", "all_done": "Završeno!",
"all": "Sve", "all": "Sve",
"available_apps": "Dostupne Aplikacije",
"available_apps_lower_case": "Dostupne aplikacije",
"check_email_reset_password": "Provjerite svoju e-poštu. Poslali smo vam link za resetiranje lozinke.", "check_email_reset_password": "Provjerite svoju e-poštu. Poslali smo vam link za resetiranje lozinke.",
"finish": "Završi", "finish": "Završi",
"few_sentences_about_yourself": "Nekoliko rečenica o sebi. Ovo će se pojaviti na vašoj osobnoj stranici.", "few_sentences_about_yourself": "Nekoliko rečenica o sebi. Ovo će se pojaviti na vašoj osobnoj stranici.",
@ -331,5 +333,6 @@
"dark": "Tamna", "dark": "Tamna",
"automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba", "automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba",
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije", "user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
"full_name": "Puno ime",
"insights_all_org_filter": "Sve" "insights_all_org_filter": "Sve"
} }

View File

@ -308,7 +308,7 @@
"layout": "Raspored", "layout": "Raspored",
"bookerlayout_default_title": "Podrazumevani prikaz", "bookerlayout_default_title": "Podrazumevani prikaz",
"bookerlayout_description": "Možete da izaberete više njih, a vaši učesnici mogu da menjaju prikaze.", "bookerlayout_description": "Možete da izaberete više njih, a vaši učesnici mogu da menjaju prikaze.",
"bookerlayout_user_settings_title": "Raspored zakazivanja", "bookerlayout_user_settings_title": "Režim prikaza rezervacija",
"bookerlayout_user_settings_description": "Možete da izaberete više njih, a učesnici mogu da menjaju prikaz. Ovo se može zameniti za svaki događaj.", "bookerlayout_user_settings_description": "Možete da izaberete više njih, a učesnici mogu da menjaju prikaz. Ovo se može zameniti za svaki događaj.",
"bookerlayout_month_view": "Mesec", "bookerlayout_month_view": "Mesec",
"bookerlayout_week_view": "Nedeljno", "bookerlayout_week_view": "Nedeljno",
@ -404,7 +404,7 @@
"recording_ready": "Link za preuzimanje snimka je spreman", "recording_ready": "Link za preuzimanje snimka je spreman",
"booking_created": "Rezervacija Napravljena", "booking_created": "Rezervacija Napravljena",
"booking_rejected": "Rezevacija je odbijena", "booking_rejected": "Rezevacija je odbijena",
"booking_requested": "Rezervacija je zahtevana", "booking_requested": "Zahtev za rezervaciju je poslat",
"meeting_ended": "Sastanak se završio", "meeting_ended": "Sastanak se završio",
"form_submitted": "Formular poslat", "form_submitted": "Formular poslat",
"event_triggers": "Okidači Dogadjaja", "event_triggers": "Okidači Dogadjaja",
@ -552,11 +552,11 @@
"team_description": "Par rečenica o vašem timu. Ovo će se pojaviti na stranici URL adrese vašeg tima.", "team_description": "Par rečenica o vašem timu. Ovo će se pojaviti na stranici URL adrese vašeg tima.",
"org_description": "Nekoliko rečenica o vašoj organizaciji. Ovo će se pojaviti na url stranici vaše organizacije.", "org_description": "Nekoliko rečenica o vašoj organizaciji. Ovo će se pojaviti na url stranici vaše organizacije.",
"members": "Članovi", "members": "Članovi",
"organization_members": "Članovi organizacije", "organization_members": "Korisnici uključeni u tarifni plan Organization",
"member": "Član", "member": "Član",
"number_member_one": "{{count}} član", "number_member_one": "{{count}} član",
"number_member_other": "{{count}} članova", "number_member_other": "{{count}} članova",
"number_selected": "{{count}} izabrano", "number_selected": "Izabrano: {{count}}",
"owner": "Vlasnik", "owner": "Vlasnik",
"admin": "Admin", "admin": "Admin",
"administrator_user": "Administrator", "administrator_user": "Administrator",
@ -1687,7 +1687,7 @@
"attendee_no_longer_attending": "Polaznik više ne pohađa vaš događaj", "attendee_no_longer_attending": "Polaznik više ne pohađa vaš događaj",
"attendee_no_longer_attending_subtitle": "Korisnik {{name}} je otkazao. To znači da se otvorilo mesto za ovaj vremenski period", "attendee_no_longer_attending_subtitle": "Korisnik {{name}} je otkazao. To znači da se otvorilo mesto za ovaj vremenski period",
"create_event_on": "Kreirajte događaj na", "create_event_on": "Kreirajte događaj na",
"create_routing_form_on": "Kreiraj obrazac za usmeravanje", "create_routing_form_on": "Kreiranje obrasca za usmeravanje za",
"default_app_link_title": "Podesite podrazumevani link aplikacije", "default_app_link_title": "Podesite podrazumevani link aplikacije",
"default_app_link_description": "Podešavanje podrazumevanog linka aplikacije omogućava novokreiranim tipovima događaja da koriste link aplikacije koji ste postavili.", "default_app_link_description": "Podešavanje podrazumevanog linka aplikacije omogućava novokreiranim tipovima događaja da koriste link aplikacije koji ste postavili.",
"organizer_default_conferencing_app": "Podrazumevana aplikacija organizatora", "organizer_default_conferencing_app": "Podrazumevana aplikacija organizatora",
@ -1876,23 +1876,23 @@
"connect_google_workspace": "Poveži Google Workspace", "connect_google_workspace": "Poveži Google Workspace",
"google_workspace_admin_tooltip": "Morate da budete Workspace administrator da biste koristili ovu opciju", "google_workspace_admin_tooltip": "Morate da budete Workspace administrator da biste koristili ovu opciju",
"first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja", "first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja",
"install_app_on": "Instaliraj aplikaciju", "install_app_on": "Instaliraj aplikaciju za",
"create_for": "Napravi za", "create_for": "Napravi za",
"organization_banner_description": "Kreirajte okruženje gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.", "organization_banner_description": "Kreirajte okruženja gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.",
"organization_banner_title": "Upravljajte organizacijama sa više timova", "organization_banner_title": "Upravljajte organizacijama sa više timova",
"set_up_your_organization": "Postavite svoju organizaciju", "set_up_your_organization": "Konfigurisanje profila organizacije",
"organizations_description": "Organizacije su deljena okruženja gde timovi mogu da kreiraju deljene vrste događaja, aplikacije, radne tokove i još mnogo toga.", "organizations_description": "Organizacije su deljena okruženja gde timovi mogu da kreiraju deljene vrste događaja, aplikacije, radne tokove i još mnogo toga.",
"must_enter_organization_name": "Morate da unesete naziv organizacije", "must_enter_organization_name": "Morate da unesete naziv organizacije",
"must_enter_organization_admin_email": "Morate da unesete adresu e-pošte vaše organizacije", "must_enter_organization_admin_email": "Morate da unesete adresu e-pošte vaše organizacije",
"admin_email": "Imejl adresa vaše organizacije", "admin_email": "Vaša adresa e-pošte u organizaciji",
"admin_username": "Korisničko ime administratora", "admin_username": "Korisničko ime administratora",
"organization_name": "Naziv organizacije", "organization_name": "Naziv organizacije",
"organization_url": "URL organizacije", "organization_url": "URL organizacije",
"organization_verify_header": "Potvrdite imejl vaše organizacije", "organization_verify_header": "Potvrdite svoju adresu e-pošte u organizaciji",
"organization_verify_email_body": "Koristite kôd u nastavku da potvrdite svoju adresu e-pošte kako biste nastavili sa podešavanjem svoje organizacije.", "organization_verify_email_body": "Koristite kôd u nastavku da potvrdite svoju adresu e-pošte kako biste nastavili sa podešavanjem svoje organizacije.",
"additional_url_parameters": "Dodatni URL parametri", "additional_url_parameters": "Dodatni URL parametri",
"about_your_organization": "O vašoj organizaciji", "about_your_organization": "O vašoj organizaciji",
"about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrste događaja, aplikacije, radne tokove i drugo.", "about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrstama događaja, aplikacijama, radnim tokovima i mnogim drugim stvarima.",
"create_your_teams": "Kreirajte svoje timove", "create_your_teams": "Kreirajte svoje timove",
"create_your_teams_description": "Počnite planiranje zajedno dodavanjem članova tima u svoju organizaciju", "create_your_teams_description": "Počnite planiranje zajedno dodavanjem članova tima u svoju organizaciju",
"invite_organization_admins": "Pozovite administratore vaše organizacije", "invite_organization_admins": "Pozovite administratore vaše organizacije",
@ -1917,7 +1917,7 @@
"org_no_teams_yet_description": "Ako ste administrator, obavezno kreirajte timove koji će ovde biti prikazani.", "org_no_teams_yet_description": "Ako ste administrator, obavezno kreirajte timove koji će ovde biti prikazani.",
"set_up": "Podesi", "set_up": "Podesi",
"set_up_your_profile": "Podesite svoj profil", "set_up_your_profile": "Podesite svoj profil",
"set_up_your_profile_description": "Neka ljudi u organizaciji {{orgName}} znaju ko ste i kako da se povežu sa vama putem javnog linka.", "set_up_your_profile_description": "Unesite informacije o svojoj ulozi u organizaciji {{orgName}}. Te informacije će videti osobe koje kliknu na vaš javni link.",
"my_profile": "Moj profil", "my_profile": "Moj profil",
"my_settings": "Moja podešavanja", "my_settings": "Moja podešavanja",
"crm": "CRM", "crm": "CRM",
@ -1925,7 +1925,7 @@
"sender_id_info": "Ime ili broj koji će biti prikazani kao pošiljalac SMS-a (neke zemlje ne dozvoljavaju alfanumeričke ID-ove pošiljaoca)", "sender_id_info": "Ime ili broj koji će biti prikazani kao pošiljalac SMS-a (neke zemlje ne dozvoljavaju alfanumeričke ID-ove pošiljaoca)",
"org_admins_can_create_new_teams": "Samo administrator vaše ogranizacije može da kreira nove timove", "org_admins_can_create_new_teams": "Samo administrator vaše ogranizacije može da kreira nove timove",
"google_new_spam_policy": "Google-ova nova politika neželjene pošte može da spreči da primate bilo koje imejlove ili obaveštenja kalendara u vezi sa ovom rezervacijom.", "google_new_spam_policy": "Google-ova nova politika neželjene pošte može da spreči da primate bilo koje imejlove ili obaveštenja kalendara u vezi sa ovom rezervacijom.",
"resolve": "Reši", "resolve": "Kako rešiti taj problem",
"no_organization_slug": "Desila se greška kod kreiranja timova za ovu organizaciju. Nedostaje URL slug.", "no_organization_slug": "Desila se greška kod kreiranja timova za ovu organizaciju. Nedostaje URL slug.",
"org_name": "Naziv organizacije", "org_name": "Naziv organizacije",
"org_url": "URL organizacije", "org_url": "URL organizacije",
@ -1933,7 +1933,7 @@
"404_the_org": "Organizacija", "404_the_org": "Organizacija",
"404_the_team": "Tim", "404_the_team": "Tim",
"404_claim_entity_org": "Zatražite poddomen za svoju organizaciju", "404_claim_entity_org": "Zatražite poddomen za svoju organizaciju",
"404_claim_entity_team": "Zahtevajte ovaj tim i počnite da uređujete kolektivni raspored", "404_claim_entity_team": "Postanite deo ovog tima i počnite da uređujete kolektivni raspored",
"insights_all_org_filter": "Sve aplikacije", "insights_all_org_filter": "Sve aplikacije",
"insights_team_filter": "Tim: {{teamName}}", "insights_team_filter": "Tim: {{teamName}}",
"insights_user_filter": "Korisnik: {{userName}}", "insights_user_filter": "Korisnik: {{userName}}",

View File

@ -262,6 +262,7 @@ export default class EventManager {
select: { select: {
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
}, },
}, },
}, },

View File

@ -22,25 +22,33 @@ export const getAggregatedAvailability = (
return mergeOverlappingDateRanges(availability); return mergeOverlappingDateRanges(availability);
}; };
function isSameDay(date1: Date, date2: Date) {
return (
date1.getUTCFullYear() === date2.getUTCFullYear() &&
date1.getUTCMonth() === date2.getUTCMonth() &&
date1.getUTCDate() === date2.getUTCDate()
);
}
function mergeOverlappingDateRanges(dateRanges: DateRange[]) { function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
const sortedDateRanges = dateRanges.sort((a, b) => a.start.diff(b.start)); //is it already sorted before? dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
const mergedDateRanges: DateRange[] = []; const mergedDateRanges: DateRange[] = [];
let currentRange = sortedDateRanges[0]; let currentRange = dateRanges[0];
if (!currentRange) { if (!currentRange) {
return []; return [];
} }
for (let i = 1; i < sortedDateRanges.length; i++) { for (let i = 1; i < dateRanges.length; i++) {
const nextRange = sortedDateRanges[i]; const nextRange = dateRanges[i];
if ( if (
currentRange.start.utc().format("DD MM YY") === nextRange.start.utc().format("DD MM YY") && isSameDay(currentRange.start.toDate(), nextRange.start.toDate()) &&
currentRange.end.isAfter(nextRange.start) currentRange.end.valueOf() > nextRange.start.valueOf()
) { ) {
currentRange = { currentRange = {
start: currentRange.start, start: currentRange.start,
end: currentRange.end.isAfter(nextRange.end) ? currentRange.end : nextRange.end, end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end,
}; };
} else { } else {
mergedDateRanges.push(currentRange); mergedDateRanges.push(currentRange);

View File

@ -13,6 +13,7 @@ import type { EventBusyDetails } from "@calcom/types/Calendar";
export async function getBusyTimes(params: { export async function getBusyTimes(params: {
credentials: Credential[]; credentials: Credential[];
userId: number; userId: number;
userEmail: string;
username: string; username: string;
eventTypeId?: number; eventTypeId?: number;
startTime: string; startTime: string;
@ -27,6 +28,7 @@ export async function getBusyTimes(params: {
const { const {
credentials, credentials,
userId, userId,
userEmail,
username, username,
eventTypeId, eventTypeId,
startTime, startTime,
@ -45,15 +47,6 @@ export async function getBusyTimes(params: {
status: BookingStatus.ACCEPTED, status: BookingStatus.ACCEPTED,
})}` })}`
); );
// get user email for attendee checking.
const user = await prisma.user.findUniqueOrThrow({
where: {
id: userId,
},
select: {
email: true,
},
});
/** /**
* A user is considered busy within a given time period if there * A user is considered busy within a given time period if there
@ -97,7 +90,7 @@ export async function getBusyTimes(params: {
...sharedQuery, ...sharedQuery,
attendees: { attendees: {
some: { some: {
email: user.email, email: userEmail,
}, },
}, },
}, },

View File

@ -180,6 +180,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
endTime: getBusyTimesEnd, endTime: getBusyTimesEnd,
eventTypeId, eventTypeId,
userId: user.id, userId: user.id,
userEmail: user.email,
username: `${user.username}`, username: `${user.username}`,
beforeEventBuffer, beforeEventBuffer,
afterEventBuffer, afterEventBuffer,

View File

@ -72,7 +72,7 @@ export async function getServerSession(options: {
image: `${CAL_URL}/${user.username}/avatar.png`, image: `${CAL_URL}/${user.username}/avatar.png`,
impersonatedByUID: token.impersonatedByUID ?? undefined, impersonatedByUID: token.impersonatedByUID ?? undefined,
belongsToActiveTeam: token.belongsToActiveTeam, belongsToActiveTeam: token.belongsToActiveTeam,
organizationId: token.organizationId, org: token.org,
locale: user.locale ?? undefined, locale: user.locale ?? undefined,
}, },
}; };

View File

@ -8,6 +8,7 @@ import GoogleProvider from "next-auth/providers/google";
import checkLicense from "@calcom/features/ee/common/server/checkLicense"; import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { getOrgFullDomain, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
@ -402,7 +403,14 @@ export const AUTH_OPTIONS: AuthOptions = {
username: true, username: true,
name: true, name: true,
email: true, email: true,
organizationId: true, organization: {
select: {
id: true,
name: true,
slug: true,
metadata: true,
},
},
role: true, role: true,
locale: true, locale: true,
teams: { teams: {
@ -419,12 +427,23 @@ export const AUTH_OPTIONS: AuthOptions = {
// Check if the existingUser has any active teams // Check if the existingUser has any active teams
const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser); const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser);
const { teams: _teams, ...existingUserWithoutTeamsField } = existingUser; const { teams: _teams, organization, ...existingUserWithoutTeamsField } = existingUser;
const parsedOrgMetadata = teamMetadataSchema.parse(organization?.metadata ?? {});
return { return {
...existingUserWithoutTeamsField, ...existingUserWithoutTeamsField,
...token, ...token,
belongsToActiveTeam, belongsToActiveTeam,
org: organization
? {
id: organization.id,
name: organization.name,
slug: organization.slug ?? parsedOrgMetadata?.requestedSlug ?? "",
fullDomain: getOrgFullDomain(organization.slug ?? parsedOrgMetadata?.requestedSlug ?? ""),
domainSuffix: subdomainSuffix(),
}
: undefined,
}; };
}; };
if (!user) { if (!user) {
@ -448,7 +467,7 @@ export const AUTH_OPTIONS: AuthOptions = {
role: user.role, role: user.role,
impersonatedByUID: user?.impersonatedByUID, impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam, belongsToActiveTeam: user?.belongsToActiveTeam,
organizationId: user?.organizationId, org: user?.org,
locale: user?.locale, locale: user?.locale,
}; };
} }
@ -487,7 +506,7 @@ export const AUTH_OPTIONS: AuthOptions = {
role: existingUser.role, role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number, impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean, belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
organizationId: token?.organizationId, org: token?.org,
locale: existingUser.locale, locale: existingUser.locale,
}; };
} }
@ -507,7 +526,7 @@ export const AUTH_OPTIONS: AuthOptions = {
role: token.role as UserPermissionRole, role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number, impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean, belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
organizationId: token?.organizationId, org: token?.org,
locale: token.locale, locale: token.locale,
}, },
}; };

View File

@ -327,6 +327,7 @@ const BookerComponent = ({
prefetchNextMonth={prefetchNextMonth} prefetchNextMonth={prefetchNextMonth}
monthCount={monthCount} monthCount={monthCount}
seatsPerTimeSlot={event.data?.seatsPerTimeSlot} seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
/> />
</BookerSection> </BookerSection>
</AnimatePresence> </AnimatePresence>

View File

@ -19,6 +19,7 @@ type AvailableTimeSlotsProps = {
prefetchNextMonth: boolean; prefetchNextMonth: boolean;
monthCount: number | undefined; monthCount: number | undefined;
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
showAvailableSeatsCount?: boolean | null;
}; };
/** /**
@ -32,6 +33,7 @@ export const AvailableTimeSlots = ({
extraDays, extraDays,
limitHeight, limitHeight,
seatsPerTimeSlot, seatsPerTimeSlot,
showAvailableSeatsCount,
prefetchNextMonth, prefetchNextMonth,
monthCount, monthCount,
}: AvailableTimeSlotsProps) => { }: AvailableTimeSlotsProps) => {
@ -60,6 +62,7 @@ export const AvailableTimeSlots = ({
seatsPerTimeSlot, seatsPerTimeSlot,
attendees, attendees,
bookingUid, bookingUid,
showAvailableSeatsCount,
}); });
if (seatsPerTimeSlot && seatsPerTimeSlot - attendees > 1) { if (seatsPerTimeSlot && seatsPerTimeSlot - attendees > 1) {
@ -116,6 +119,7 @@ export const AvailableTimeSlots = ({
date={dayjs(slots.date)} date={dayjs(slots.date)}
slots={slots.slots} slots={slots.slots}
seatsPerTimeSlot={seatsPerTimeSlot} seatsPerTimeSlot={seatsPerTimeSlot}
showAvailableSeatsCount={showAvailableSeatsCount}
availableMonth={ availableMonth={
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM") dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
? dayjs(slots.date).format("MMM") ? dayjs(slots.date).format("MMM")

View File

@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings"; import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details"; import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
import { useTimePreferences } from "@calcom/features/bookings/lib"; import { useTimePreferences } from "@calcom/features/bookings/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -130,13 +131,12 @@ export const EventMeta = () => {
<EventMetaBlock icon={User} className={`${colorClass}`}> <EventMetaBlock icon={User} className={`${colorClass}`}>
<div className="text-bookinghighlight flex items-start text-sm"> <div className="text-bookinghighlight flex items-start text-sm">
<p> <p>
{bookingSeatAttendeesQty ? eventTotalSeats - bookingSeatAttendeesQty : eventTotalSeats} /{" "} <SeatsAvailabilityText
{eventTotalSeats}{" "} showExact={!!seatedEventData.showAvailableSeatsCount}
{t("seats_available", { totalSeats={eventTotalSeats}
count: bookingSeatAttendeesQty bookedSeats={bookingSeatAttendeesQty || 0}
? eventTotalSeats - bookingSeatAttendeesQty variant="fraction"
: eventTotalSeats, />
})}
</p> </p>
</div> </div>
</EventMetaBlock> </EventMetaBlock>

View File

@ -33,6 +33,7 @@ type SeatedEventData = {
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
attendees?: number; attendees?: number;
bookingUid?: string; bookingUid?: string;
showAvailableSeatsCount?: boolean | null;
}; };
export type BookerStore = { export type BookerStore = {
@ -206,6 +207,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
seatsPerTimeSlot: undefined, seatsPerTimeSlot: undefined,
attendees: undefined, attendees: undefined,
bookingUid: undefined, bookingUid: undefined,
showAvailableSeatsCount: true,
}, },
setSeatedEventData: (seatedEventData: SeatedEventData) => { setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData }); set({ seatedEventData });

View File

@ -12,6 +12,7 @@ import { Button, SkeletonText } from "@calcom/ui";
import { useBookerStore } from "../Booker/store"; import { useBookerStore } from "../Booker/store";
import { useTimePreferences } from "../lib"; import { useTimePreferences } from "../lib";
import { SeatsAvailabilityText } from "./SeatsAvailabilityText";
import { TimeFormatToggle } from "./TimeFormatToggle"; import { TimeFormatToggle } from "./TimeFormatToggle";
type AvailableTimesProps = { type AvailableTimesProps = {
@ -24,6 +25,7 @@ type AvailableTimesProps = {
bookingUid?: string bookingUid?: string
) => void; ) => void;
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
showAvailableSeatsCount?: boolean | null;
showTimeFormatToggle?: boolean; showTimeFormatToggle?: boolean;
className?: string; className?: string;
availableMonth?: string | undefined; availableMonth?: string | undefined;
@ -35,6 +37,7 @@ export const AvailableTimes = ({
slots, slots,
onTimeSelect, onTimeSelect,
seatsPerTimeSlot, seatsPerTimeSlot,
showAvailableSeatsCount,
showTimeFormatToggle = true, showTimeFormatToggle = true,
className, className,
availableMonth, availableMonth,
@ -110,15 +113,16 @@ export const AvailableTimes = ({
{dayjs.utc(slot.time).tz(timezone).format(timeFormat)} {dayjs.utc(slot.time).tz(timezone).format(timeFormat)}
{bookingFull && <p className="text-sm">{t("booking_full")}</p>} {bookingFull && <p className="text-sm">{t("booking_full")}</p>}
{hasTimeSlots && !bookingFull && ( {hasTimeSlots && !bookingFull && (
<p className="flex items-center text-sm lowercase"> <p className="flex items-center text-sm">
<span <span
className={classNames(colorClass, "mr-1 inline-block h-2 w-2 rounded-full")} className={classNames(colorClass, "mr-1 inline-block h-2 w-2 rounded-full")}
aria-hidden aria-hidden
/> />
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot}{" "} <SeatsAvailabilityText
{t("seats_available", { showExact={!!showAvailableSeatsCount}
count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot, totalSeats={seatsPerTimeSlot}
})} bookedSeats={slot.attendees || 0}
/>
</p> </p>
)} )}
</Button> </Button>

View File

@ -0,0 +1,51 @@
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
type Props = {
/**
* Whether to show the exact number of seats available or not
*
* @default true
*/
showExact: boolean;
/**
* Shows available seats count as either whole number or fraction.
*
* Applies only when `showExact` is `true`
*
* @default "whole"
*/
variant?: "whole" | "fraction";
/** Number of seats booked in the event */
bookedSeats: number;
/** Total number of seats in the event */
totalSeats: number;
};
export const SeatsAvailabilityText = ({
showExact = true,
bookedSeats,
totalSeats,
variant = "whole",
}: Props) => {
const { t } = useLocale();
const availableSeats = totalSeats - bookedSeats;
const isHalfFull = bookedSeats / totalSeats >= 0.5;
const isNearlyFull = bookedSeats / totalSeats >= 0.83;
return (
<span className={classNames(showExact && "lowercase")}>
{showExact
? `${availableSeats}${variant === "fraction" ? ` / ${totalSeats}` : ""} ${t("seats_available", {
count: availableSeats,
})}`
: isNearlyFull
? t("seats_nearly_full")
: isHalfFull
? t("seats_half_full")
: t("seats_available", {
count: availableSeats,
})}
</span>
);
};

View File

@ -21,7 +21,10 @@ function RenderIcon({
return ( return (
<img <img
src={eventLocationType.iconUrl} src={eventLocationType.iconUrl}
className="me-[10px] h-4 w-4 opacity-70 invert-[.65] dark:invert-0" className={classNames(
eventLocationType?.iconUrl?.includes("-dark") && "dark:invert",
"me-[10px] h-4 w-4"
)}
alt={`${eventLocationType.label} icon`} alt={`${eventLocationType.label} icon`}
/> />
); );

View File

@ -275,6 +275,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
recurringEvent: true, recurringEvent: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingLimits: true, bookingLimits: true,
durationLimits: true, durationLimits: true,
parentId: true, parentId: true,
@ -1071,6 +1072,7 @@ async function handler(
// if seats are not enabled we should default true // if seats are not enabled we should default true
seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true,
seatsPerTimeSlot: eventType.seatsPerTimeSlot, seatsPerTimeSlot: eventType.seatsPerTimeSlot,
seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true,
schedulingType: eventType.schedulingType, schedulingType: eventType.schedulingType,
}; };

View File

@ -10,7 +10,7 @@ import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
*/ */
export type OrganizationBranding = export type OrganizationBranding =
| ({ | ({
logo?: string | null | undefined; id: number;
name?: string; name?: string;
slug: string; slug: string;
fullDomain: string; fullDomain: string;

View File

@ -16,7 +16,7 @@ export const getServerSideProps = async ({ req, res }: GetServerSidePropsContext
// Check if logged in user has an organization assigned // Check if logged in user has an organization assigned
const session = await getServerSession({ req, res }); const session = await getServerSession({ req, res });
if (!session?.user.organizationId) { if (!session?.user.org?.id) {
return { return {
notFound: true, notFound: true,
}; };
@ -26,7 +26,7 @@ export const getServerSideProps = async ({ req, res }: GetServerSidePropsContext
const membership = await prisma.membership.findFirst({ const membership = await prisma.membership.findFirst({
where: { where: {
userId: session?.user.id, userId: session?.user.id,
teamId: session?.user.organizationId, teamId: session?.user.org.id,
}, },
select: { select: {
role: true, role: true,

View File

@ -69,7 +69,7 @@ const MembersView = () => {
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState<boolean>(false); const [showMemberInvitationModal, setShowMemberInvitationModal] = useState<boolean>(false);
const [members, setMembers] = useState<Members>([]); const [members, setMembers] = useState<Members>([]);
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
enabled: !!session.data?.user?.organizationId, enabled: !!session.data?.user?.org,
}); });
const { data: team, isLoading: isTeamLoading } = trpc.viewer.organizations.getOtherTeam.useQuery( const { data: team, isLoading: isTeamLoading } = trpc.viewer.organizations.getOtherTeam.useQuery(
{ teamId }, { teamId },

View File

@ -215,7 +215,7 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
const session = useSession(); const session = useSession();
const bookerUrl = useBookerUrl(); const bookerUrl = useBookerUrl();
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
enabled: !!session.data?.user?.organizationId, enabled: !!session.data?.user?.org,
}); });
const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({ const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({
async onSuccess() { async onSuccess() {

View File

@ -80,7 +80,7 @@ const MembersView = () => {
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(showDialog); const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(showDialog);
const [showInviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false); const [showInviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false);
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
enabled: !!session.data?.user?.organizationId, enabled: !!session.data?.user?.org,
}); });
const { data: orgMembersNotInThisTeam, isLoading: isOrgListLoading } = const { data: orgMembersNotInThisTeam, isLoading: isOrgListLoading } =

View File

@ -263,6 +263,7 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
} }
onTimeSelect={onTimeSelect} onTimeSelect={onTimeSelect}
slots={slots} slots={slots}
showAvailableSeatsCount={eventType.seatsShowAvailabilityCount}
/> />
</div> </div>
) : null} ) : null}

View File

@ -39,6 +39,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
price: true, price: true,
currency: true, currency: true,
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAvailabilityCount: true,
bookingFields: true, bookingFields: true,
team: { team: {
select: { select: {

View File

@ -159,7 +159,7 @@ const useTabs = () => {
// check if name is in adminRequiredKeys // check if name is in adminRequiredKeys
return tabs.filter((tab) => { return tabs.filter((tab) => {
if (organizationRequiredKeys.includes(tab.name)) return !!session.data?.user?.organizationId; if (organizationRequiredKeys.includes(tab.name)) return !!session.data?.user?.org;
if (isAdmin) return true; if (isAdmin) return true;
return !adminRequiredKeys.includes(tab.name); return !adminRequiredKeys.includes(tab.name);
@ -205,7 +205,7 @@ const SettingsSidebarContainer = ({
const { data: teams } = trpc.viewer.teams.list.useQuery(); const { data: teams } = trpc.viewer.teams.list.useQuery();
const session = useSession(); const session = useSession();
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
enabled: !!session.data?.user?.organizationId, enabled: !!session.data?.user?.org,
}); });
const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(); const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery();
@ -523,6 +523,12 @@ const SettingsSidebarContainer = ({
</div> </div>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-0.5"> <CollapsibleContent className="space-y-0.5">
<VerticalTabItem
name={t("profile")}
href={`/settings/organizations/teams/other/${otherTeam.id}/profile`}
textClassNames="px-3 text-emphasis font-medium text-sm"
disableChevron
/>
<VerticalTabItem <VerticalTabItem
name={t("members")} name={t("members")}
href={`/settings/organizations/teams/other/${otherTeam.id}/members`} href={`/settings/organizations/teams/other/${otherTeam.id}/members`}

View File

@ -22,7 +22,6 @@ import AdminPasswordBanner from "@calcom/features/users/components/AdminPassword
import VerifyEmailBanner from "@calcom/features/users/components/VerifyEmailBanner"; import VerifyEmailBanner from "@calcom/features/users/components/VerifyEmailBanner";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { APP_NAME, DESKTOP_APP_LINK, JOIN_DISCORD, ROADMAP, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, DESKTOP_APP_LINK, JOIN_DISCORD, ROADMAP, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import getBrandColours from "@calcom/lib/getBrandColours"; import getBrandColours from "@calcom/lib/getBrandColours";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useIsomorphicLayoutEffect } from "@calcom/lib/hooks/useIsomorphicLayoutEffect"; import { useIsomorphicLayoutEffect } from "@calcom/lib/hooks/useIsomorphicLayoutEffect";
@ -792,13 +791,12 @@ function SideBarContainer({ bannersHeight }: SideBarContainerProps) {
function SideBar({ bannersHeight, user }: SideBarProps) { function SideBar({ bannersHeight, user }: SideBarProps) {
const { t, isLocaleReady } = useLocale(); const { t, isLocaleReady } = useLocale();
const orgBranding = useOrgBranding(); const orgBranding = useOrgBranding();
const isOrgBrandingDataFetched = orgBranding !== undefined;
const publicPageUrl = useMemo(() => { const publicPageUrl = useMemo(() => {
if (!user?.organizationId) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`; if (!user?.org?.id) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`;
const publicPageUrl = orgBranding?.slug ? getOrgFullDomain(orgBranding.slug) : ""; const publicPageUrl = orgBranding?.slug ? getOrgFullDomain(orgBranding.slug) : "";
return publicPageUrl; return publicPageUrl;
}, [orgBranding?.slug, user?.organizationId, user?.username]); }, [orgBranding?.slug, user?.username, user?.org?.id]);
const bottomNavItems: NavigationItemType[] = [ const bottomNavItems: NavigationItemType[] = [
{ {
@ -819,7 +817,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
}, },
{ {
name: "settings", name: "settings",
href: user?.organizationId ? `/settings/organizations/profile` : "/settings/my-account/profile", href: user?.org ? `/settings/organizations/profile` : "/settings/my-account/profile",
icon: Settings, icon: Settings,
}, },
]; ];
@ -830,12 +828,12 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
className="desktop-transparent bg-muted border-muted fixed left-0 hidden h-full max-h-screen w-14 flex-col overflow-y-auto overflow-x-hidden border-r dark:bg-gradient-to-tr dark:from-[#2a2a2a] dark:to-[#1c1c1c] md:sticky md:flex lg:w-56 lg:px-3"> className="desktop-transparent bg-muted border-muted fixed left-0 hidden h-full max-h-screen w-14 flex-col overflow-y-auto overflow-x-hidden border-r dark:bg-gradient-to-tr dark:from-[#2a2a2a] dark:to-[#1c1c1c] md:sticky md:flex lg:w-56 lg:px-3">
<div className="flex h-full flex-col justify-between py-3 lg:pt-4"> <div className="flex h-full flex-col justify-between py-3 lg:pt-4">
<header className="items-center justify-between md:hidden lg:flex"> <header className="items-center justify-between md:hidden lg:flex">
{!isOrgBrandingDataFetched ? null : orgBranding ? ( {orgBranding ? (
<Link href="/settings/organizations/profile" className="px-1.5"> <Link href="/settings/organizations/profile" className="px-1.5">
<div className="flex items-center gap-2 font-medium"> <div className="flex items-center gap-2 font-medium">
<Avatar <Avatar
alt={`${orgBranding.name} logo`} alt={`${orgBranding.name} logo`}
imageSrc={getPlaceholderAvatar(orgBranding.logo, orgBranding.name)} imageSrc={`${orgBranding.fullDomain}/avatar.png`}
size="xsm" size="xsm"
/> />
<p className="text line-clamp-1 text-sm"> <p className="text line-clamp-1 text-sm">

View File

@ -7,7 +7,7 @@ import type { Action, State } from "./UserListTable";
export function ChangeUserRoleModal(props: { state: State; dispatch: Dispatch<Action> }) { export function ChangeUserRoleModal(props: { state: State; dispatch: Dispatch<Action> }) {
const { data: session } = useSession(); const { data: session } = useSession();
const orgId = session?.user.organizationId; const orgId = session?.user.org?.id;
if (!orgId || !props.state.changeMemberRole.user) return null; if (!orgId || !props.state.changeMemberRole.user) return null;
return ( return (

View File

@ -40,10 +40,10 @@ export function DeleteMemberModal({ state, dispatch }: { state: State; dispatch:
confirmBtnText={t("confirm_remove_member")} confirmBtnText={t("confirm_remove_member")}
onConfirm={() => { onConfirm={() => {
// Shouldnt ever happen just for type safety // Shouldnt ever happen just for type safety
if (!session?.user.organizationId || !state?.deleteMember?.user?.id) return; if (!session?.user.org?.id || !state?.deleteMember?.user?.id) return;
removeMemberMutation.mutate({ removeMemberMutation.mutate({
teamId: session?.user.organizationId, teamId: session?.user.org.id,
memberId: state?.deleteMember?.user.id, memberId: state?.deleteMember?.user.id,
isOrg: true, isOrg: true,
}); });

View File

@ -9,7 +9,7 @@ import type { Action, State } from "./UserListTable";
export function ImpersonationMemberModal(props: { state: State; dispatch: Dispatch<Action> }) { export function ImpersonationMemberModal(props: { state: State; dispatch: Dispatch<Action> }) {
const { t } = useLocale(); const { t } = useLocale();
const { data: session } = useSession(); const { data: session } = useSession();
const teamId = session?.user.organizationId; const teamId = session?.user.org?.id;
const user = props.state.impersonateMember.user; const user = props.state.impersonateMember.user;
if (!user || !teamId) return null; if (!user || !teamId) return null;

View File

@ -46,9 +46,9 @@ export function InviteMemberModal(props: Props) {
}, },
}); });
if (!session?.user.organizationId) return null; if (!session?.user.org?.id) return null;
const orgId = session.user.organizationId; const orgId = session.user.org.id;
return ( return (
<MemberInvitationModal <MemberInvitationModal

View File

@ -79,6 +79,7 @@ const commons = {
schedulingType: SchedulingType.COLLECTIVE, schedulingType: SchedulingType.COLLECTIVE,
seatsPerTimeSlot: null, seatsPerTimeSlot: null,
seatsShowAttendees: null, seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
id: 0, id: 0,
hideCalendarNotes: false, hideCalendarNotes: false,
recurringEvent: null, recurringEvent: null,

View File

@ -174,6 +174,7 @@ export default async function getEventTypeById({
destinationCalendar: true, destinationCalendar: true,
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
webhooks: { webhooks: {
select: { select: {
id: true, id: true,

View File

@ -93,6 +93,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
afterEventBuffer: 0, afterEventBuffer: 0,
seatsPerTimeSlot: null, seatsPerTimeSlot: null,
seatsShowAttendees: null, seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
schedulingType: null, schedulingType: null,
scheduleId: null, scheduleId: null,
bookingLimits: null, bookingLimits: null,

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "seatsShowAvailabilityCount" BOOLEAN DEFAULT true;

View File

@ -98,6 +98,7 @@ model EventType {
afterEventBuffer Int @default(0) afterEventBuffer Int @default(0)
seatsPerTimeSlot Int? seatsPerTimeSlot Int?
seatsShowAttendees Boolean? @default(false) seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
schedulingType SchedulingType? schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id]) schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int? scheduleId Int?

View File

@ -3,6 +3,7 @@ import { Prisma } from "@prisma/client";
export const availabilityUserSelect = Prisma.validator<Prisma.UserSelect>()({ export const availabilityUserSelect = Prisma.validator<Prisma.UserSelect>()({
id: true, id: true,
timeZone: true, timeZone: true,
email: true,
bufferTime: true, bufferTime: true,
startTime: true, startTime: true,
username: true, username: true,
@ -22,7 +23,6 @@ export const availabilityUserSelect = Prisma.validator<Prisma.UserSelect>()({
}); });
export const baseUserSelect = Prisma.validator<Prisma.UserSelect>()({ export const baseUserSelect = Prisma.validator<Prisma.UserSelect>()({
email: true,
name: true, name: true,
destinationCalendar: true, destinationCalendar: true,
locale: true, locale: true,
@ -35,7 +35,6 @@ export const baseUserSelect = Prisma.validator<Prisma.UserSelect>()({
export const userSelect = Prisma.validator<Prisma.UserArgs>()({ export const userSelect = Prisma.validator<Prisma.UserArgs>()({
select: { select: {
email: true,
name: true, name: true,
allowDynamicBooking: true, allowDynamicBooking: true,
destinationCalendar: true, destinationCalendar: true,

View File

@ -575,6 +575,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
successRedirectUrl: true, successRedirectUrl: true,
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
periodType: true, periodType: true,
hashedLink: true, hashedLink: true,
webhooks: true, webhooks: true,

View File

@ -206,6 +206,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
bookingFields: true, bookingFields: true,
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
eventName: true, eventName: true,
}, },
}, },
@ -295,6 +296,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
cancellationReason: "Payment method removed by organizer", cancellationReason: "Payment method removed by organizer",
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot, seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
seatsShowAttendees: booking.eventType?.seatsShowAttendees, seatsShowAttendees: booking.eventType?.seatsShowAttendees,
seatsShowAvailabilityCount: booking.eventType?.seatsShowAvailabilityCount,
}, },
{ {
eventName: booking?.eventType?.eventName, eventName: booking?.eventType?.eventName,

View File

@ -191,6 +191,7 @@ async function getBookings({
currency: true, currency: true,
metadata: true, metadata: true,
seatsShowAttendees: true, seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
team: { team: {
select: { select: {
id: true, id: true,

View File

@ -239,7 +239,6 @@ export async function sendVerificationEmail({
}) { }) {
const token: string = randomBytes(32).toString("hex"); const token: string = randomBytes(32).toString("hex");
if (!connectionInfo.autoAccept) {
await prisma.verificationToken.create({ await prisma.verificationToken.create({
data: { data: {
identifier: usernameOrEmail, identifier: usernameOrEmail,
@ -252,6 +251,7 @@ export async function sendVerificationEmail({
}, },
}, },
}); });
if (!connectionInfo.autoAccept) {
await sendTeamInviteEmail({ await sendTeamInviteEmail({
language: translation, language: translation,
from: ctx.user.name || `${team.name}'s admin`, from: ctx.user.name || `${team.name}'s admin`,
@ -262,15 +262,6 @@ export async function sendVerificationEmail({
isOrg: input.isOrg, isOrg: input.isOrg,
}); });
} else { } else {
// we have already joined the team in createNewUserConnectToOrgIfExists so we dont need to connect via token
await prisma.verificationToken.create({
data: {
identifier: usernameOrEmail,
token,
expires: new Date(new Date().setHours(168)), // +1 week
},
});
await sendOrganizationAutoJoinEmail({ await sendOrganizationAutoJoinEmail({
language: translation, language: translation,
from: ctx.user.name || `${team.name}'s admin`, from: ctx.user.name || `${team.name}'s admin`,

View File

@ -177,6 +177,7 @@ export interface CalendarEvent {
eventTypeId?: number | null; eventTypeId?: number | null;
appsStatus?: AppsStatus[]; appsStatus?: AppsStatus[];
seatsShowAttendees?: boolean | null; seatsShowAttendees?: boolean | null;
seatsShowAvailabilityCount?: boolean | null;
attendeeSeatId?: string; attendeeSeatId?: string;
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
schedulingType?: SchedulingType | null; schedulingType?: SchedulingType | null;

View File

@ -16,7 +16,13 @@ declare module "next-auth" {
email_verified?: boolean; email_verified?: boolean;
impersonatedByUID?: number; impersonatedByUID?: number;
belongsToActiveTeam?: boolean; belongsToActiveTeam?: boolean;
organizationId?: number | null; org?: {
id: number;
name?: string;
slug: string;
fullDomain: string;
domainSuffix: string;
};
username?: PrismaUser["username"]; username?: PrismaUser["username"];
role?: PrismaUser["role"] | "INACTIVE_ADMIN"; role?: PrismaUser["role"] | "INACTIVE_ADMIN";
locale?: string | null; locale?: string | null;
@ -32,6 +38,13 @@ declare module "next-auth/jwt" {
role?: UserPermissionRole | "INACTIVE_ADMIN" | null; role?: UserPermissionRole | "INACTIVE_ADMIN" | null;
impersonatedByUID?: number | null; impersonatedByUID?: number | null;
belongsToActiveTeam?: boolean; belongsToActiveTeam?: boolean;
org?: {
id: number;
name?: string;
slug: string;
fullDomain: string;
domainSuffix: string;
};
organizationId?: number | null; organizationId?: number | null;
locale?: string; locale?: string;
} }