fix: update styles of charts on /insights (#22557)
* fix: update styles of charts on /insights * clean up * clean up * share the same styles with ChartCardItem * margin for event trends * fix type error
This commit is contained in:
@@ -71,22 +71,22 @@ function InsightsPageContent() {
|
||||
|
||||
<BookingStatusLineChart />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<PopularEventsTable />
|
||||
<AverageEventDurationChart />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||
<MostBookedTeamMembersTable />
|
||||
<LeastBookedTeamMembersTable />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<MostCancelledBookingsTables />
|
||||
</div>
|
||||
<RecentFeedbackTable />
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<HighestNoShowHostTable />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||
<HighestRatedMembersTable />
|
||||
<LowestRatedMembersTable />
|
||||
<div className="sm:col-span-2">
|
||||
<RecentFeedbackTable />
|
||||
</div>
|
||||
</div>
|
||||
<small className="text-default block text-center">
|
||||
{t("looking_for_more_insights")}{" "}
|
||||
|
||||
@@ -2167,12 +2167,14 @@
|
||||
"popular_events": "Popular Events",
|
||||
"no_event_types_found": "No event types found",
|
||||
"average_event_duration": "Average Event Duration",
|
||||
"most_booked_members": "Most Booked Members",
|
||||
"least_booked_members": "Least Booked Members",
|
||||
"most_booked_members": "Most Booked",
|
||||
"least_booked_members": "Least Booked",
|
||||
"events_created": "Events Created",
|
||||
"events_completed": "Events Completed",
|
||||
"events_cancelled": "Events Cancelled",
|
||||
"events_rescheduled": "Events Rescheduled",
|
||||
"events": "Events",
|
||||
"performance": "Performance",
|
||||
"from_last_period": "from last period",
|
||||
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
||||
"redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
|
||||
@@ -2720,10 +2722,12 @@
|
||||
"event_ratings": "Average Ratings",
|
||||
"event_no_show": "No-Show (Host)",
|
||||
"event_no_show_guest": "No-Show (Guest)",
|
||||
"recent_ratings": "Recent ratings",
|
||||
"recent_ratings": "Recent Ratings",
|
||||
"no_ratings": "No ratings submitted",
|
||||
"no_ratings_description": "Add a workflow with 'Rating' to collect ratings after meetings",
|
||||
"most_no_show_host": "Most No Show Members",
|
||||
"most_no_show_host": "Most No-Show",
|
||||
"highest_rated": "Highest Rated",
|
||||
"lowest_rated": "Lowest Rated",
|
||||
"highest_rated_members": "Members with highest rated meetings",
|
||||
"lowest_rated_members": "Members with lowest rated meetings",
|
||||
"csat_score": "CSAT Score",
|
||||
@@ -3143,7 +3147,7 @@
|
||||
"display": "Display",
|
||||
"could_not_find_slug_to_publish_org": "Could not find slug to publish the organization",
|
||||
"picklist": "Picklist",
|
||||
"most_cancelled_bookings": "Most Cancelled Bookings",
|
||||
"most_cancelled_bookings": "Most Cancelled",
|
||||
"interface_language": "Interface Language",
|
||||
"visitors_browser_language": "Visitor's browser language",
|
||||
"name_or_email": "Name or Email",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { valueFormatter } from "../lib/valueFormatter";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LineChart } from "./LineChart";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
|
||||
@@ -36,8 +34,7 @@ export const AverageEventDurationChart = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
const isNoData = data.every((item) => item["Average"] === 0);
|
||||
return (
|
||||
<CardInsights>
|
||||
<Title className="text-emphasis">{t("average_event_duration")}</Title>
|
||||
<ChartCard title={t("average_event_duration")}>
|
||||
{isNoData && (
|
||||
<div className="text-default flex h-60 text-center">
|
||||
<p className="m-auto text-sm font-light">{t("insights_no_data_found_for_filter")}</p>
|
||||
@@ -53,6 +50,6 @@ export const AverageEventDurationChart = () => {
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
)}
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Grid } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { SkeletonText, SkeletonContainer } from "@calcom/ui/components/skeleton";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { KPICard } from "./KPICard";
|
||||
|
||||
export const BookingKPICards = () => {
|
||||
@@ -30,17 +29,9 @@ export const BookingKPICards = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const categories: {
|
||||
const eventCategories: {
|
||||
title: string;
|
||||
index:
|
||||
| "created"
|
||||
| "completed"
|
||||
| "rescheduled"
|
||||
| "cancelled"
|
||||
| "no_show"
|
||||
| "rating"
|
||||
| "csat"
|
||||
| "no_show_guest";
|
||||
index: "created" | "completed" | "rescheduled" | "cancelled";
|
||||
}[] = [
|
||||
{
|
||||
title: t("events_created"),
|
||||
@@ -58,6 +49,12 @@ export const BookingKPICards = () => {
|
||||
title: t("events_cancelled"),
|
||||
index: "cancelled",
|
||||
},
|
||||
];
|
||||
|
||||
const performanceCategories: {
|
||||
title: string;
|
||||
index: "rating" | "no_show" | "no_show_guest" | "csat";
|
||||
}[] = [
|
||||
{
|
||||
title: t("event_ratings"),
|
||||
index: "rating",
|
||||
@@ -77,41 +74,112 @@ export const BookingKPICards = () => {
|
||||
];
|
||||
|
||||
if (isPending) {
|
||||
return <LoadingKPICards categories={categories} />;
|
||||
return (
|
||||
<LoadingKPICards eventCategories={eventCategories} performanceCategories={performanceCategories} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid numColsSm={2} numColsLg={4} className="gap-x-4 gap-y-4">
|
||||
{categories.map((item) => (
|
||||
<KPICard
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
value={data[item.index].count}
|
||||
previousMetricData={data[item.index]}
|
||||
previousDateRange={data.previousRange}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
<div className="space-y-4">
|
||||
<ChartCard title={t("events")}>
|
||||
<StatContainer>
|
||||
{eventCategories.map((item, index) => (
|
||||
<StatItem key={item.title} index={index}>
|
||||
<KPICard
|
||||
title={item.title}
|
||||
value={data[item.index].count}
|
||||
previousMetricData={data[item.index]}
|
||||
previousDateRange={data.previousRange}
|
||||
/>
|
||||
</StatItem>
|
||||
))}
|
||||
</StatContainer>
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard title={t("performance")}>
|
||||
<StatContainer>
|
||||
{performanceCategories.map((item, index) => (
|
||||
<StatItem key={item.title} index={index}>
|
||||
<KPICard
|
||||
title={item.title}
|
||||
value={data[item.index].count}
|
||||
previousMetricData={data[item.index]}
|
||||
previousDateRange={data.previousRange}
|
||||
/>
|
||||
</StatItem>
|
||||
))}
|
||||
</StatContainer>
|
||||
</ChartCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingKPICards = (props: { categories: { title: string; index: string }[] }) => {
|
||||
const { categories } = props;
|
||||
const LoadingKPICards = (props: {
|
||||
eventCategories: { title: string; index: string }[];
|
||||
performanceCategories: { title: string; index: string }[];
|
||||
}) => {
|
||||
const { eventCategories, performanceCategories } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Grid numColsSm={2} numColsLg={4} className="gap-x-4 gap-y-4">
|
||||
{categories.map((item) => (
|
||||
<CardInsights key={item.title}>
|
||||
<SkeletonContainer className="flex w-full flex-col">
|
||||
<SkeletonText className="mt-2 h-4 w-32" />
|
||||
<SkeletonText className="mt-2 h-6 w-16" />
|
||||
<SkeletonText className="mt-4 h-6 w-44" />
|
||||
</SkeletonContainer>
|
||||
</CardInsights>
|
||||
))}
|
||||
</Grid>
|
||||
<div className="space-y-4">
|
||||
<ChartCard title={t("events")}>
|
||||
<StatContainer>
|
||||
{eventCategories.map((item, index) => (
|
||||
<StatItem key={item.title} index={index}>
|
||||
<div>
|
||||
<SkeletonText className="mb-2 h-4 w-24" />
|
||||
<div className="items-baseline justify-start space-x-3 truncate">
|
||||
<SkeletonText className="h-8 w-16" />
|
||||
</div>
|
||||
<div className="mt-4 justify-start space-x-2">
|
||||
<SkeletonText className="h-4 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
</StatItem>
|
||||
))}
|
||||
</StatContainer>
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard title={t("performance")}>
|
||||
<StatContainer>
|
||||
{performanceCategories.map((item, index) => (
|
||||
<StatItem key={item.title} index={index}>
|
||||
<div>
|
||||
<SkeletonText className="mb-2 h-4 w-24" />
|
||||
<div className="items-baseline justify-start space-x-3 truncate">
|
||||
<SkeletonText className="h-8 w-16" />
|
||||
</div>
|
||||
<div className="mt-4 justify-start space-x-2">
|
||||
<SkeletonText className="h-4 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
</StatItem>
|
||||
))}
|
||||
</StatContainer>
|
||||
</ChartCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// StatContainer: wraps the grid
|
||||
function StatContainer({ children }: { children: React.ReactNode }) {
|
||||
return <div className="group grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4">{children}</div>;
|
||||
}
|
||||
|
||||
// StatItem: handles border logic
|
||||
function StatItem({ index, children }: { index: number; children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"border-muted flex-1 p-4",
|
||||
index === 0 && "border-b sm:border-r md:border-b-0 md:border-r",
|
||||
index === 1 && "border-b sm:border-r-0 md:border-b-0 md:border-r",
|
||||
index === 2 && "border-b sm:border-b-0 sm:border-r md:border-b-0"
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { valueFormatter } from "../lib/valueFormatter";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LineChart } from "./LineChart";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
|
||||
@@ -39,16 +37,15 @@ export const BookingStatusLineChart = () => {
|
||||
if (!isSuccess) return null;
|
||||
|
||||
return (
|
||||
<CardInsights>
|
||||
<Title className="text-emphasis">{t("event_trends")}</Title>
|
||||
<ChartCard title={t("event_trends")}>
|
||||
<LineChart
|
||||
className="linechart mt-4 h-80"
|
||||
className="linechart ml-4 mt-4 h-80 sm:ml-0"
|
||||
data={eventsTimeLine ?? []}
|
||||
categories={["Created", "Completed", "Rescheduled", "Cancelled", "No-Show (Host)", "No-Show (Guest)"]}
|
||||
index="Month"
|
||||
colors={["purple", "green", "blue", "red", "slate", "orange"]}
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, type ReactNode } from "react";
|
||||
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
@@ -51,6 +51,15 @@ export function ChartCard({
|
||||
);
|
||||
}
|
||||
|
||||
export function ChartCardItem({ count, children }: { count?: number | string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="text-default border-muted flex items-center justify-between border-b px-3 py-3.5 last:border-b-0">
|
||||
<div className="text-sm font-medium">{children}</div>
|
||||
{count !== undefined && <div className="text-sm font-medium">{count}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Legend({ items, size = "default" }: { items: LegendItem[]; size?: LegendSize }) {
|
||||
return (
|
||||
<div className="bg-default flex items-center gap-2 rounded-lg px-1.5 py-1">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Table, TableBody, TableCell, TableRow, Text } from "@tremor/react";
|
||||
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
export const FeedbackTable = ({
|
||||
data,
|
||||
@@ -23,50 +22,44 @@ export const FeedbackTable = ({
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<>
|
||||
{data && data?.length > 0 ? (
|
||||
data?.map((item) => (
|
||||
<TableRow key={item.userId}>
|
||||
<TableCell className="flex flex-row">
|
||||
<Avatar
|
||||
alt={item.user.name || ""}
|
||||
size="sm"
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: item.user.avatarUrl })}
|
||||
title={item.user.name || ""}
|
||||
className="m-2"
|
||||
/>
|
||||
<p className="text-default mx-0 my-auto">
|
||||
<strong>{item.user.name}</strong>
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>
|
||||
<strong className="text-default">{item.rating}</strong>
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
<Text>
|
||||
<strong className="text-default">{item.feedback}</strong>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon="zap"
|
||||
headline={t("no_ratings")}
|
||||
description={t("no_ratings_description")}
|
||||
buttonRaw={
|
||||
<Button target="_blank" color="secondary" href="/workflows">
|
||||
{t("workflows")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="overflow-hidden rounded-md">
|
||||
{data && data?.length > 0 ? (
|
||||
data?.map((item) => (
|
||||
<div
|
||||
key={item.userId}
|
||||
className="border-subtle flex items-center justify-between border-b px-4 py-3 last:border-b-0">
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
alt={item.user.name || ""}
|
||||
size="sm"
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: item.user.avatarUrl })}
|
||||
title={item.user.name || ""}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div className="text-default text-sm font-medium">{item.user.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-default text-sm font-medium">{item.rating}</div>
|
||||
<Tooltip content={item.feedback}>
|
||||
<div className="text-default max-w-32 md:max-w-52 truncate text-sm font-medium">
|
||||
{item.feedback}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon="zap"
|
||||
headline={t("no_ratings")}
|
||||
description={t("no_ratings_description")}
|
||||
buttonRaw={
|
||||
<Button target="_blank" color="secondary" href="/workflows">
|
||||
{t("workflows")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
import { TotalUserFeedbackTable } from "./TotalUserFeedbackTable";
|
||||
|
||||
@@ -33,10 +31,9 @@ export const HighestNoShowHostTable = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return data && data.length > 0 ? (
|
||||
<CardInsights className="shadow-none">
|
||||
<Title className="text-emphasis">{t("most_no_show_host")}</Title>
|
||||
<ChartCard title={t("most_no_show_host")}>
|
||||
<TotalUserFeedbackTable data={data} />
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
import { TotalUserFeedbackTable } from "./TotalUserFeedbackTable";
|
||||
|
||||
@@ -33,10 +31,9 @@ export const HighestRatedMembersTable = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return data && data.length > 0 ? (
|
||||
<CardInsights className="shadow-none">
|
||||
<Title className="text-emphasis">{t("highest_rated_members")}</Title>
|
||||
<ChartCard title={t("highest_rated")}>
|
||||
<TotalUserFeedbackTable data={data} />
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import { calculateDeltaType, colors, valueFormatter } from "../lib";
|
||||
import { CardInsights } from "./Card";
|
||||
|
||||
export const KPICard = ({
|
||||
title,
|
||||
@@ -21,7 +20,7 @@ export const KPICard = ({
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<CardInsights key={title}>
|
||||
<div>
|
||||
<Text className="text-default">{title}</Text>
|
||||
<Flex className="items-baseline justify-start space-x-3 truncate">
|
||||
<Metric className="text-emphasis">{valueFormatter(previousMetricData.count)}</Metric>
|
||||
@@ -47,6 +46,6 @@ export const KPICard = ({
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CardInsights>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
|
||||
|
||||
@@ -33,9 +31,8 @@ export const LeastBookedTeamMembersTable = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return (
|
||||
<CardInsights>
|
||||
<Title className="text-emphasis">{t("least_booked_members")}</Title>
|
||||
<ChartCard title={t("least_booked_members")}>
|
||||
<TotalBookingUsersTable data={data} />
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
import { TotalUserFeedbackTable } from "./TotalUserFeedbackTable";
|
||||
|
||||
@@ -33,10 +31,9 @@ export const LowestRatedMembersTable = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return data && data.length > 0 ? (
|
||||
<CardInsights className="shadow-none">
|
||||
<Title className="text-emphasis">{t("lowest_rated_members")}</Title>
|
||||
<ChartCard title={t("lowest_rated")}>
|
||||
<TotalUserFeedbackTable data={data} />
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
|
||||
|
||||
@@ -33,9 +31,8 @@ export const MostBookedTeamMembersTable = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return (
|
||||
<CardInsights className="shadow-none">
|
||||
<Title className="text-emphasis">{t("most_booked_members")}</Title>
|
||||
<ChartCard title={t("most_booked_members")}>
|
||||
<TotalBookingUsersTable data={data} />
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
|
||||
|
||||
@@ -31,9 +29,8 @@ export const MostCancelledBookingsTables = () => {
|
||||
if (isPending) return <LoadingInsight />;
|
||||
|
||||
return (
|
||||
<CardInsights className="shadow-none">
|
||||
<Title className="text-emphasis">{t("most_cancelled_bookings")}</Title>
|
||||
<ChartCard title={t("most_cancelled_bookings")}>
|
||||
{!isSuccess || !startDate || !endDate || !teamId ? null : <TotalBookingUsersTable data={data} />}
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Table, TableBody, TableCell, TableRow, Text, Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard, ChartCardItem } from "./ChartCard";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
|
||||
export const PopularEventsTable = () => {
|
||||
@@ -34,30 +32,24 @@ export const PopularEventsTable = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return (
|
||||
<CardInsights>
|
||||
<Title className="text-emphasis">{t("popular_events")}</Title>
|
||||
<Table className="mt-5">
|
||||
<TableBody>
|
||||
{data.map(
|
||||
(item) =>
|
||||
item.eventTypeId && (
|
||||
<TableRow key={item.eventTypeId}>
|
||||
<TableCell className="text-default">{item.eventTypeName}</TableCell>
|
||||
<TableCell>
|
||||
<Text className="text-default text-right">
|
||||
<strong>{item.count}</strong>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<ChartCard title={t("popular_events")}>
|
||||
<div>
|
||||
{data
|
||||
.filter((item) => item.eventTypeId)
|
||||
.map((item) => (
|
||||
<ChartCardItem key={item.eventTypeId} count={item.count}>
|
||||
<div className="flex">
|
||||
<div className="bg-subtle mr-1.5 h-5 w-[2px] shrink-0 rounded-sm" />
|
||||
<p>{item.eventTypeName}</p>
|
||||
</div>
|
||||
</ChartCardItem>
|
||||
))}
|
||||
</div>
|
||||
{data.length === 0 && (
|
||||
<div className="flex h-60 text-center">
|
||||
<p className="m-auto text-sm font-light">{t("insights_no_data_found_for_filter")}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { CardInsights } from "./Card";
|
||||
import { ChartCard } from "./ChartCard";
|
||||
import { FeedbackTable } from "./FeedbackTable";
|
||||
import { LoadingInsight } from "./LoadingInsights";
|
||||
|
||||
@@ -33,9 +31,8 @@ export const RecentFeedbackTable = () => {
|
||||
if (!isSuccess || !data) return null;
|
||||
|
||||
return (
|
||||
<CardInsights>
|
||||
<Title className="text-emphasis">{t("recent_ratings")}</Title>
|
||||
<ChartCard title={t("recent_ratings")}>
|
||||
<FeedbackTable data={data} />
|
||||
</CardInsights>
|
||||
</ChartCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Table, TableBody, TableCell, TableRow, Text } from "@tremor/react";
|
||||
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
import { ChartCardItem } from "./ChartCard";
|
||||
|
||||
export const TotalBookingUsersTable = ({
|
||||
data,
|
||||
}: {
|
||||
@@ -19,38 +19,27 @@ export const TotalBookingUsersTable = ({
|
||||
}) => {
|
||||
const filteredData = data && data?.length > 0 ? data?.filter((item) => !!item.user) : [];
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableRow key={item.userId}>
|
||||
<TableCell className="flex flex-row">
|
||||
<Avatar
|
||||
alt={item.user.name || ""}
|
||||
size="sm"
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: item.user.avatarUrl })}
|
||||
title={item.user.name || ""}
|
||||
className="m-2"
|
||||
/>
|
||||
<p className="text-default mx-0 my-auto">
|
||||
<strong>{item.user.name}</strong>
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>
|
||||
<strong className="text-default">{item.count}</strong>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>No members found</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="overflow-hidden rounded-md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<ChartCardItem key={item.userId} count={item.count}>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
alt={item.user.name || ""}
|
||||
size="sm"
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: item.user.avatarUrl })}
|
||||
title={item.user.name || ""}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div>{item.user.name}</div>
|
||||
</div>
|
||||
</ChartCardItem>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-60 text-center">
|
||||
<p className="m-auto text-sm font-light">No members found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Table, TableBody, TableCell, TableRow, Text } from "@tremor/react";
|
||||
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
import { ChartCardItem } from "./ChartCard";
|
||||
|
||||
export const TotalUserFeedbackTable = ({
|
||||
data,
|
||||
}: {
|
||||
@@ -19,36 +19,29 @@ export const TotalUserFeedbackTable = ({
|
||||
| undefined;
|
||||
}) => {
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<>
|
||||
{data &&
|
||||
data?.length > 0 &&
|
||||
data?.map((item) => (
|
||||
<TableRow key={item.userId}>
|
||||
<TableCell className="flex flex-row">
|
||||
<Avatar
|
||||
alt={item.user.name || ""}
|
||||
size="sm"
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: item.user.avatarUrl })}
|
||||
title={item.user.name || ""}
|
||||
className="m-2"
|
||||
/>
|
||||
<p className="text-default mx-0 my-auto">
|
||||
<strong>{item.user.name}</strong>
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Text>
|
||||
<strong className="text-default">
|
||||
{item.averageRating ? item.averageRating.toFixed(1) : item.count}
|
||||
</strong>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="overflow-hidden rounded-md">
|
||||
{data && data?.length > 0 ? (
|
||||
data?.map((item) => (
|
||||
<ChartCardItem
|
||||
key={item.userId}
|
||||
count={item.averageRating ? item.averageRating.toFixed(1) : item.count}>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
alt={item.user.name || ""}
|
||||
size="sm"
|
||||
imageSrc={getUserAvatarUrl({ avatarUrl: item.user.avatarUrl })}
|
||||
title={item.user.name || ""}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div className="text-default text-sm font-medium">{item.user.name}</div>
|
||||
</div>
|
||||
</ChartCardItem>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-60 text-center">
|
||||
<p className="m-auto text-sm font-light">No data found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user