feat: Availability Slider enabled for teams (#10902)

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
sean-brydon 2023-08-23 22:23:24 +01:00 committed by GitHub
parent e6e6f09547
commit e020a2c9a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 194 additions and 66 deletions

View File

@ -2,7 +2,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useCallback } from "react";
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
import { getLayout } from "@calcom/features/MainLayout";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { ShellMain } from "@calcom/features/shell/Shell";
@ -135,7 +134,6 @@ export default function AvailabilityPage() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const orgBranding = useOrgBranding();
// Get a new searchParams string by merging the current
// searchParams with a provided key/value pair
@ -156,23 +154,22 @@ export default function AvailabilityPage() {
subtitle={t("configure_availability")}
CTA={
<div className="flex gap-2">
{orgBranding && (
<ToggleGroup
className="hidden md:block"
defaultValue={searchParams?.get("type") ?? "mine"}
onValueChange={(value) => {
router.push(`${pathname}?${createQueryString("type", value)}`);
}}
options={[
{ value: "mine", label: t("my_availability") },
{ value: "team", label: t("team_availability") },
]}
/>
)}
<ToggleGroup
className="hidden md:block"
defaultValue={searchParams?.get("type") ?? "mine"}
onValueChange={(value) => {
if (!value) return;
router.push(`${pathname}?${createQueryString("type", value)}`);
}}
options={[
{ value: "mine", label: t("my_availability") },
{ value: "team", label: t("team_availability") },
]}
/>
<NewScheduleButton />
</div>
}>
{searchParams?.get("type") === "team" && orgBranding ? (
{searchParams?.get("type") === "team" ? (
<AvailabilitySliderTable />
) : (
<WithQuery

View File

@ -3,11 +3,14 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useMemo, useRef, useCallback, useEffect, useState } from "react";
import dayjs from "@calcom/dayjs";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import type { DateRange } from "@calcom/lib/date-ranges";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
import { UpgradeTip } from "../../tips/UpgradeTip";
import { TBContext, createTimezoneBuddyStore } from "../store";
import { TimeDial } from "./TimeDial";
@ -20,6 +23,32 @@ export interface SliderUser {
dateRanges: DateRange[];
}
function UpgradeTeamTip() {
const { t } = useLocale();
return (
<UpgradeTip
title={t("calcom_is_better_with_team", { appName: APP_NAME }) as string}
description="add_your_team_members"
background="/tips/teams"
features={[]}
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
<></>
</UpgradeTip>
);
}
export function AvailabilitySliderTable() {
const tableContainerRef = useRef<HTMLDivElement>(null);
const [browsingDate, setBrowsingDate] = useState(dayjs());
@ -143,6 +172,9 @@ export function AvailabilitySliderTable() {
fetchMoreOnBottomReached(tableContainerRef.current);
}, [fetchMoreOnBottomReached]);
// This means they are not apart of any teams so we show the upgrade tip
if (!flatData.length) return <UpgradeTeamTip />;
return (
<TBContext.Provider
value={createTimezoneBuddyStore({

View File

@ -1,3 +1,6 @@
import { Prisma } from "@prisma/client";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import type { DateRange } from "@calcom/lib/date-ranges";
import { buildDateRanges } from "@calcom/lib/date-ranges";
@ -15,25 +18,32 @@ type GetOptions = {
input: TListTeamAvailaiblityScheme;
};
export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) => {
const organizationId = ctx.user.organizationId;
async function getTeamMembers({
teamId,
teamIds,
cursor,
limit,
}: {
teamId?: number;
teamIds?: number[];
cursor: number | null | undefined;
limit: number;
}) {
let whereQuery: Prisma.MembershipWhereInput = {
teamId,
};
if (!organizationId) {
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization." });
if (teamIds) {
whereQuery = {
teamId: {
in: teamIds,
},
};
}
const { cursor, limit } = input;
const getTotalMembers = await prisma.membership.count({
return await prisma.membership.findMany({
where: {
teamId: organizationId,
},
});
// I couldnt get this query to work direct on membership table
const teamMembers = await prisma.membership.findMany({
where: {
teamId: organizationId,
...whereQuery,
accepted: true,
},
select: {
@ -54,7 +64,128 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
orderBy: {
id: "asc",
},
distinct: ["userId"],
});
}
type Member = Awaited<ReturnType<typeof getTeamMembers>>[number];
async function buildMember(member: Member, dateFrom: Dayjs, dateTo: Dayjs) {
if (!member.user.defaultScheduleId) {
return {
id: member.user.id,
username: member.user.username,
email: member.user.email,
timeZone: member.user.timeZone,
role: member.role,
dateRanges: [] as DateRange[],
};
}
const schedule = await prisma.schedule.findUnique({
where: { id: member.user.defaultScheduleId },
select: { availability: true, timeZone: true },
});
const timeZone = schedule?.timeZone || member.user.timeZone;
const dateRanges = buildDateRanges({
dateFrom,
dateTo,
timeZone,
availability: schedule?.availability ?? [],
});
return {
id: member.user.id,
username: member.user.username,
email: member.user.email,
timeZone,
role: member.role,
dateRanges,
};
}
async function getInfoForAllTeams({ ctx, input }: GetOptions) {
const { cursor, limit } = input;
// Get all teamIds for the user
const teamIds = await prisma.membership
.findMany({
where: {
userId: ctx.user.id,
},
select: {
id: true,
},
})
.then((memberships) => memberships.map((membership) => membership.id));
if (!teamIds.length) {
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization or team." });
}
const getTotalMembers = await prisma.$queryRaw<{
count: number;
}>(Prisma.sql`
SELECT
COUNT(DISTINCT "userId") as "count"
FROM "Membership"
WHERE "teamId" IN (${Prisma.join(teamIds)})
`);
const teamMembers = await getTeamMembers({
teamIds,
cursor,
limit,
});
return {
teamMembers,
totalTeamMembers: getTotalMembers.count,
};
}
export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) => {
const { cursor, limit } = input;
const teamId = input.teamId || ctx.user.organizationId;
let teamMembers: Member[] = [];
let totalTeamMembers = 0;
if (!teamId) {
// Get all users TODO:
const teamAllInfo = await getInfoForAllTeams({ ctx, input });
teamMembers = teamAllInfo.teamMembers;
totalTeamMembers = teamAllInfo.totalTeamMembers;
} else {
const isMember = await prisma.membership.findFirst({
where: {
teamId,
userId: ctx.user.id,
},
});
if (!isMember) {
teamMembers = [];
totalTeamMembers = 0;
} else {
const { cursor, limit } = input;
totalTeamMembers = await prisma.membership.count({
where: {
teamId: teamId,
},
});
// I couldnt get this query to work direct on membership table
teamMembers = await getTeamMembers({
teamId,
cursor,
limit,
});
}
}
let nextCursor: typeof cursor | undefined = undefined;
if (teamMembers && teamMembers.length > limit) {
@ -65,40 +196,7 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
const dateFrom = dayjs(input.startDate).tz(input.loggedInUsersTz).subtract(1, "day");
const dateTo = dayjs(input.endDate).tz(input.loggedInUsersTz).add(1, "day");
const buildMembers = teamMembers?.map(async (member) => {
if (!member.user.defaultScheduleId) {
return {
id: member.user.id,
username: member.user.username,
email: member.user.email,
timeZone: member.user.timeZone,
role: member.role,
dateRanges: [] as DateRange[],
};
}
const schedule = await prisma.schedule.findUnique({
where: { id: member.user.defaultScheduleId },
select: { availability: true, timeZone: true },
});
const timeZone = schedule?.timeZone || member.user.timeZone;
const dateRanges = buildDateRanges({
dateFrom,
dateTo,
timeZone,
availability: schedule?.availability ?? [],
});
return {
id: member.user.id,
username: member.user.username,
email: member.user.email,
timeZone,
role: member.role,
dateRanges,
};
});
const buildMembers = teamMembers?.map((member) => buildMember(member, dateFrom, dateTo));
const members = await Promise.all(buildMembers);
@ -106,7 +204,7 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
rows: members || [],
nextCursor,
meta: {
totalRowCount: getTotalMembers || 0,
totalRowCount: totalTeamMembers,
},
};
};

View File

@ -6,6 +6,7 @@ export const ZListTeamAvailaiblityScheme = z.object({
startDate: z.string(),
endDate: z.string(),
loggedInUsersTz: z.string(),
teamId: z.number().optional(),
});
export type TListTeamAvailaiblityScheme = z.infer<typeof ZListTeamAvailaiblityScheme>;