Add calendar source to busy blocks (#3074)
* Add calendar source to busy blocks * Update troubleshoot.tsx * Type fixes * Adds sources to availablity # Conflicts: # apps/web/pages/availability/troubleshoot.tsx * Update troubleshoot v1 and v2 with source element * Type fixes Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Alan <alannnc@gmail.com>pull/4261/head^2
parent
7da272f67a
commit
34408c5593
|
@ -2,6 +2,7 @@
|
|||
* @deprecated modifications to this file should be v2 only
|
||||
* Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead
|
||||
*/
|
||||
import type { IBusySlot } from "pages/v2/availability/troubleshoot";
|
||||
import { useState } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
@ -30,6 +31,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||
username: user.username!,
|
||||
dateFrom: selectedDate.startOf("day").utc().format(),
|
||||
dateTo: selectedDate.endOf("day").utc().format(),
|
||||
withSource: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
|
@ -59,23 +61,28 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||
{isLoading ? (
|
||||
<Loader />
|
||||
) : data && data.busy.length > 0 ? (
|
||||
data.busy.map((slot) => (
|
||||
<div key={slot.start} className="overflow-hidden rounded-sm bg-neutral-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">
|
||||
{t("calendar_shows_busy_between")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.start}>
|
||||
{dayjs(slot.start).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("and")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.end}>
|
||||
{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})`}
|
||||
data.busy
|
||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||
.map((slot: IBusySlot) => (
|
||||
<div
|
||||
key={`${slot.start}-${slot.title ?? "untitled"}`}
|
||||
className="overflow-hidden rounded-sm bg-neutral-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">
|
||||
{t("calendar_shows_busy_between")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.start}>
|
||||
{dayjs(slot.start).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("and")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.end}>
|
||||
{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>
|
||||
</div>
|
||||
))
|
||||
))
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-sm bg-neutral-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||
import Shell from "@calcom/ui/Shell";
|
||||
|
@ -11,42 +11,31 @@ import Loader from "@components/Loader";
|
|||
|
||||
type User = inferQueryOutput<"viewer.me">;
|
||||
|
||||
export interface IBusySlot {
|
||||
start: string;
|
||||
end: string;
|
||||
title?: string;
|
||||
source?: string | null;
|
||||
}
|
||||
|
||||
const AvailabilityView = ({ user }: { user: User }) => {
|
||||
const { t } = useLocale();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [availability, setAvailability] = useState<{ end: string; start: string; title?: string }[]>([]);
|
||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailability = (date: Dayjs) => {
|
||||
const dateFrom = date.startOf("day").utc().format();
|
||||
const dateTo = date.endOf("day").utc().format();
|
||||
setLoading(true);
|
||||
|
||||
fetch(`/api/availability/${user.username}?dateFrom=${dateFrom}&dateTo=${dateTo}`)
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then((availableIntervals) => {
|
||||
setAvailability(availableIntervals.busy);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
fetchAvailability(selectedDate);
|
||||
}, [user.username, selectedDate]);
|
||||
const { data, isLoading } = trpc.useQuery(
|
||||
[
|
||||
"viewer.availability.user",
|
||||
{
|
||||
username: user.username!,
|
||||
dateFrom: selectedDate.startOf("day").utc().format(),
|
||||
dateTo: selectedDate.endOf("day").utc().format(),
|
||||
withSource: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
enabled: !!user.username,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
|
||||
|
@ -67,26 +56,29 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : availability.length > 0 ? (
|
||||
availability.map((slot) => (
|
||||
<div key={slot.start} className="overflow-hidden rounded-md bg-neutral-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">
|
||||
{t("calendar_shows_busy_between")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.start}>
|
||||
{dayjs(slot.start).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("and")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.end}>
|
||||
{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})`}
|
||||
) : data && data.busy.length > 0 ? (
|
||||
data.busy
|
||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||
.map((slot: IBusySlot) => (
|
||||
<div key={slot.start} className="overflow-hidden rounded-md bg-neutral-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">
|
||||
{t("calendar_shows_busy_between")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.start}>
|
||||
{dayjs(slot.start).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("and")}{" "}
|
||||
<span className="font-medium text-neutral-800" title={slot.end}>
|
||||
{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>
|
||||
</div>
|
||||
))
|
||||
))
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-md bg-neutral-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||
|
@ -115,3 +107,11 @@ export default function Troubleshoot() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ const getCachedResults = async (
|
|||
/** Filter out nulls */
|
||||
if (!c) return [];
|
||||
/** We rely on the index so we can match credentials with calendars */
|
||||
const { id, type } = calendarCredentials[i];
|
||||
const { id, type, appId } = calendarCredentials[i];
|
||||
/** We just pass the calendars that matched the credential type,
|
||||
* TODO: Migrate credential type or appId
|
||||
*/
|
||||
|
@ -113,7 +113,10 @@ const getCachedResults = async (
|
|||
}
|
||||
log.debug(`Cache MISS: Calendar Availability for key ${cacheKey}`);
|
||||
/** If we don't then we actually fetch external calendars (which can be very slow) */
|
||||
const availability = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
|
||||
const availability = (await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars)).map((a) => ({
|
||||
...a,
|
||||
source: `${appId}`,
|
||||
}));
|
||||
/** We save the availability to a few seconds so recurrent calls are nearly instant */
|
||||
|
||||
cache.put(cacheHashedKey, availability, CACHING_TIME);
|
||||
|
@ -142,7 +145,6 @@ export const getBusyCalendarTimes = async (
|
|||
} catch (error) {
|
||||
log.warn(error);
|
||||
}
|
||||
|
||||
return results.reduce((acc, availability) => acc.concat(availability), []);
|
||||
};
|
||||
|
||||
|
|
|
@ -42,7 +42,12 @@ export async function getBusyTimes(params: {
|
|||
},
|
||||
})
|
||||
.then((bookings) =>
|
||||
bookings.map(({ startTime, endTime, title }) => ({ end: endTime, start: startTime, title }))
|
||||
bookings.map(({ startTime, endTime, title, id }) => ({
|
||||
end: endTime,
|
||||
start: startTime,
|
||||
title,
|
||||
source: `eventType-${eventTypeId}-booking-${id}`,
|
||||
}))
|
||||
);
|
||||
logger.silly(`Busy Time from Cal Bookings ${JSON.stringify(busyTimes)}`);
|
||||
const endPrismaBookingGet = performance.now();
|
||||
|
|
|
@ -19,6 +19,7 @@ const availabilitySchema = z
|
|||
username: z.string().optional(),
|
||||
userId: z.number().optional(),
|
||||
afterEventBuffer: z.number().optional(),
|
||||
withSource: z.boolean().optional(),
|
||||
})
|
||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||
|
||||
|
@ -80,6 +81,7 @@ export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
|
|||
/** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */
|
||||
export async function getUserAvailability(
|
||||
query: {
|
||||
withSource?: boolean;
|
||||
username?: string;
|
||||
userId?: number;
|
||||
dateFrom: string;
|
||||
|
@ -128,11 +130,13 @@ export async function getUserAvailability(
|
|||
});
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
...a,
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toISOString(),
|
||||
end: dayjs(a.end)
|
||||
.add(currentUser.bufferTime + (afterEventBuffer || 0), "minute")
|
||||
.toISOString(),
|
||||
title: a.title,
|
||||
source: query.withSource ? a.source : undefined,
|
||||
}));
|
||||
|
||||
const schedule = eventType?.schedule
|
||||
|
|
|
@ -82,15 +82,10 @@ export const availabilityRouter = createProtectedRouter()
|
|||
dateFrom: z.string(),
|
||||
dateTo: z.string(),
|
||||
eventTypeId: stringOrNumber.optional(),
|
||||
withSource: z.boolean().optional(),
|
||||
}),
|
||||
async resolve({ input }) {
|
||||
const { username, eventTypeId, dateTo, dateFrom } = input;
|
||||
return getUserAvailability({
|
||||
username,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
});
|
||||
return getUserAvailability(input);
|
||||
},
|
||||
})
|
||||
.mutation("schedule.create", {
|
||||
|
|
|
@ -25,10 +25,15 @@ export type Person = {
|
|||
id?: string;
|
||||
};
|
||||
|
||||
export type EventBusyDate = Record<"start" | "end", Date | string>;
|
||||
export type EventBusyDate = {
|
||||
start: Date | string;
|
||||
end: Date | string;
|
||||
source?: string | null;
|
||||
};
|
||||
|
||||
export type EventBusyDetails = EventBusyDate & {
|
||||
title?: string;
|
||||
source?: string | null;
|
||||
};
|
||||
|
||||
export type CalendarServiceType = typeof Calendar;
|
||||
|
|
Loading…
Reference in New Issue