diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index f600c6e697..32bb2ca515 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -141,17 +141,6 @@ function BookingListItem(booking: BookingItemProps) { : []), ]; - const showRecordingActions: ActionType[] = [ - { - id: "view_recordings", - label: t("view_recordings"), - onClick: () => { - setViewRecordingsDialogIsOpen(true); - }, - disabled: mutation.isLoading, - }, - ]; - let bookedActions: ActionType[] = [ { id: "cancel", @@ -270,11 +259,21 @@ function BookingListItem(booking: BookingItemProps) { const bookingLink = buildBookingLink(); const title = booking.title; - // To be used after we run query on legacy bookings - // const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed; - const showRecordingsButtons = - (booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed; + const showRecordingsButtons = !!(booking.isRecorded && isPast && isConfirmed); + const checkForRecordingsButton = + !showRecordingsButtons && (booking.location === "integrations:daily" || booking?.location?.trim() === ""); + + const showRecordingActions: ActionType[] = [ + { + id: checkForRecordingsButton ? "check_for_recordings" : "view_recordings", + label: checkForRecordingsButton ? t("check_for_recordings") : t("view_recordings"), + onClick: () => { + setViewRecordingsDialogIsOpen(true); + }, + disabled: mutation.isLoading, + }, + ]; return ( <> @@ -299,7 +298,7 @@ function BookingListItem(booking: BookingItemProps) { paymentCurrency={booking.payment[0].currency} /> )} - {showRecordingsButtons && ( + {(showRecordingsButtons || checkForRecordingsButton) && ( ) : null} {isPast && isPending && !isConfirmed ? : null} - {showRecordingsButtons && } + {(showRecordingsButtons || checkForRecordingsButton) && ( + + )} {isCancelled && booking.rescheduled && (
diff --git a/apps/web/pages/api/recorded-daily-video.ts b/apps/web/pages/api/recorded-daily-video.ts index 0ce22581a8..c35a9d5c7f 100644 --- a/apps/web/pages/api/recorded-daily-video.ts +++ b/apps/web/pages/api/recorded-daily-video.ts @@ -62,6 +62,46 @@ const triggerWebhook = async ({ await Promise.all(promises); }; +const checkIfUserIsPartOfTheSameTeam = async ( + teamId: number | undefined | null, + userId: number, + userEmail: string | undefined | null +) => { + if (!teamId) return false; + + const getUserQuery = () => { + if (!!userEmail) { + return { + OR: [ + { + id: userId, + }, + { + email: userEmail, + }, + ], + }; + } else { + return { + id: userId, + }; + } + }; + + const team = await prisma.team.findFirst({ + where: { + id: teamId, + members: { + some: { + user: getUserQuery(), + }, + }, + }, + }); + + return !!team; +}; + async function handler(req: NextApiRequest, res: NextApiResponse) { if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) { return res.status(405).json({ message: "No SendGrid API key or email" }); @@ -137,12 +177,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const isUserAttendeeOrOrganiser = booking?.user?.id === session.user.id || - attendeesList.find((attendee) => attendee.id === session.user.id); + attendeesList.find( + (attendee) => attendee.id === session.user.id || attendee.email === session.user.email + ); if (!isUserAttendeeOrOrganiser) { - return res.status(403).send({ - message: "Unauthorised", - }); + const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam( + booking?.eventType?.teamId, + session.user.id, + session.user.email + ); + + if (!isUserMemberOfTheTeam) { + return res.status(403).send({ + message: "Unauthorised", + }); + } } await prisma.booking.update({ @@ -202,7 +252,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return res.status(403).json({ message: "User does not have team plan to send out emails" }); } catch (err) { - console.warn("something_went_wrong", err); + console.warn("Error in /recorded-daily-video", err); return res.status(500).json({ message: "something went wrong" }); } } diff --git a/apps/web/pages/settings/security/password.tsx b/apps/web/pages/settings/security/password.tsx index 71077c9447..e008234279 100644 --- a/apps/web/pages/settings/security/password.tsx +++ b/apps/web/pages/settings/security/password.tsx @@ -164,14 +164,13 @@ const PasswordView = ({ user }: PasswordViewProps) => { <> {user && user.identityProvider !== IdentityProvider.CAL ? ( -
-
-

- {t("account_managed_by_identity_provider", { - provider: identityProviderNameMap[user.identityProvider], - })} -

-
+
+

+ {t("account_managed_by_identity_provider", { + provider: identityProviderNameMap[user.identityProvider], + })} +

+

{t("account_managed_by_identity_provider_description", { provider: identityProviderNameMap[user.identityProvider], @@ -180,7 +179,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {

) : (
-
+
{formMethods.formState.errors.apiError && (
diff --git a/apps/web/playwright/fixtures/regularBookings.ts b/apps/web/playwright/fixtures/regularBookings.ts index 5bd9d80c3a..4d2f6328f4 100644 --- a/apps/web/playwright/fixtures/regularBookings.ts +++ b/apps/web/playwright/fixtures/regularBookings.ts @@ -1,5 +1,7 @@ import { expect, type Page } from "@playwright/test"; +import dayjs from "@calcom/dayjs"; + import type { createUsersFixture } from "./users"; const reschedulePlaceholderText = "Let others know why you need to reschedule"; @@ -38,6 +40,12 @@ type fillAndConfirmBookingParams = { type UserFixture = ReturnType; +function isLastDayOfMonth(): boolean { + const today = dayjs(); + const endOfMonth = today.endOf("month"); + return today.isSame(endOfMonth, "day"); +} + const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => { const questionActions: QuestionActions = { phone: async () => { @@ -114,6 +122,17 @@ export async function loginUser(users: UserFixture) { await pro.apiLogin(); } +const goToNextMonthIfNoAvailabilities = async (eventTypePage: Page) => { + try { + if (isLastDayOfMonth()) { + await eventTypePage.getByTestId("view_next_month").waitFor({ timeout: 6000 }); + await eventTypePage.getByTestId("view_next_month").click(); + } + } catch (err) { + console.info("No need to click on view next month button"); + } +}; + export function createBookingPageFixture(page: Page) { return { goToEventType: async (eventType: string) => { @@ -154,19 +173,13 @@ export function createBookingPageFixture(page: Page) { return eventtypePromise; }, selectTimeSlot: async (eventTypePage: Page) => { - while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) { - await eventTypePage.getByRole("button", { name: "View next" }).click(); - } + await goToNextMonthIfNoAvailabilities(eventTypePage); await eventTypePage.getByTestId("time").first().click(); }, clickReschedule: async () => { await page.getByText("Reschedule").click(); }, - navigateToAvailableTimeSlot: async () => { - while (await page.getByRole("button", { name: "View next" }).isVisible()) { - await page.getByRole("button", { name: "View next" }).click(); - } - }, + selectFirstAvailableTime: async () => { await page.getByTestId("time").first().click(); }, @@ -186,6 +199,7 @@ export function createBookingPageFixture(page: Page) { }, rescheduleBooking: async (eventTypePage: Page) => { + await goToNextMonthIfNoAvailabilities(eventTypePage); await eventTypePage.getByText("Reschedule").click(); while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) { await eventTypePage.getByRole("button", { name: "View next" }).click(); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index f0b6749d7b..f80967618f 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1296,6 +1296,7 @@ "select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.", "check_for_conflicts": "Check for conflicts", "view_recordings": "View recordings", + "check_for_recordings":"Check for recordings", "adding_events_to": "Adding events to", "follow_system_preferences": "Follow system preferences", "custom_brand_colors": "Custom brand colors", diff --git a/packages/emails/templates/organizer-daily-video-download-recording-email.ts b/packages/emails/templates/organizer-daily-video-download-recording-email.ts index 60c673ef5c..714ba4f7e4 100644 --- a/packages/emails/templates/organizer-daily-video-download-recording-email.ts +++ b/packages/emails/templates/organizer-daily-video-download-recording-email.ts @@ -50,8 +50,13 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail return this.getFormattedRecipientTime({ time: this.calEvent.endTime, format }); } + protected getLocale(): string { + return this.calEvent.organizer.language.locale; + } + protected getFormattedDate() { const organizerTimeFormat = this.calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR; + return `${this.getOrganizerStart(organizerTimeFormat)} - ${this.getOrganizerEnd( organizerTimeFormat )}, ${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t( diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx index 2bea7d04fa..6571bc1372 100644 --- a/packages/features/calendars/DatePicker.tsx +++ b/packages/features/calendars/DatePicker.tsx @@ -95,7 +95,7 @@ const NoAvailabilityOverlay = ({ return (

{t("no_availability_in_month", { month: month })}

-
diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.timezone.test.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.timezone.test.ts index c8475e3311..0d8c5d10aa 100644 --- a/packages/features/calendars/lib/getAvailableDatesInMonth.timezone.test.ts +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.timezone.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test, vi } from "vitest"; +import dayjs from "@calcom/dayjs"; import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; @@ -63,5 +64,22 @@ describe("Test Suite: Date Picker", () => { vi.setSystemTime(vi.getRealSystemTime()); vi.useRealTimers(); }); + + test("it returns the correct responses end of month", () => { + // test a date at one minute past midnight, end of month. + // we use dayjs() as the system timezone can still modify the Date. + vi.useFakeTimers().setSystemTime(dayjs().endOf("month").startOf("day").add(1, "second").toDate()); + + const currentDate = new Date(); + const result = getAvailableDatesInMonth({ + browsingDate: currentDate, + }); + + expect(result).toHaveLength(1); + + // Undo the forced time we applied earlier, reset to system default. + vi.setSystemTime(vi.getRealSystemTime()); + vi.useRealTimers(); + }); }); }); diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.ts index 8e50ef9793..a00f83d516 100644 --- a/packages/features/calendars/lib/getAvailableDatesInMonth.ts +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.ts @@ -1,3 +1,4 @@ +import dayjs from "@calcom/dayjs"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; // calculate the available dates in the month: @@ -21,7 +22,9 @@ export function getAvailableDatesInMonth({ ); for ( let date = browsingDate > minDate ? browsingDate : minDate; - date <= lastDateOfMonth; + // Check if date is before the last date of the month + // or is the same day, in the same month, in the same year. + date < lastDateOfMonth || dayjs(date).isSame(lastDateOfMonth, "day"); date = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1) ) { // intersect included dates diff --git a/packages/features/ee/organizations/pages/settings/appearance.tsx b/packages/features/ee/organizations/pages/settings/appearance.tsx index 622191baf6..04a004e9e3 100644 --- a/packages/features/ee/organizations/pages/settings/appearance.tsx +++ b/packages/features/ee/organizations/pages/settings/appearance.tsx @@ -194,7 +194,7 @@ const OrgAppearanceView = ({ />
) : ( -
+
{t("only_owner_change")}
)} diff --git a/packages/features/ee/organizations/pages/settings/profile.tsx b/packages/features/ee/organizations/pages/settings/profile.tsx index 2ded27e320..e6d652dfd1 100644 --- a/packages/features/ee/organizations/pages/settings/profile.tsx +++ b/packages/features/ee/organizations/pages/settings/profile.tsx @@ -113,7 +113,7 @@ const OrgProfileView = () => { {isOrgAdminOrOwner ? ( ) : ( -
+
diff --git a/packages/features/ee/payments/components/Payment.tsx b/packages/features/ee/payments/components/Payment.tsx index dc575e6320..92705c3deb 100644 --- a/packages/features/ee/payments/components/Payment.tsx +++ b/packages/features/ee/payments/components/Payment.tsx @@ -147,7 +147,7 @@ const PaymentForm = (props: Props) => { formatParams: { amount: { currency: props.payment.currency } }, })} onChange={(e) => setHoldAcknowledged(e.target.checked)} - descriptionClassName="text-blue-900 font-semibold" + descriptionClassName="text-info font-semibold" />
)} diff --git a/packages/features/ee/video/ViewRecordingsDialog.tsx b/packages/features/ee/video/ViewRecordingsDialog.tsx index 74563b35fd..6134304d25 100644 --- a/packages/features/ee/video/ViewRecordingsDialog.tsx +++ b/packages/features/ee/video/ViewRecordingsDialog.tsx @@ -174,7 +174,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => { return ( - + {roomName ? (