Compare commits
16 Commits
main
...
feat/troub
Author | SHA1 | Date |
---|---|---|
Sean Brydon | d9d2aff953 | |
Sean Brydon | 2a118e1a6b | |
Sean Brydon | 8464e74c43 | |
Sean Brydon | f11f587d1b | |
Sean Brydon | df9936858a | |
Sean Brydon | 98e12b162a | |
Sean Brydon | f080838855 | |
Sean Brydon | 2904230009 | |
Sean Brydon | 61eb439fa0 | |
Sean Brydon | 1b87cf80b9 | |
Sean Brydon | a9bf0d625f | |
Sean Brydon | 6b6e9dba2b | |
Sean Brydon | daab6a17d0 | |
Sean Brydon | 8e97169de4 | |
Sean Brydon | 6f01802f22 | |
Sean Brydon | f93d2d83e5 |
|
@ -1,139 +1,12 @@
|
||||||
import dayjs from "@calcom/dayjs";
|
import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
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 PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
type User = RouterOutputs["viewer"]["me"];
|
function TroubleshooterPage() {
|
||||||
|
return <Troubleshooter />;
|
||||||
export interface IBusySlot {
|
|
||||||
start: string | Date;
|
|
||||||
end: string | Date;
|
|
||||||
title?: string;
|
|
||||||
source?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AvailabilityView = ({ user }: { user: User }) => {
|
TroubleshooterPage.getLayout = getLayout;
|
||||||
const { t } = useLocale();
|
TroubleshooterPage.PageWrapper = PageWrapper;
|
||||||
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
|
export default TroubleshooterPage;
|
||||||
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 (
|
|
||||||
<div className="bg-default max-w-xl overflow-hidden rounded-md shadow">
|
|
||||||
<div className="px-4 py-5 sm:p-6">
|
|
||||||
{t("overview_of_day")}{" "}
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className="inline h-8 border-none bg-inherit p-0"
|
|
||||||
defaultValue={formattedSelectedDate}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) setSelectedDate(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<small className="text-muted block">{t("hover_over_bold_times_tip")}</small>
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
|
||||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
|
||||||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{(() => {
|
|
||||||
if (isLoading)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SkeletonText className="block h-16 w-full" />
|
|
||||||
<SkeletonText className="block h-16 w-full" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
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) => (
|
|
||||||
<div
|
|
||||||
key={dayjs(slot.start).format("HH:mm")}
|
|
||||||
className="bg-subtle overflow-hidden rounded-md"
|
|
||||||
data-testid="troubleshooter-busy-time">
|
|
||||||
<div className="text-emphasis px-4 py-5 sm:p-6">
|
|
||||||
{t("calendar_shows_busy_between")}{" "}
|
|
||||||
<span className="text-default font-medium" title={dayjs(slot.start).format("HH:mm")}>
|
|
||||||
{dayjs(slot.start).format("HH:mm")}
|
|
||||||
</span>{" "}
|
|
||||||
{t("and")}{" "}
|
|
||||||
<span className="text-default font-medium" title={dayjs(slot.end).format("HH:mm")}>
|
|
||||||
{dayjs(slot.end).format("HH:mm")}
|
|
||||||
</span>{" "}
|
|
||||||
{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 && <small>{` - (source: ${slot.source})`}</small>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
return (
|
|
||||||
<div className="bg-subtle overflow-hidden rounded-md">
|
|
||||||
<div className="text-emphasis px-4 py-5 sm:p-6">{t("calendar_no_busy_slots")}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
|
||||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
|
||||||
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Troubleshoot() {
|
|
||||||
const { data, isLoading } = trpc.viewer.me.useQuery();
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Shell heading={t("troubleshoot")} hideHeadingOnMobile subtitle={t("troubleshoot_description")}>
|
|
||||||
{!isLoading && data && <AvailabilityView user={data} />}
|
|
||||||
</Shell>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
|
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 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 (
|
||||||
|
<div className="bg-default max-w-xl overflow-hidden rounded-md shadow">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
{t("overview_of_day")}{" "}
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="inline h-8 border-none bg-inherit p-0"
|
||||||
|
defaultValue={formattedSelectedDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value) setSelectedDate(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<small className="text-muted block">{t("hover_over_bold_times_tip")}</small>
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||||
|
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||||
|
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(() => {
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SkeletonText className="block h-16 w-full" />
|
||||||
|
<SkeletonText className="block h-16 w-full" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
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) => (
|
||||||
|
<div
|
||||||
|
key={dayjs(slot.start).format("HH:mm")}
|
||||||
|
className="bg-subtle overflow-hidden rounded-md"
|
||||||
|
data-testid="troubleshooter-busy-time">
|
||||||
|
<div className="text-emphasis px-4 py-5 sm:p-6">
|
||||||
|
{t("calendar_shows_busy_between")}{" "}
|
||||||
|
<span className="text-default font-medium" title={dayjs(slot.start).format("HH:mm")}>
|
||||||
|
{dayjs(slot.start).format("HH:mm")}
|
||||||
|
</span>{" "}
|
||||||
|
{t("and")}{" "}
|
||||||
|
<span className="text-default font-medium" title={dayjs(slot.end).format("HH:mm")}>
|
||||||
|
{dayjs(slot.end).format("HH:mm")}
|
||||||
|
</span>{" "}
|
||||||
|
{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 && <small>{` - (source: ${slot.source})`}</small>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div className="bg-subtle overflow-hidden rounded-md">
|
||||||
|
<div className="text-emphasis px-4 py-5 sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||||
|
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||||
|
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Troubleshoot() {
|
||||||
|
const { data, isLoading } = trpc.viewer.me.useQuery();
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Shell heading={t("troubleshoot")} hideHeadingOnMobile subtitle={t("troubleshoot_description")}>
|
||||||
|
{!isLoading && data && <AvailabilityView user={data} />}
|
||||||
|
</Shell>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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}`;
|
||||||
|
}
|
|
@ -2094,6 +2094,11 @@
|
||||||
"overlay_my_calendar":"Overlay my calendar",
|
"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.",
|
"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.",
|
"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",
|
"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.",
|
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { cva } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { Tooltip } from "@calcom/ui";
|
||||||
|
|
||||||
import type { CalendarEvent } from "../../types/events";
|
import type { CalendarEvent } from "../../types/events";
|
||||||
|
|
||||||
|
@ -13,7 +15,7 @@ type EventProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventClasses = cva(
|
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 flex-col overflow-y-auto rounded-[6px] px-[6px] py-1 text-xs font-semibold leading-5 ",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
status: {
|
status: {
|
||||||
|
@ -62,23 +64,28 @@ export function Event({
|
||||||
const Component = onEventClick ? "button" : "div";
|
const Component = onEventClick ? "button" : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Tooltip content={event.title}>
|
||||||
onClick={() => onEventClick?.(event)} // Note this is not the button event. It is the calendar event.
|
<Component
|
||||||
className={eventClasses({
|
onClick={() => onEventClick?.(event)} // Note this is not the button event. It is the calendar event.
|
||||||
status: options?.status,
|
className={classNames(
|
||||||
disabled,
|
eventClasses({
|
||||||
selected,
|
status: options?.status,
|
||||||
borderColor,
|
disabled,
|
||||||
})}
|
selected,
|
||||||
style={styles}>
|
borderColor,
|
||||||
<div className="w-full overflow-hidden overflow-ellipsis whitespace-nowrap text-left leading-4">
|
}),
|
||||||
{event.title}
|
options?.className
|
||||||
</div>
|
)}
|
||||||
{eventDuration > 30 && (
|
style={styles}>
|
||||||
<p className="text-subtle text-left text-[10px] leading-none">
|
<div className="w-full overflow-hidden overflow-ellipsis whitespace-nowrap text-left leading-4">
|
||||||
{dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
|
{event.title}
|
||||||
</p>
|
</div>
|
||||||
)}
|
{eventDuration > 30 && (
|
||||||
</Component>
|
<p className="text-subtle text-left text-[10px] leading-none">
|
||||||
|
{dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Component>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,12 @@ import type dayjs from "@calcom/dayjs";
|
||||||
|
|
||||||
export const VeritcalLines = ({ days }: { days: dayjs.Dayjs[] }) => {
|
export const VeritcalLines = ({ days }: { days: dayjs.Dayjs[] }) => {
|
||||||
const isRTL = () => {
|
const isRTL = () => {
|
||||||
const userLocale = navigator.language;
|
let userLanguage = "en"; // Default to 'en' if navigator is not defined
|
||||||
const userLanguage = new Intl.Locale(userLocale).language;
|
|
||||||
|
if (typeof window !== "undefined" && typeof navigator !== "undefined") {
|
||||||
|
const userLocale = navigator.language;
|
||||||
|
userLanguage = new Intl.Locale(userLocale).language;
|
||||||
|
}
|
||||||
return ["ar", "he", "fa", "ur"].includes(userLanguage);
|
return ["ar", "he", "fa", "ur"].includes(userLanguage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,5 +10,6 @@ export interface CalendarEvent {
|
||||||
status?: BookingStatus;
|
status?: BookingStatus;
|
||||||
allDay?: boolean;
|
allDay?: boolean;
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import StickyBox from "react-sticky-box";
|
||||||
|
import type { TroubleshooterProps } from "troubleshooter/types";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
const extraDaysConfig = {
|
||||||
|
desktop: 7,
|
||||||
|
tablet: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
|
const isTablet = useMediaQuery("(max-width: 1024px)");
|
||||||
|
const StickyOnDesktop = isMobile ? "div" : StickyBox;
|
||||||
|
const extraDays = isTablet ? extraDaysConfig.tablet : extraDaysConfig.desktop;
|
||||||
|
|
||||||
|
useInitalizeTroubleshooterStore({
|
||||||
|
month: month,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"text-default flex min-h-full w-full flex-col items-center overflow-clip [--troublehooster-meta-width:350px] lg:[--troubleshooter-meta-width:430px]"
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100vw",
|
||||||
|
minHeight: "100vh",
|
||||||
|
height: "auto",
|
||||||
|
gridTemplateAreas: `
|
||||||
|
"meta header header"
|
||||||
|
"meta main main"
|
||||||
|
`,
|
||||||
|
gridTemplateColumns: "var(--troubleshooter-meta-width) 1fr",
|
||||||
|
gridTemplateRows: "70px auto",
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
"bg-default dark:bg-muted grid max-w-full items-start dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row"
|
||||||
|
)}>
|
||||||
|
<div className={classNames("bg-default dark:bg-muted sticky top-0 z-10 [grid-area:header]")}>
|
||||||
|
<TroubleshooterHeader extraDays={extraDays} isMobile={isMobile} />
|
||||||
|
</div>
|
||||||
|
<StickyOnDesktop key="meta" className={classNames("relative z-10 flex [grid-area:meta]")}>
|
||||||
|
<div className="max-w-screen flex w-full flex-col [grid-area:meta] md:w-[var(--troubleshooter-meta-width)]">
|
||||||
|
<TroubleshooterSidebar />
|
||||||
|
</div>
|
||||||
|
</StickyOnDesktop>
|
||||||
|
|
||||||
|
<div className="border-subtle sticky top-0 ml-[-1px] h-full [grid-area:main] md:border-l">
|
||||||
|
<LargeCalendar extraDays={extraDays} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Troubleshooter = ({ month }: TroubleshooterProps) => {
|
||||||
|
return <TroubleshooterComponent month={month} />;
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<TroubleshooterListItemContainer
|
||||||
|
title="Office Hours"
|
||||||
|
subtitle="Mon-Fri; 9:00 AM - 5:00 PM"
|
||||||
|
suffixSlot={
|
||||||
|
<div>
|
||||||
|
<Badge variant="green" withDot size="sm">
|
||||||
|
Connected
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-subtle text-sm font-medium leading-none">{t("date_overrides")}</p>
|
||||||
|
<Switch label="google@calendar.com" />
|
||||||
|
</div>
|
||||||
|
</TroubleshooterListItemContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AvailabiltySchedulesContainer() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
<p className="text-sm font-medium leading-none">{t("availabilty_schedules")}</p>
|
||||||
|
<AvailabiltyItem />
|
||||||
|
<Button color="secondary" className="justify-center gap-2">
|
||||||
|
{t("manage_availabilty_schedules")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
||||||
|
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||||
|
import { useSlotsForAvailableDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
|
|
||||||
|
import { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
|
||||||
|
import { useBookerStore } from "../store";
|
||||||
|
import { useEvent, useScheduleForEvent } from "../utils/event";
|
||||||
|
|
||||||
|
type AvailableTimeSlotsProps = {
|
||||||
|
extraDays?: number;
|
||||||
|
limitHeight?: boolean;
|
||||||
|
prefetchNextMonth: boolean;
|
||||||
|
monthCount: number | undefined;
|
||||||
|
seatsPerTimeSlot?: number | null;
|
||||||
|
showAvailableSeatsCount?: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders available time slots for a given date.
|
||||||
|
* It will extract the date from the booker store.
|
||||||
|
* Next to that you can also pass in the `extraDays` prop, this
|
||||||
|
* will also fetch the next `extraDays` days and show multiple days
|
||||||
|
* in columns next to each other.
|
||||||
|
*/
|
||||||
|
export const AvailableTimeSlots = ({
|
||||||
|
extraDays,
|
||||||
|
limitHeight,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
showAvailableSeatsCount,
|
||||||
|
prefetchNextMonth,
|
||||||
|
monthCount,
|
||||||
|
}: AvailableTimeSlotsProps) => {
|
||||||
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
|
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||||
|
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||||
|
const setSeatedEventData = useBookerStore((state) => state.setSeatedEventData);
|
||||||
|
const isEmbed = useIsEmbed();
|
||||||
|
const event = useEvent();
|
||||||
|
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
||||||
|
const [layout] = useBookerStore((state) => [state.layout]);
|
||||||
|
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const onTimeSelect = (
|
||||||
|
time: string,
|
||||||
|
attendees: number,
|
||||||
|
seatsPerTimeSlot?: number | null,
|
||||||
|
bookingUid?: string
|
||||||
|
) => {
|
||||||
|
setSelectedTimeslot(time);
|
||||||
|
|
||||||
|
if (seatsPerTimeSlot) {
|
||||||
|
setSeatedEventData({
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
attendees,
|
||||||
|
bookingUid,
|
||||||
|
showAvailableSeatsCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (seatsPerTimeSlot && seatsPerTimeSlot - attendees > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.data) return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedule = useScheduleForEvent({
|
||||||
|
prefetchNextMonth,
|
||||||
|
monthCount,
|
||||||
|
});
|
||||||
|
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
|
||||||
|
const nonEmptyScheduleDaysFromSelectedDate = nonEmptyScheduleDays.filter(
|
||||||
|
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Creates an array of dates to fetch slots for.
|
||||||
|
// If `extraDays` is passed in, we will extend the array with the next `extraDays` days.
|
||||||
|
const dates = !extraDays
|
||||||
|
? [date]
|
||||||
|
: nonEmptyScheduleDaysFromSelectedDate.length > 0
|
||||||
|
? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const slotsPerDay = useSlotsForAvailableDates(dates, schedule?.data?.slots);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmbed) return;
|
||||||
|
if (containerRef.current && !schedule.isLoading && isMobile) {
|
||||||
|
containerRef.current.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}
|
||||||
|
}, [containerRef, schedule.isLoading, isEmbed, isMobile]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex">
|
||||||
|
{schedule.isLoading ? (
|
||||||
|
<div className="mb-3 h-8" />
|
||||||
|
) : (
|
||||||
|
slotsPerDay.length > 0 &&
|
||||||
|
slotsPerDay.map((slots) => (
|
||||||
|
<AvailableTimesHeader
|
||||||
|
key={slots.date}
|
||||||
|
date={dayjs(slots.date)}
|
||||||
|
showTimeFormatToggle={!isColumnView}
|
||||||
|
availableMonth={
|
||||||
|
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
|
||||||
|
? dayjs(slots.date).format("MMM")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={classNames(
|
||||||
|
limitHeight && "scroll-bar flex-grow overflow-auto md:h-[400px]",
|
||||||
|
!limitHeight && "flex h-full w-full flex-row gap-4"
|
||||||
|
)}>
|
||||||
|
{schedule.isLoading
|
||||||
|
? // Shows exact amount of days as skeleton.
|
||||||
|
Array.from({ length: 1 + (extraDays ?? 0) }).map((_, i) => <AvailableTimesSkeleton key={i} />)
|
||||||
|
: slotsPerDay.length > 0 &&
|
||||||
|
slotsPerDay.map((slots) => (
|
||||||
|
<AvailableTimes
|
||||||
|
className="scroll-bar w-full overflow-y-auto overflow-x-hidden"
|
||||||
|
key={slots.date}
|
||||||
|
showTimeFormatToggle={!isColumnView}
|
||||||
|
onTimeSelect={onTimeSelect}
|
||||||
|
slots={slots.slots}
|
||||||
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
|
showAvailableSeatsCount={showAvailableSeatsCount}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,117 @@
|
||||||
|
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 (
|
||||||
|
<TroubleshooterListItemContainer
|
||||||
|
title={props.title}
|
||||||
|
subtitle={props.subtitle}
|
||||||
|
prefixSlot={
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="h-4 w-4 self-center rounded-[4px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: props.colorDot,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
suffixSlot={
|
||||||
|
<div>
|
||||||
|
<Badge variant={badgeStatus} withDot size="sm">
|
||||||
|
{badgeText}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div className="[&>*]:text-emphasis flex flex-col gap-3">
|
||||||
|
{props.calendars?.map((calendar) => {
|
||||||
|
return <Switch key={calendar.name} checked={calendar.active} label={calendar.name} disabled />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</TroubleshooterListItemContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyCalendarToggleItem() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TroubleshooterListItemContainer
|
||||||
|
title="Please install a calendar"
|
||||||
|
prefixSlot={
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
suffixSlot={
|
||||||
|
<div>
|
||||||
|
<Badge variant="orange" withDot size="sm">
|
||||||
|
Not found
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Button color="secondary" className="justify-center gap-2">
|
||||||
|
{t("install_calendar")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TroubleshooterListItemContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CalendarToggleContainer() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery();
|
||||||
|
|
||||||
|
const hasConnectedCalendars = data && data?.connectedCalendars.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
<p className="text-sm font-medium leading-none">{t("calendars_were_checking_for_conflicts")}</p>
|
||||||
|
{hasConnectedCalendars && !isLoading ? (
|
||||||
|
<>
|
||||||
|
{data.connectedCalendars.map((calendar, idx) => {
|
||||||
|
const foundPrimary = calendar.calendars?.find((item) => item.primary);
|
||||||
|
return (
|
||||||
|
<CalendarToggleItem
|
||||||
|
key={calendar.credentialId}
|
||||||
|
title={calendar.integration.name}
|
||||||
|
colorDot={SELECTION_COLORS[idx] || "#000000"}
|
||||||
|
subtitle={foundPrimary?.name ?? "Nameless Calendar"}
|
||||||
|
status={calendar.error ? "not_found" : "connected"}
|
||||||
|
calendars={calendar.calendars?.map((item) => {
|
||||||
|
return {
|
||||||
|
active: item.isSelected,
|
||||||
|
name: item.name,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button color="secondary" className="justify-center gap-2">
|
||||||
|
{t("manage_calendars")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EmptyCalendarToggleItem />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Badge } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
|
||||||
|
|
||||||
|
function ConnectedAppsItem() {
|
||||||
|
return (
|
||||||
|
<TroubleshooterListItemHeader
|
||||||
|
title="Google Cal"
|
||||||
|
subtitle="google@calendar.com"
|
||||||
|
prefixSlot={
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
suffixSlot={
|
||||||
|
<div>
|
||||||
|
<Badge variant="green" withDot size="sm">
|
||||||
|
Connected
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectedAppsContainer() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
<p className="text-sm font-medium leading-none">{t("other_apps")}</p>
|
||||||
|
<div className="[&>*:first-child]:rounded-t-md [&>*:last-child]:rounded-b-md [&>*:last-child]:border-b">
|
||||||
|
<ConnectedAppsItem />
|
||||||
|
<ConnectedAppsItem />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<Label>Availability Schedule</Label>
|
||||||
|
<TroubleshooterListItemHeader
|
||||||
|
className="group rounded-md border-b"
|
||||||
|
prefixSlot={<div className="w-4 rounded-[4px] bg-black" />}
|
||||||
|
title={schedule?.name ?? "Loading"}
|
||||||
|
suffixSlot={
|
||||||
|
schedule && (
|
||||||
|
<Link href={`/availability/${schedule.id}`} className="inline-flex">
|
||||||
|
<Badge color="orange" size="sm" className="hidden hover:cursor-pointer group-hover:inline-flex">
|
||||||
|
{t("edit")}
|
||||||
|
</Badge>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
|
import { SelectField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { useTroubleshooterStore } from "../store";
|
||||||
|
|
||||||
|
export function EventTypeSelect() {
|
||||||
|
const { data: eventTypes } = 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]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectField
|
||||||
|
label="Event Type"
|
||||||
|
options={options}
|
||||||
|
value={options.find((option) => option.value === selectedEventType?.slug) || options[0]}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option) return;
|
||||||
|
setSelectedEventType({
|
||||||
|
slug: option.value,
|
||||||
|
id: option.id,
|
||||||
|
duration: option.duration,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
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 { data: session } = useSession();
|
||||||
|
const startDate = selectedDate ? dayjs(selectedDate) : dayjs();
|
||||||
|
|
||||||
|
const { data: busyEvents, isLoading } = 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 [];
|
||||||
|
|
||||||
|
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: "#F97417",
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (busyEvents.dateOverrides) {
|
||||||
|
for (const date in busyEvents.dateOverrides) {
|
||||||
|
const dateOverride = busyEvents.dateOverrides[date];
|
||||||
|
calendarEvents.push({
|
||||||
|
id: calendarEvents.length,
|
||||||
|
title: "Date Override",
|
||||||
|
start: new Date(dateOverride.start),
|
||||||
|
end: new Date(dateOverride.end),
|
||||||
|
options: {
|
||||||
|
borderColor: "black",
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendarEvents;
|
||||||
|
}, [busyEvents]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full [--calendar-dates-sticky-offset:66px]">
|
||||||
|
<Calendar
|
||||||
|
startHour={0}
|
||||||
|
endHour={23}
|
||||||
|
events={events}
|
||||||
|
availableTimeslots={availableSlots}
|
||||||
|
startDate={startDate.toDate()}
|
||||||
|
endDate={endDate}
|
||||||
|
gridCellsPerHour={60 / 15}
|
||||||
|
hoverEventDuration={30}
|
||||||
|
hideHeader
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<h3 className="min-w-[150px] text-base font-semibold leading-4">
|
||||||
|
{formattedMonth.format(selectedDate.toDate())} {selectedDate.format("D")}
|
||||||
|
{!isSameYear() && <span className="text-subtle">, {selectedDate.format("YYYY")} </span>}-{" "}
|
||||||
|
{!isSameMonth() && formattedMonth.format(endDate.toDate())} {endDate.format("D")},{" "}
|
||||||
|
<span className="text-subtle">
|
||||||
|
{isSameYear() ? selectedDate.format("YYYY") : endDate.format("YYYY")}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-default relative z-10 flex border-b px-5 py-4 ltr:border-l rtl:border-r">
|
||||||
|
<div className="flex items-center gap-5 rtl:flex-grow">
|
||||||
|
<FormattedSelectedDateRange />
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
className="group rtl:ml-1 rtl:rotate-180"
|
||||||
|
variant="icon"
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={ChevronLeft}
|
||||||
|
aria-label="Previous Day"
|
||||||
|
onClick={() => addToSelectedDate(-extraDays)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="group rtl:mr-1 rtl:rotate-180"
|
||||||
|
variant="icon"
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={ChevronRight}
|
||||||
|
aria-label="Next Day"
|
||||||
|
onClick={() => addToSelectedDate(extraDays)}
|
||||||
|
/>
|
||||||
|
{selectedDateMin3DaysDifference && (
|
||||||
|
<Button
|
||||||
|
className="capitalize ltr:ml-2 rtl:mr-2"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => setSelectedDate(today.format("YYYY-MM-DD"))}>
|
||||||
|
{t("today")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<div className={classNames("border-subtle flex max-w-full gap-3 border border-b-0 px-4 py-2", className)}>
|
||||||
|
{prefixSlot}
|
||||||
|
<div className="flex h-full max-w-full flex-1 flex-col flex-nowrap truncate text-sm leading-4">
|
||||||
|
<p className="font-medium">{title}</p>
|
||||||
|
{subtitle && <p className="font-normal">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
{suffixSlot}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TroubleshooterListItemContainer({
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: PropsWithChildren<TroubleshooterListItemContainerProps>) {
|
||||||
|
return (
|
||||||
|
<div className="[&>*:first-child]:rounded-t-md ">
|
||||||
|
<TroubleshooterListItemHeader {...rest} />
|
||||||
|
<div className="border-subtle flex flex-col space-y-3 rounded-b-md border p-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="hover:bg-subtle group-hover:text-default text-emphasis group flex h-6 max-h-6 w-full flex-row items-center rounded-md px-3 py-2">
|
||||||
|
<ArrowLeft className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
|
||||||
|
<Skeleton
|
||||||
|
title={name}
|
||||||
|
as="p"
|
||||||
|
className="max-w-36 min-h-4 truncate text-xl font-semibold"
|
||||||
|
loadingClassName="ms-3">
|
||||||
|
{name}
|
||||||
|
</Skeleton>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TroubleshooterSidebar = () => {
|
||||||
|
const { i18n, t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-10 flex w-full flex-col gap-6 py-6 pr-6">
|
||||||
|
<BackButtonInSidebar name={t("troubleshooter")} />
|
||||||
|
<EventTypeSelect />
|
||||||
|
<EventScheduleItem />
|
||||||
|
<CalendarToggleContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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<typeof Shell>) {
|
||||||
|
return (
|
||||||
|
<Shell withoutSeo={true} flexChildrenContainer hideHeadingOnMobile {...rest} SidebarContainer={<></>}>
|
||||||
|
<div className="flex flex-1 [&>*]:flex-1">
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLayout = (page: React.ReactElement) => <TroubleshooterLayout>{page}</TroubleshooterLayout>;
|
|
@ -0,0 +1,101 @@
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<TroubleshooterStore>((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");
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useInitalizeTroubleshooterStore = ({ month }: StoreInitializeType) => {
|
||||||
|
const initializeStore = useTroubleshooterStore((state) => state.initialize);
|
||||||
|
useEffect(() => {
|
||||||
|
initializeStore({
|
||||||
|
month,
|
||||||
|
});
|
||||||
|
}, [initializeStore, month]);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { ZCreateInputSchema } from "./create.schema";
|
||||||
import { ZDeleteInputSchema } from "./delete.schema";
|
import { ZDeleteInputSchema } from "./delete.schema";
|
||||||
import { ZScheduleDuplicateSchema } from "./duplicate.schema";
|
import { ZScheduleDuplicateSchema } from "./duplicate.schema";
|
||||||
import { ZGetInputSchema } from "./get.schema";
|
import { ZGetInputSchema } from "./get.schema";
|
||||||
|
import { ZGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.schema";
|
||||||
import { ZGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
|
import { ZGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
|
||||||
import { ZUpdateInputSchema } from "./update.schema";
|
import { ZUpdateInputSchema } from "./update.schema";
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ type ScheduleRouterHandlerCache = {
|
||||||
update?: typeof import("./update.handler").updateHandler;
|
update?: typeof import("./update.handler").updateHandler;
|
||||||
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
|
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
|
||||||
getScheduleByUserId?: typeof import("./getScheduleByUserId.handler").getScheduleByUserIdHandler;
|
getScheduleByUserId?: typeof import("./getScheduleByUserId.handler").getScheduleByUserIdHandler;
|
||||||
|
getScheduleByEventSlug?: typeof import("./getScheduleByEventTypeSlug.handler").getScheduleByEventSlugHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UNSTABLE_HANDLER_CACHE: ScheduleRouterHandlerCache = {};
|
const UNSTABLE_HANDLER_CACHE: ScheduleRouterHandlerCache = {};
|
||||||
|
@ -118,4 +120,21 @@ export const scheduleRouter = router({
|
||||||
input,
|
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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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<TrpcSessionUser>;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZGetByEventSlugInputSchema = z.object({
|
||||||
|
eventSlug: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TGetByEventSlugInputSchema = z.infer<typeof ZGetByEventSlugInputSchema>;
|
Loading…
Reference in New Issue