From bdd3b132d441a226cb0a0c477cc86fe34e1c3272 Mon Sep 17 00:00:00 2001
From: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Date: Mon, 20 Nov 2023 12:19:33 +0000
Subject: [PATCH] feat: troubleshooter with weekly view (V2) (#12280)
* Inital UI + layout setup
* use booker approach of grid
* event-select - sidebar + store work
* adds get schedule by event-type-slug
* Calendar toggle
* Load schedule from event slug
* Add busy events to calendar
* useschedule
* Store more event info than just slug
* Add date override to calendar
* Changes sizes on smaller screens
* add event title as a tooltip
* Ensure header navigation works
* Stop navigator throwing errors on inital render
* Correct br
* Event duration fixes
* Add getMoreInfo if user is authed with current request.username
* Add calendar color map wip
* Add WIP comments for coloured outlines
* Revert more info changes
* Calculate date override correctly
* Add description option
* Fix inital schedule data not being populated
* Nudge overlap over to make it clearer
* Fix disabled state
* WIP on math logic
* Event list overlapping events logic
* NIT about width
* i18n + manage calendars link
* Delete old troubleshooter
* Update packages/features/calendars/weeklyview/components/event/EventList.tsx
* Remove t-slots
* Fix i18n & install calendar action
* sm:imrovments
* NITS
* Fix types
* fix: back button
* Month prop null as we control from query param
* Add head SEO
* Fix headseo import
* Fix date override tests
---
apps/web/pages/availability/troubleshoot.tsx | 141 ++---------------
apps/web/playwright/availability.e2e.ts | 1 +
apps/web/public/static/locales/en/common.json | 6 +
packages/core/getCalendarsEvents.ts | 6 +-
.../weeklyview/components/event/Event.tsx | 58 ++++---
.../weeklyview/components/event/EventList.tsx | 86 +++++++----
.../components/verticalLines/index.tsx | 8 +-
.../calendars/weeklyview/state/store.ts | 4 +-
.../calendars/weeklyview/types/events.ts | 4 +
.../troubleshooter/Troubleshooter.tsx | 70 +++++++++
.../AvailabilitySchedulesContainer.tsx | 38 +++++
.../components/CalendarToggleContainer.tsx | 121 +++++++++++++++
.../components/ConnectedAppsContainer.tsx | 38 +++++
.../components/EventScheduleItem.tsx | 42 ++++++
.../components/EventTypeSelect.tsx | 53 +++++++
.../components/LargeCalendar.tsx | 142 ++++++++++++++++++
.../components/TroubleshooterHeader.tsx | 80 ++++++++++
.../TroubleshooterListItemContainer.tsx | 42 ++++++
.../components/TroubleshooterSidebar.tsx | 39 +++++
packages/features/troubleshooter/layout.tsx | 23 +++
packages/features/troubleshooter/store.ts | 110 ++++++++++++++
packages/features/troubleshooter/types.ts | 13 ++
.../viewer/availability/schedule/_router.tsx | 19 +++
.../getScheduleByEventTypeSlug.handler.ts | 69 +++++++++
.../getScheduleByEventTypeSlug.schema.ts | 7 +
.../viewer/availability/user.handler.ts | 7 +-
26 files changed, 1038 insertions(+), 189 deletions(-)
create mode 100644 packages/features/troubleshooter/Troubleshooter.tsx
create mode 100644 packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx
create mode 100644 packages/features/troubleshooter/components/CalendarToggleContainer.tsx
create mode 100644 packages/features/troubleshooter/components/ConnectedAppsContainer.tsx
create mode 100644 packages/features/troubleshooter/components/EventScheduleItem.tsx
create mode 100644 packages/features/troubleshooter/components/EventTypeSelect.tsx
create mode 100644 packages/features/troubleshooter/components/LargeCalendar.tsx
create mode 100644 packages/features/troubleshooter/components/TroubleshooterHeader.tsx
create mode 100644 packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx
create mode 100644 packages/features/troubleshooter/components/TroubleshooterSidebar.tsx
create mode 100644 packages/features/troubleshooter/layout.tsx
create mode 100644 packages/features/troubleshooter/store.ts
create mode 100644 packages/features/troubleshooter/types.ts
create mode 100644 packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts
create mode 100644 packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts
diff --git a/apps/web/pages/availability/troubleshoot.tsx b/apps/web/pages/availability/troubleshoot.tsx
index e3bfd24f0e..e37b598d68 100644
--- a/apps/web/pages/availability/troubleshoot.tsx
+++ b/apps/web/pages/availability/troubleshoot.tsx
@@ -1,139 +1,20 @@
-import dayjs from "@calcom/dayjs";
-import Shell from "@calcom/features/shell/Shell";
+import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
+import { getLayout } from "@calcom/features/troubleshooter/layout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
-import type { RouterOutputs } from "@calcom/trpc/react";
-import { trpc } from "@calcom/trpc/react";
-import { SkeletonText } from "@calcom/ui";
-
-import useRouterQuery from "@lib/hooks/useRouterQuery";
+import { HeadSeo } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
-type User = RouterOutputs["viewer"]["me"];
-
-export interface IBusySlot {
- start: string | Date;
- end: string | Date;
- title?: string;
- source?: string | null;
-}
-
-const AvailabilityView = ({ user }: { user: User }) => {
- const { t } = useLocale();
- const { date, setQuery: setSelectedDate } = useRouterQuery("date");
- const selectedDate = dayjs(date);
- const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
-
- const { data, isLoading } = trpc.viewer.availability.user.useQuery(
- {
- username: user.username || "",
- dateFrom: selectedDate.startOf("day").utc().format(),
- dateTo: selectedDate.endOf("day").utc().format(),
- withSource: true,
- },
- {
- enabled: !!user.username,
- }
- );
-
- const overrides =
- data?.dateOverrides.reduce((acc, override) => {
- if (
- formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
- formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
- )
- return acc;
- acc.push({ ...override, source: "Date override" });
- return acc;
- }, [] as IBusySlot[]) || [];
-
- return (
-
-
- {t("overview_of_day")}{" "}
-
{
- if (e.target.value) setSelectedDate(e.target.value);
- }}
- />
-
{t("hover_over_bold_times_tip")}
-
-
-
- {t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
-
-
- {(() => {
- if (isLoading)
- return (
- <>
-
-
- >
- );
-
- if (data && (data.busy.length > 0 || overrides.length > 0))
- return [...data.busy, ...overrides]
- .sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
- .map((slot: IBusySlot) => (
-
-
- {t("calendar_shows_busy_between")}{" "}
-
- {dayjs(slot.start).format("HH:mm")}
- {" "}
- {t("and")}{" "}
-
- {dayjs(slot.end).format("HH:mm")}
- {" "}
- {t("on")} {dayjs(slot.start).format("D")}{" "}
- {t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
- {slot.title && ` - (${slot.title})`}
- {slot.source && {` - (source: ${slot.source})`} }
-
-
- ));
- return (
-
-
{t("calendar_no_busy_slots")}
-
- );
- })()}
-
-
-
- {t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
-
-
-
-
-
- );
-};
-
-export default function Troubleshoot() {
- const { data, isLoading } = trpc.viewer.me.useQuery();
+function TroubleshooterPage() {
const { t } = useLocale();
return (
-
-
- {!isLoading && data && }
-
-
+ <>
+
+
+ >
);
}
-Troubleshoot.PageWrapper = PageWrapper;
-function convertMinsToHrsMins(mins: number) {
- const h = Math.floor(mins / 60);
- const m = mins % 60;
- const hs = h < 10 ? `0${h}` : h;
- const ms = m < 10 ? `0${m}` : m;
- return `${hs}:${ms}`;
-}
+TroubleshooterPage.getLayout = getLayout;
+TroubleshooterPage.PageWrapper = PageWrapper;
+export default TroubleshooterPage;
diff --git a/apps/web/playwright/availability.e2e.ts b/apps/web/playwright/availability.e2e.ts
index d065e3dbb5..bd07557603 100644
--- a/apps/web/playwright/availability.e2e.ts
+++ b/apps/web/playwright/availability.e2e.ts
@@ -40,6 +40,7 @@ test.describe("Availablity tests", () => {
const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date);
const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`;
await page.goto(troubleshooterURL);
+ await page.waitForLoadState("networkidle");
await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1);
});
});
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 69906ee67e..c3a505de55 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2112,8 +2112,14 @@
"overlay_my_calendar":"Overlay my calendar",
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
+ "troubleshooting":"Troubleshooting",
+ "calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
+ "availabilty_schedules":"Availability schedules",
+ "manage_calendars":"Manage calendars",
+ "manage_availability_schedules":"Manage availability schedules",
"lock_timezone_toggle_on_booking_page": "Lock timezone on booking page",
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
+ "install_calendar":"Install Calendar",
"branded_subdomain": "Branded Subdomain",
"branded_subdomain_description": "Get your own branded subdomain, such as acme.cal.com",
"org_insights": "Organization-wide Insights",
diff --git a/packages/core/getCalendarsEvents.ts b/packages/core/getCalendarsEvents.ts
index 8ee9d44fb0..f8700ddcf9 100644
--- a/packages/core/getCalendarsEvents.ts
+++ b/packages/core/getCalendarsEvents.ts
@@ -33,6 +33,7 @@ const getCalendarsEvents = async (
const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type);
if (!passedSelectedCalendars.length) return [];
/** We extract external Ids so we don't cache too much */
+
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
/** If we don't then we actually fetch external calendars (which can be very slow) */
performance.mark("eventBusyDatesStart");
@@ -51,7 +52,10 @@ const getCalendarsEvents = async (
"eventBusyDatesEnd"
);
- return eventBusyDates.map((a) => ({ ...a, source: `${appId}` }));
+ return eventBusyDates.map((a) => ({
+ ...a,
+ source: `${appId}`,
+ }));
});
const awaitedResults = await Promise.all(results);
performance.mark("getBusyCalendarTimesEnd");
diff --git a/packages/features/calendars/weeklyview/components/event/Event.tsx b/packages/features/calendars/weeklyview/components/event/Event.tsx
index c997e9a16d..8cb854ae09 100644
--- a/packages/features/calendars/weeklyview/components/event/Event.tsx
+++ b/packages/features/calendars/weeklyview/components/event/Event.tsx
@@ -1,6 +1,8 @@
import { cva } from "class-variance-authority";
import dayjs from "@calcom/dayjs";
+import classNames from "@calcom/lib/classNames";
+import { Tooltip } from "@calcom/ui";
import type { CalendarEvent } from "../../types/events";
@@ -13,7 +15,7 @@ type EventProps = {
};
const eventClasses = cva(
- "group flex h-full w-full flex-col overflow-y-auto rounded-[4px] px-[6px] py-1 text-xs font-semibold leading-5 ",
+ "group flex h-full w-full overflow-y-auto rounded-[6px] px-[6px] text-xs font-semibold leading-5 opacity-80",
{
variants: {
status: {
@@ -62,23 +64,41 @@ export function Event({
const Component = onEventClick ? "button" : "div";
return (
- onEventClick?.(event)} // Note this is not the button event. It is the calendar event.
- className={eventClasses({
- status: options?.status,
- disabled,
- selected,
- borderColor,
- })}
- style={styles}>
-
- {event.title}
-
- {eventDuration > 30 && (
-
- {dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
-
- )}
-
+
+ onEventClick?.(event)} // Note this is not the button event. It is the calendar event.
+ className={classNames(
+ eventClasses({
+ status: options?.status,
+ disabled,
+ selected,
+ borderColor,
+ }),
+ eventDuration > 30 && "flex-col py-1",
+ options?.className
+ )}
+ style={styles}>
+
+
{event.title}
+ {eventDuration <= 30 && !event.options?.hideTime && (
+
+ {dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
+
+ )}
+
+ {eventDuration > 30 && !event.options?.hideTime && (
+
+ {dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
+
+ )}
+ {eventDuration > 45 && event.description && (
+ {event.description}
+ )}
+
+
);
}
diff --git a/packages/features/calendars/weeklyview/components/event/EventList.tsx b/packages/features/calendars/weeklyview/components/event/EventList.tsx
index 57cbb47e8c..ed6af0e763 100644
--- a/packages/features/calendars/weeklyview/components/event/EventList.tsx
+++ b/packages/features/calendars/weeklyview/components/event/EventList.tsx
@@ -1,3 +1,4 @@
+import { useRef } from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
@@ -19,6 +20,14 @@ export function EventList({ day }: Props) {
shallow
);
+ // Use a ref so we dont trigger a re-render
+ const longestRef = useRef<{
+ start: Date;
+ end: Date;
+ duration: number;
+ idx: number;
+ } | null>(null);
+
return (
<>
{events
@@ -41,47 +50,59 @@ export function EventList({ day }: Props) {
const nextEvent = eventsArray[idx + 1];
const prevEvent = eventsArray[idx - 1];
- // Check for overlapping events since this is sorted it should just work.
- if (nextEvent) {
- const nextEventStart = dayjs(nextEvent.start);
- const nextEventEnd = dayjs(nextEvent.end);
- // check if next event starts before this event ends
- if (nextEventStart.isBefore(eventEnd)) {
- // figure out which event has the longest duration
- const nextEventDuration = nextEventEnd.diff(nextEventStart, "minutes");
- if (nextEventDuration > eventDuration) {
+ if (!longestRef.current) {
+ longestRef.current = {
+ idx,
+ start: eventStart.toDate(),
+ end: eventEnd.toDate(),
+ duration: eventDuration,
+ };
+ } else if (
+ eventDuration > longestRef.current.duration &&
+ eventStart.isBetween(longestRef.current.start, longestRef.current.end)
+ ) {
+ longestRef.current = {
+ idx,
+ start: eventStart.toDate(),
+ end: eventEnd.toDate(),
+ duration: eventDuration,
+ };
+ }
+ // By default longest event doesnt have any styles applied
+ if (longestRef.current.idx !== idx) {
+ if (nextEvent) {
+ // If we have a next event
+ const nextStart = dayjs(nextEvent.start);
+ // If the next event is inbetween the longest start and end make 65% width
+ if (nextStart.isBetween(longestRef.current.start, longestRef.current.end)) {
zIndex = 65;
-
marginLeft = "auto";
- // 8 looks like a really random number but we need to take into account the bordersize on the event.
- // Logically it should be 5% but this causes a bit of a overhang which we don't want.
- right = 8;
+ right = 4;
+ width = width / 2;
+
+ // If not - we check to see if the next starts within 5 mins of this event - allowing us to do side by side events whenwe have
+ // close start times
+ } else if (nextStart.isBetween(eventStart.add(-5, "minutes"), eventStart.add(5, "minutes"))) {
+ zIndex = 65;
+ marginLeft = "auto";
+ right = 4;
width = width / 2;
}
- }
+ } else if (prevEvent) {
+ const prevStart = dayjs(prevEvent.start);
- if (nextEventStart.isSame(eventStart)) {
- zIndex = 66;
+ // If the next event is inbetween the longest start and end make 65% width
- marginLeft = "auto";
- right = 8;
- width = width / 2;
- }
- } else if (prevEvent) {
- const prevEventStart = dayjs(prevEvent.start);
- const prevEventEnd = dayjs(prevEvent.end);
- // check if next event starts before this event ends
- if (prevEventEnd.isAfter(eventStart)) {
- // figure out which event has the longest duration
- const prevEventDuration = prevEventEnd.diff(prevEventStart, "minutes");
- if (prevEventDuration > eventDuration) {
+ if (prevStart.isBetween(longestRef.current.start, longestRef.current.end)) {
zIndex = 65;
marginLeft = "auto";
- right = 8;
+ right = 4;
+ // If not - we check to see if the next starts within 5 mins of this event - allowing us to do side by side events whenwe have
+ // close start times (Inverse of above )
+ } else if (eventStart.isBetween(prevStart.add(5, "minutes"), prevStart.add(-5, "minutes"))) {
+ zIndex = 65;
+ right = 4;
width = width / 2;
- if (eventDuration >= 30) {
- width = 80;
- }
}
}
}
@@ -90,6 +111,7 @@ export function EventList({ day }: Props) {
{
const isRTL = () => {
- const userLocale = navigator.language;
- const userLanguage = new Intl.Locale(userLocale).language;
+ let userLanguage = "en"; // Default to 'en' if navigator is not defined
+
+ if (typeof window !== "undefined" && typeof navigator !== "undefined") {
+ const userLocale = navigator.language;
+ userLanguage = new Intl.Locale(userLocale).language;
+ }
return ["ar", "he", "fa", "ur"].includes(userLanguage);
};
diff --git a/packages/features/calendars/weeklyview/state/store.ts b/packages/features/calendars/weeklyview/state/store.ts
index 9424eec9b8..af1b52358d 100644
--- a/packages/features/calendars/weeklyview/state/store.ts
+++ b/packages/features/calendars/weeklyview/state/store.ts
@@ -32,9 +32,7 @@ export const useCalendarStore = create
((set) => ({
let events = state.events;
if (state.sortEvents) {
- events = state.events.sort(
- (a, b) => dayjs(a.start).get("milliseconds") - dayjs(b.start).get("milliseconds")
- );
+ events = state.events.sort((a, b) => dayjs(a.start).valueOf() - dayjs(b.start).valueOf());
}
const blockingDates = mergeOverlappingDateRanges(state.blockingDates || []); // We merge overlapping dates so we don't get duplicate blocking "Cells" in the UI
diff --git a/packages/features/calendars/weeklyview/types/events.ts b/packages/features/calendars/weeklyview/types/events.ts
index bade4cf0c4..493750ea8f 100644
--- a/packages/features/calendars/weeklyview/types/events.ts
+++ b/packages/features/calendars/weeklyview/types/events.ts
@@ -3,12 +3,16 @@ import type { BookingStatus } from "@calcom/prisma/enums";
export interface CalendarEvent {
id: number;
title: string;
+ description?: string;
start: Date | string; // You can pass in a string from DB since we use dayjs for the dates.
end: Date;
source?: string;
options?: {
status?: BookingStatus;
+ hideTime?: boolean;
allDay?: boolean;
borderColor?: string;
+ className?: string;
+ "data-test-id"?: string;
};
}
diff --git a/packages/features/troubleshooter/Troubleshooter.tsx b/packages/features/troubleshooter/Troubleshooter.tsx
new file mode 100644
index 0000000000..ad9457fdb1
--- /dev/null
+++ b/packages/features/troubleshooter/Troubleshooter.tsx
@@ -0,0 +1,70 @@
+import StickyBox from "react-sticky-box";
+
+import classNames from "@calcom/lib/classNames";
+import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
+
+import { LargeCalendar } from "./components/LargeCalendar";
+import { TroubleshooterHeader } from "./components/TroubleshooterHeader";
+import { TroubleshooterSidebar } from "./components/TroubleshooterSidebar";
+import { useInitalizeTroubleshooterStore } from "./store";
+import type { TroubleshooterProps } from "./types";
+
+const extraDaysConfig = {
+ desktop: 7,
+ tablet: 4,
+};
+
+const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
+ const isMobile = useMediaQuery("(max-width: 768px)");
+ const isTablet = useMediaQuery("(max-width: 1024px)");
+ const extraDays = isTablet ? extraDaysConfig.tablet : extraDaysConfig.desktop;
+
+ useInitalizeTroubleshooterStore({
+ month: month,
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export const Troubleshooter = ({ month }: TroubleshooterProps) => {
+ return ;
+};
diff --git a/packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx b/packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx
new file mode 100644
index 0000000000..ee3196b731
--- /dev/null
+++ b/packages/features/troubleshooter/components/AvailabilitySchedulesContainer.tsx
@@ -0,0 +1,38 @@
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Badge, Button, Switch } from "@calcom/ui";
+
+import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
+
+function AvailabiltyItem() {
+ const { t } = useLocale();
+ return (
+
+
+ Connected
+
+
+ }>
+
+
{t("date_overrides")}
+
+
+
+ );
+}
+
+export function AvailabiltySchedulesContainer() {
+ const { t } = useLocale();
+ return (
+
+
{t("availabilty_schedules")}
+
+
+ {t("manage_availabilty_schedules")}
+
+
+ );
+}
diff --git a/packages/features/troubleshooter/components/CalendarToggleContainer.tsx b/packages/features/troubleshooter/components/CalendarToggleContainer.tsx
new file mode 100644
index 0000000000..4fceb7bf81
--- /dev/null
+++ b/packages/features/troubleshooter/components/CalendarToggleContainer.tsx
@@ -0,0 +1,121 @@
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Badge, Button, Switch } from "@calcom/ui";
+
+import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
+
+const SELECTION_COLORS = ["#f97316", "#84cc16", "#06b6d4", "#8b5cf6", "#ec4899", "#f43f5e"];
+
+interface CalendarToggleItemProps {
+ title: string;
+ subtitle: string;
+ colorDot?: string;
+ status: "connected" | "not_found";
+ calendars?: {
+ active?: boolean;
+ name?: string;
+ }[];
+}
+function CalendarToggleItem(props: CalendarToggleItemProps) {
+ const badgeStatus = props.status === "connected" ? "green" : "orange";
+ const badgeText = props.status === "connected" ? "Connected" : "Not found";
+ return (
+
+
+ >
+ }
+ suffixSlot={
+
+
+ {badgeText}
+
+
+ }>
+
+ {props.calendars?.map((calendar) => {
+ return ;
+ })}
+
+
+ );
+}
+
+function EmptyCalendarToggleItem() {
+ const { t } = useLocale();
+
+ return (
+
+
+ >
+ }
+ suffixSlot={
+
+
+ Not found
+
+
+ }>
+
+
+ {t("install_calendar")}
+
+
+
+ );
+}
+
+export function CalendarToggleContainer() {
+ const { t } = useLocale();
+ const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery();
+
+ const hasConnectedCalendars = data && data?.connectedCalendars.length > 0;
+
+ return (
+
+
{t("calendars_were_checking_for_conflicts")}
+ {hasConnectedCalendars && !isLoading ? (
+ <>
+ {data.connectedCalendars.map((calendar) => {
+ const foundPrimary = calendar.calendars?.find((item) => item.primary);
+ // Will be used when getAvailbility is modified to use externalId instead of appId for source.
+ // const color = SELECTION_COLORS[idx] || "#000000";
+ // // Add calendar to color map using externalId (what we use on the backend to determine source)
+ // addToColorMap(foundPrimary?.externalId, color);
+ return (
+
{
+ return {
+ active: item.isSelected,
+ name: item.name,
+ };
+ })}
+ />
+ );
+ })}
+
+ {t("manage_calendars")}
+
+ >
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/packages/features/troubleshooter/components/ConnectedAppsContainer.tsx b/packages/features/troubleshooter/components/ConnectedAppsContainer.tsx
new file mode 100644
index 0000000000..d25c70ada1
--- /dev/null
+++ b/packages/features/troubleshooter/components/ConnectedAppsContainer.tsx
@@ -0,0 +1,38 @@
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Badge } from "@calcom/ui";
+
+import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
+
+function ConnectedAppsItem() {
+ return (
+
+
+ >
+ }
+ suffixSlot={
+
+
+ Connected
+
+
+ }
+ />
+ );
+}
+
+export function ConnectedAppsContainer() {
+ const { t } = useLocale();
+ return (
+
+
{t("other_apps")}
+
+
+
+
+
+ );
+}
diff --git a/packages/features/troubleshooter/components/EventScheduleItem.tsx b/packages/features/troubleshooter/components/EventScheduleItem.tsx
new file mode 100644
index 0000000000..5cd47b2ccb
--- /dev/null
+++ b/packages/features/troubleshooter/components/EventScheduleItem.tsx
@@ -0,0 +1,42 @@
+import Link from "next/link";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Badge, Label } from "@calcom/ui";
+
+import { useTroubleshooterStore } from "../store";
+import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
+
+export function EventScheduleItem() {
+ const { t } = useLocale();
+ const selectedEventType = useTroubleshooterStore((state) => state.event);
+
+ const { data: schedule } = trpc.viewer.availability.schedule.getScheduleByEventSlug.useQuery(
+ {
+ eventSlug: selectedEventType?.slug as string,
+ },
+ {
+ enabled: !!selectedEventType?.slug,
+ }
+ );
+
+ return (
+
+ Availability Schedule
+ }
+ title={schedule?.name ?? "Loading"}
+ suffixSlot={
+ schedule && (
+
+
+ {t("edit")}
+
+
+ )
+ }
+ />
+
+ );
+}
diff --git a/packages/features/troubleshooter/components/EventTypeSelect.tsx b/packages/features/troubleshooter/components/EventTypeSelect.tsx
new file mode 100644
index 0000000000..dac68067b7
--- /dev/null
+++ b/packages/features/troubleshooter/components/EventTypeSelect.tsx
@@ -0,0 +1,53 @@
+import { useMemo, useEffect } from "react";
+
+import { trpc } from "@calcom/trpc";
+import { SelectField } from "@calcom/ui";
+
+import { useTroubleshooterStore } from "../store";
+
+export function EventTypeSelect() {
+ const { data: eventTypes, isLoading } = trpc.viewer.eventTypes.list.useQuery();
+ const selectedEventType = useTroubleshooterStore((state) => state.event);
+ const setSelectedEventType = useTroubleshooterStore((state) => state.setEvent);
+
+ // const selectedEventQueryParam = getQueryParam("eventType");
+
+ const options = useMemo(() => {
+ if (!eventTypes) return [];
+ return eventTypes.map((e) => ({
+ label: e.title,
+ value: e.slug,
+ id: e.id,
+ duration: e.length,
+ }));
+ }, [eventTypes]);
+
+ useEffect(() => {
+ if (!selectedEventType && eventTypes && eventTypes[0]) {
+ const { id, slug, length } = eventTypes[0];
+ setSelectedEventType({
+ id,
+ slug,
+ duration: length,
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [eventTypes]);
+
+ return (
+ option.value === selectedEventType?.slug) || options[0]}
+ onChange={(option) => {
+ if (!option) return;
+ setSelectedEventType({
+ slug: option.value,
+ id: option.id,
+ duration: option.duration,
+ });
+ }}
+ />
+ );
+}
diff --git a/packages/features/troubleshooter/components/LargeCalendar.tsx b/packages/features/troubleshooter/components/LargeCalendar.tsx
new file mode 100644
index 0000000000..7884e38e18
--- /dev/null
+++ b/packages/features/troubleshooter/components/LargeCalendar.tsx
@@ -0,0 +1,142 @@
+import { useSession } from "next-auth/react";
+import { useMemo } from "react";
+
+import dayjs from "@calcom/dayjs";
+import { Calendar } from "@calcom/features/calendars/weeklyview";
+import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
+import { BookingStatus } from "@calcom/prisma/enums";
+import { trpc } from "@calcom/trpc";
+
+import { useTimePreferences } from "../../bookings/lib/timePreferences";
+import { useSchedule } from "../../schedules/lib/use-schedule";
+import { useTroubleshooterStore } from "../store";
+
+export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
+ const { timezone } = useTimePreferences();
+ const selectedDate = useTroubleshooterStore((state) => state.selectedDate);
+ const event = useTroubleshooterStore((state) => state.event);
+ const calendarToColorMap = useTroubleshooterStore((state) => state.calendarToColorMap);
+ const { data: session } = useSession();
+ const startDate = selectedDate ? dayjs(selectedDate) : dayjs();
+
+ const { data: busyEvents } = trpc.viewer.availability.user.useQuery(
+ {
+ username: session?.user?.username || "",
+ dateFrom: startDate.startOf("day").utc().format(),
+ dateTo: startDate
+ .endOf("day")
+ .add(extraDays - 1, "day")
+ .utc()
+ .format(),
+ withSource: true,
+ },
+ {
+ enabled: !!session?.user?.username,
+ }
+ );
+
+ const { data: schedule } = useSchedule({
+ username: session?.user.username || "",
+ eventSlug: event?.slug,
+ eventId: event?.id,
+ timezone,
+ month: startDate.format("YYYY-MM"),
+ });
+
+ const endDate = dayjs(startDate)
+ .add(extraDays - 1, "day")
+ .toDate();
+
+ const availableSlots = useMemo(() => {
+ const availableTimeslots: CalendarAvailableTimeslots = {};
+ if (!schedule) return availableTimeslots;
+ if (!schedule?.slots) return availableTimeslots;
+
+ for (const day in schedule.slots) {
+ availableTimeslots[day] = schedule.slots[day].map((slot) => ({
+ start: dayjs(slot.time).toDate(),
+ end: dayjs(slot.time)
+ .add(event?.duration ?? 30, "minutes")
+ .toDate(),
+ }));
+ }
+
+ return availableTimeslots;
+ }, [schedule, event]);
+
+ const events = useMemo(() => {
+ if (!busyEvents?.busy) return [];
+
+ // TODO: Add buffer times in here as well just requires a bit of logic for fetching event type and then adding buffer time
+ // start: dayjs(startTime)
+ // .subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute")
+ // .toDate(),
+ // end: dayjs(endTime)
+ // .add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute")
+ // .toDate(),
+
+ const calendarEvents = busyEvents?.busy.map((event, idx) => {
+ return {
+ id: idx,
+ title: event.title ?? `Busy`,
+ start: new Date(event.start),
+ end: new Date(event.end),
+ options: {
+ borderColor:
+ event.source && calendarToColorMap[event.source] ? calendarToColorMap[event.source] : "black",
+ status: BookingStatus.ACCEPTED,
+ "data-test-id": "troubleshooter-busy-event",
+ },
+ };
+ });
+
+ if (busyEvents.dateOverrides) {
+ busyEvents.dateOverrides.forEach((dateOverride) => {
+ const dateOverrideStart = dayjs(dateOverride.start);
+ const dateOverrideEnd = dayjs(dateOverride.end);
+
+ if (!dateOverrideStart.isSame(dateOverrideEnd)) {
+ return;
+ }
+
+ const dayOfWeekNum = dateOverrideStart.day();
+
+ const workingHoursForDay = busyEvents.workingHours.find((workingHours) =>
+ workingHours.days.includes(dayOfWeekNum)
+ );
+
+ if (!workingHoursForDay) return;
+
+ calendarEvents.push({
+ id: calendarEvents.length,
+ title: "Date Override",
+ start: dateOverrideStart.add(workingHoursForDay.startTime, "minutes").toDate(),
+ end: dateOverrideEnd.add(workingHoursForDay.endTime, "minutes").toDate(),
+ options: {
+ borderColor: "black",
+ status: BookingStatus.ACCEPTED,
+ "data-test-id": "troubleshooter-busy-time",
+ },
+ });
+ });
+ }
+ return calendarEvents;
+ }, [busyEvents, calendarToColorMap]);
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/features/troubleshooter/components/TroubleshooterHeader.tsx b/packages/features/troubleshooter/components/TroubleshooterHeader.tsx
new file mode 100644
index 0000000000..90e6318184
--- /dev/null
+++ b/packages/features/troubleshooter/components/TroubleshooterHeader.tsx
@@ -0,0 +1,80 @@
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { useMemo } from "react";
+
+import dayjs from "@calcom/dayjs";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, ButtonGroup } from "@calcom/ui";
+
+import { useTroubleshooterStore } from "../store";
+
+export function TroubleshooterHeader({ extraDays, isMobile }: { extraDays: number; isMobile: boolean }) {
+ const { t, i18n } = useLocale();
+ const selectedDateString = useTroubleshooterStore((state) => state.selectedDate);
+ const setSelectedDate = useTroubleshooterStore((state) => state.setSelectedDate);
+ const addToSelectedDate = useTroubleshooterStore((state) => state.addToSelectedDate);
+ const selectedDate = selectedDateString ? dayjs(selectedDateString) : dayjs();
+ const today = dayjs();
+ const selectedDateMin3DaysDifference = useMemo(() => {
+ const diff = today.diff(selectedDate, "days");
+ return diff > 3 || diff < -3;
+ }, [today, selectedDate]);
+
+ if (isMobile) return null;
+
+ const endDate = selectedDate.add(extraDays - 1, "days");
+
+ const isSameMonth = () => {
+ return selectedDate.format("MMM") === endDate.format("MMM");
+ };
+
+ const isSameYear = () => {
+ return selectedDate.format("YYYY") === endDate.format("YYYY");
+ };
+ const formattedMonth = new Intl.DateTimeFormat(i18n.language, { month: "short" });
+ const FormattedSelectedDateRange = () => {
+ return (
+
+ {formattedMonth.format(selectedDate.toDate())} {selectedDate.format("D")}
+ {!isSameYear() && , {selectedDate.format("YYYY")} }-{" "}
+ {!isSameMonth() && formattedMonth.format(endDate.toDate())} {endDate.format("D")},{" "}
+
+ {isSameYear() ? selectedDate.format("YYYY") : endDate.format("YYYY")}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+ addToSelectedDate(-extraDays)}
+ />
+ addToSelectedDate(extraDays)}
+ />
+ {selectedDateMin3DaysDifference && (
+ setSelectedDate(today.format("YYYY-MM-DD"))}>
+ {t("today")}
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx b/packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx
new file mode 100644
index 0000000000..0d5bb956a3
--- /dev/null
+++ b/packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx
@@ -0,0 +1,42 @@
+import type { PropsWithChildren } from "react";
+
+import classNames from "@calcom/lib/classNames";
+
+interface TroubleshooterListItemContainerProps {
+ title: string;
+ subtitle?: string;
+ suffixSlot?: React.ReactNode;
+ prefixSlot?: React.ReactNode;
+ className?: string;
+}
+
+export function TroubleshooterListItemHeader({
+ prefixSlot,
+ title,
+ subtitle,
+ suffixSlot,
+ className,
+}: TroubleshooterListItemContainerProps) {
+ return (
+
+ {prefixSlot}
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+ {suffixSlot}
+
+ );
+}
+
+export function TroubleshooterListItemContainer({
+ children,
+ ...rest
+}: PropsWithChildren) {
+ return (
+
+ );
+}
diff --git a/packages/features/troubleshooter/components/TroubleshooterSidebar.tsx b/packages/features/troubleshooter/components/TroubleshooterSidebar.tsx
new file mode 100644
index 0000000000..101656db27
--- /dev/null
+++ b/packages/features/troubleshooter/components/TroubleshooterSidebar.tsx
@@ -0,0 +1,39 @@
+import { ArrowLeft } from "lucide-react";
+import Link from "next/link";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Skeleton } from "@calcom/ui";
+
+import { CalendarToggleContainer } from "./CalendarToggleContainer";
+import { EventScheduleItem } from "./EventScheduleItem";
+import { EventTypeSelect } from "./EventTypeSelect";
+
+const BackButtonInSidebar = ({ name }: { name: string }) => {
+ return (
+
+
+
+ {name}
+
+
+ );
+};
+
+export const TroubleshooterSidebar = () => {
+ const { t } = useLocale();
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/features/troubleshooter/layout.tsx b/packages/features/troubleshooter/layout.tsx
new file mode 100644
index 0000000000..fdcc968cd4
--- /dev/null
+++ b/packages/features/troubleshooter/layout.tsx
@@ -0,0 +1,23 @@
+import type { ComponentProps } from "react";
+import React, { Suspense } from "react";
+
+import Shell from "@calcom/features/shell/Shell";
+import { ErrorBoundary } from "@calcom/ui";
+import { Loader } from "@calcom/ui/components/icon";
+
+export default function TroubleshooterLayout({
+ children,
+ ...rest
+}: { children: React.ReactNode } & ComponentProps) {
+ return (
+ >}>
+
+
+ }>{children}
+
+
+
+ );
+}
+
+export const getLayout = (page: React.ReactElement) => {page} ;
diff --git a/packages/features/troubleshooter/store.ts b/packages/features/troubleshooter/store.ts
new file mode 100644
index 0000000000..4d5daa2601
--- /dev/null
+++ b/packages/features/troubleshooter/store.ts
@@ -0,0 +1,110 @@
+import { useEffect } from "react";
+import { create } from "zustand";
+
+import dayjs from "@calcom/dayjs";
+
+import { updateQueryParam, getQueryParam, removeQueryParam } from "../bookings/Booker/utils/query-param";
+
+/**
+ * Arguments passed into store initializer, containing
+ * the event data.
+ */
+type StoreInitializeType = {
+ month: string | null;
+};
+
+type EventType = {
+ id: number;
+ slug: string;
+ duration: number;
+};
+
+export type TroubleshooterStore = {
+ event: EventType | null;
+ setEvent: (eventSlug: EventType) => void;
+ month: string | null;
+ setMonth: (month: string | null) => void;
+ selectedDate: string | null;
+ setSelectedDate: (date: string | null) => void;
+ addToSelectedDate: (days: number) => void;
+ initialize: (data: StoreInitializeType) => void;
+ calendarToColorMap: Record;
+ addToCalendarToColorMap: (calendarId: string | undefined, color: string) => void;
+};
+
+/**
+ * The booker store contains the data of the component's
+ * current state. This data can be reused within child components
+ * by importing this hook.
+ *
+ * See comments in interface above for more information on it's specific values.
+ */
+export const useTroubleshooterStore = create((set, get) => ({
+ selectedDate: getQueryParam("date") || null,
+ setSelectedDate: (selectedDate: string | null) => {
+ // unset selected date
+ if (!selectedDate) {
+ removeQueryParam("date");
+ return;
+ }
+
+ const currentSelection = dayjs(get().selectedDate);
+ const newSelection = dayjs(selectedDate);
+ set({ selectedDate });
+ updateQueryParam("date", selectedDate ?? "");
+
+ // Setting month make sure small calendar in fullscreen layouts also updates.
+ if (newSelection.month() !== currentSelection.month()) {
+ set({ month: newSelection.format("YYYY-MM") });
+ updateQueryParam("month", newSelection.format("YYYY-MM"));
+ }
+ },
+ addToSelectedDate: (days: number) => {
+ const selectedDate = get().selectedDate;
+ const currentSelection = selectedDate ? dayjs(get().selectedDate) : dayjs();
+ const newSelection = currentSelection.add(days, "day");
+ const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
+
+ if (newSelection.month() !== currentSelection.month()) {
+ set({ month: newSelection.format("YYYY-MM") });
+ updateQueryParam("month", newSelection.format("YYYY-MM"));
+ }
+
+ set({ selectedDate: newSelectionFormatted });
+ updateQueryParam("date", newSelectionFormatted);
+ },
+ event: null,
+ setEvent: (event: EventType) => {
+ set({ event });
+ updateQueryParam("eventType", event.slug ?? "");
+ },
+ month: getQueryParam("month") || getQueryParam("date") || dayjs().format("YYYY-MM"),
+ setMonth: (month: string | null) => {
+ set({ month });
+ updateQueryParam("month", month ?? "");
+ get().setSelectedDate(null);
+ },
+ initialize: ({ month }: StoreInitializeType) => {
+ if (month) {
+ set({ month });
+ updateQueryParam("month", month);
+ }
+ //removeQueryParam("layout");
+ },
+ calendarToColorMap: {},
+ addToCalendarToColorMap: (calendarId: string | undefined, color: string) => {
+ if (!calendarId) return;
+ const calendarToColorMap = get().calendarToColorMap;
+ calendarToColorMap[calendarId] = color;
+ set({ calendarToColorMap });
+ },
+}));
+
+export const useInitalizeTroubleshooterStore = ({ month }: StoreInitializeType) => {
+ const initializeStore = useTroubleshooterStore((state) => state.initialize);
+ useEffect(() => {
+ initializeStore({
+ month,
+ });
+ }, [initializeStore, month]);
+};
diff --git a/packages/features/troubleshooter/types.ts b/packages/features/troubleshooter/types.ts
new file mode 100644
index 0000000000..a1029e077b
--- /dev/null
+++ b/packages/features/troubleshooter/types.ts
@@ -0,0 +1,13 @@
+export interface TroubleshooterProps {
+ /**
+ * If month is NOT set as a prop on the component, we expect a query parameter
+ * called `month` to be present on the url. If that is missing, the component will
+ * default to the current month.
+ * @note In case you're using a client side router, please pass the value in as a prop,
+ * since the component will leverage window.location, which might not have the query param yet.
+ * @format YYYY-MM.
+ * @optional
+ */
+ month: string | null;
+ selectedDate?: Date;
+}
diff --git a/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx b/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx
index b886f81a64..f6e6fde73b 100644
--- a/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx
+++ b/packages/trpc/server/routers/viewer/availability/schedule/_router.tsx
@@ -4,6 +4,7 @@ import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZScheduleDuplicateSchema } from "./duplicate.schema";
import { ZGetInputSchema } from "./get.schema";
+import { ZGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.schema";
import { ZGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
import { ZUpdateInputSchema } from "./update.schema";
@@ -14,6 +15,7 @@ type ScheduleRouterHandlerCache = {
update?: typeof import("./update.handler").updateHandler;
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
getScheduleByUserId?: typeof import("./getScheduleByUserId.handler").getScheduleByUserIdHandler;
+ getScheduleByEventSlug?: typeof import("./getScheduleByEventTypeSlug.handler").getScheduleByEventSlugHandler;
};
const UNSTABLE_HANDLER_CACHE: ScheduleRouterHandlerCache = {};
@@ -118,4 +120,21 @@ export const scheduleRouter = router({
input,
});
}),
+ getScheduleByEventSlug: authedProcedure.input(ZGetByEventSlugInputSchema).query(async ({ input, ctx }) => {
+ if (!UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug) {
+ UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug = await import(
+ "./getScheduleByEventTypeSlug.handler"
+ ).then((mod) => mod.getScheduleByEventSlugHandler);
+ }
+
+ // Unreachable code but required for type safety
+ if (!UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug) {
+ throw new Error("Failed to load handler");
+ }
+
+ return UNSTABLE_HANDLER_CACHE.getScheduleByEventSlug({
+ ctx,
+ input,
+ });
+ }),
});
diff --git a/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts
new file mode 100644
index 0000000000..50457eff3a
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts
@@ -0,0 +1,69 @@
+import type { PrismaClient } from "@calcom/prisma";
+
+import type { TrpcSessionUser } from "../../../../trpc";
+import { getHandler } from "./get.handler";
+import type { TGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.schema";
+
+type GetOptions = {
+ ctx: {
+ user: NonNullable;
+ prisma: PrismaClient;
+ };
+ input: TGetByEventSlugInputSchema;
+};
+
+const EMPTY_SCHEDULE = [[], [], [], [], [], [], []];
+
+export const getScheduleByEventSlugHandler = async ({ ctx, input }: GetOptions) => {
+ const foundScheduleForSlug = await ctx.prisma.eventType.findFirst({
+ where: {
+ slug: input.eventSlug,
+ userId: ctx.user.id,
+ },
+ select: {
+ scheduleId: true,
+ },
+ });
+
+ try {
+ // This looks kinda weird that we throw straight in the catch - its so that we can return a default schedule if the user has not completed onboarding @shiraz will loveme for this
+ if (!foundScheduleForSlug?.scheduleId) {
+ const foundUserDefaultId = await ctx.prisma.user.findUnique({
+ where: {
+ id: ctx.user.id,
+ },
+ select: {
+ defaultScheduleId: true,
+ },
+ });
+
+ if (foundUserDefaultId?.defaultScheduleId) {
+ return await getHandler({
+ ctx,
+ input: {
+ scheduleId: foundUserDefaultId?.defaultScheduleId,
+ },
+ });
+ }
+
+ throw new Error("NOT_FOUND");
+ }
+ return await getHandler({
+ ctx,
+ input: {
+ scheduleId: foundScheduleForSlug?.scheduleId,
+ },
+ });
+ } catch (e) {
+ console.log(e);
+ return {
+ id: -1,
+ name: "No schedules found",
+ availability: EMPTY_SCHEDULE,
+ dateOverrides: [],
+ timeZone: ctx.user.timeZone || "Europe/London",
+ workingHours: [],
+ isDefault: true,
+ };
+ }
+};
diff --git a/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts
new file mode 100644
index 0000000000..b65f1b5e37
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.schema.ts
@@ -0,0 +1,7 @@
+import { z } from "zod";
+
+export const ZGetByEventSlugInputSchema = z.object({
+ eventSlug: z.string(),
+});
+
+export type TGetByEventSlugInputSchema = z.infer;
diff --git a/packages/trpc/server/routers/viewer/availability/user.handler.ts b/packages/trpc/server/routers/viewer/availability/user.handler.ts
index d45c2d85c0..decfe5ca66 100644
--- a/packages/trpc/server/routers/viewer/availability/user.handler.ts
+++ b/packages/trpc/server/routers/viewer/availability/user.handler.ts
@@ -1,12 +1,15 @@
import { getUserAvailability } from "@calcom/core/getUserAvailability";
+import type { TrpcSessionUser } from "../../../trpc";
import type { TUserInputSchema } from "./user.schema";
type UserOptions = {
- ctx: Record;
+ ctx: {
+ user: NonNullable;
+ };
input: TUserInputSchema;
};
export const userHandler = async ({ input }: UserOptions) => {
- return getUserAvailability(input);
+ return getUserAvailability(input, undefined);
};