feat: Availability Slider enabled for teams (#10902)
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
parent
e6e6f09547
commit
e020a2c9a4
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Reference in New Issue
Block a user