insights follow up (#7922)
* init page * init insights frontend * nit * nit * nit * merge * fixed icons * i18n, needs features * Init insights trpc * Using trpc on client * Added events timeline * Seed analytics script * connect ui with trpc * Added and fixed event time lines * WIP popular days and avg time duration event type * added new metric graphs * improved upgrade tip * always show upgrade screen * upgrade tremor.so and select inputs for page * Remove log * Move everything to components and add context * Fix select types using calcom ui one * Adding translations * Add missing translations * Add more translations * min fix * Fixes for date select * Prefer early return and mobile design fixes * Fix style for mobile * Fix data with userId filter from popular events * add user id to average time duration * fix types for select-react * Removed submodules * Delete website * Update yarn.lock * Code organization and type fixes * trpc fixes * Builds are now passing * Relocates server code * Add url state in insights * Update FiltersProvider.tsx * Cleanup * Update embed-iframe.ts * Update FilterType.tsx * Update seed-app-store.config.json * Update index.tsx * Renamed seeder * Update FiltersProvider.tsx * Fix for query params * no wrap on lg screen * Fix shadow borders from tremor components, fix title font * Add ring-gray to match filters * add cursor pointer * copy improvements * blue to black * fixed date select focus * Adds missing translation strings * Fix url state for filter type * Apply suggestions from code review * Updated yarn lock * Adds feature flag * Type fix --------- Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: zomars <zomars@me.com>pull/7997/head^2
parent
3a08eb3dca
commit
6c51e2a970
|
@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
|
|||
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon";
|
||||
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
|
@ -40,6 +40,43 @@ export default function Custom404() {
|
|||
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
||||
const isSignup = router.asPath.startsWith("/signup");
|
||||
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
||||
/**
|
||||
* If we're on 404 and the route is insights it means it is disabled
|
||||
* TODO: Abstract this for all disabled features
|
||||
**/
|
||||
const isInsights = router.asPath.startsWith("/insights");
|
||||
if (isInsights) {
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
title="Feature is currently disabled"
|
||||
description={t("404_page_not_found")}
|
||||
nextSeoProps={{
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
}}
|
||||
/>
|
||||
<div className="min-h-screen bg-white px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
|
||||
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||
Feature is currently disabled
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
<div className="mt-8">
|
||||
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import {
|
||||
AverageEventDurationChart,
|
||||
BookingKPICards,
|
||||
|
@ -39,17 +40,17 @@ export default function InsightsPage() {
|
|||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const features = [
|
||||
{
|
||||
icon: <FiUsers className="h-5 w-5 text-red-500" />,
|
||||
icon: <FiUsers className="h-5 w-5" />,
|
||||
title: t("view_bookings_across"),
|
||||
description: t("view_bookings_across_description"),
|
||||
},
|
||||
{
|
||||
icon: <FiRefreshCcw className="h-5 w-5 text-blue-500" />,
|
||||
icon: <FiRefreshCcw className="h-5 w-5" />,
|
||||
title: t("identify_booking_trends"),
|
||||
description: t("identify_booking_trends_description"),
|
||||
},
|
||||
{
|
||||
icon: <FiUserPlus className="h-5 w-5 text-green-500" />,
|
||||
icon: <FiUserPlus className="h-5 w-5" />,
|
||||
title: t("spot_popular_event_types"),
|
||||
description: t("spot_popular_event_types_description"),
|
||||
},
|
||||
|
@ -114,3 +115,19 @@ export default function InsightsPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If feature flag is disabled, return not found on getServerSideProps
|
||||
export const getServerSideProps = async () => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
|
||||
if (flags.insights === false) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1218,6 +1218,7 @@
|
|||
"impersonation": "Impersonation",
|
||||
"impersonation_description": "Settings to manage user impersonation",
|
||||
"users": "Users",
|
||||
"user": "User",
|
||||
"profile_description": "Manage settings for your {{appName}} profile",
|
||||
"users_description": "Here you can find a list of all users",
|
||||
"users_listing": "User listing",
|
||||
|
@ -1687,7 +1688,9 @@
|
|||
"events_rescheduled": "Events Rescheduled",
|
||||
"from_last_period": "from last period",
|
||||
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
||||
"analytics_for_organisation": "Analytics for {{organisationName}}",
|
||||
"subtitle_analytics": "This is a organisation analytics",
|
||||
"event_trends": "Event Trends"
|
||||
"analytics_for_organisation": "Insights for {{organisationName}}",
|
||||
"subtitle_analytics": "Learn more about your team's activity",
|
||||
"event_trends": "Event Trends",
|
||||
"clear_filters": "Clear Filters",
|
||||
"insights": "Insights"
|
||||
}
|
||||
|
|
|
@ -1213,6 +1213,7 @@
|
|||
"impersonation": "Suplantación",
|
||||
"impersonation_description": "Configuración para administrar la suplantación del usuario",
|
||||
"users": "Usuarios",
|
||||
"user": "Usuario",
|
||||
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
|
||||
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
|
||||
"users_listing": "Lista de usuarios",
|
||||
|
|
|
@ -497,3 +497,5 @@ hr {
|
|||
::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
**/
|
||||
export type AppFlags = {
|
||||
emails: boolean;
|
||||
insights: boolean;
|
||||
teams: boolean;
|
||||
webhooks: boolean;
|
||||
workflows: boolean;
|
||||
"booking-page-v2": boolean;
|
||||
"v2-booking-page": boolean;
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Card, LineChart, Title } from "@tremor/react";
|
||||
import { LineChart, Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { valueFormatter } from "../lib/valueFormatter";
|
||||
import { CardInsights } from "./Card";
|
||||
|
||||
export const AverageEventDurationChart = () => {
|
||||
const { t } = useLocale();
|
||||
|
@ -23,7 +24,7 @@ export const AverageEventDurationChart = () => {
|
|||
if (!isSuccess || data?.length == 0 || !startDate || !endDate || !teamId) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardInsights>
|
||||
<Title>{t("average_event_duration")}</Title>
|
||||
<LineChart
|
||||
className="mt-4 h-80"
|
||||
|
@ -33,6 +34,6 @@ export const AverageEventDurationChart = () => {
|
|||
colors={["blue"]}
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
</Card>
|
||||
</CardInsights>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Card, LineChart, Title } from "@tremor/react";
|
||||
import { LineChart, Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { valueFormatter } from "../lib/valueFormatter";
|
||||
import { CardInsights } from "./Card";
|
||||
|
||||
export const BookingStatusLineChart = () => {
|
||||
const { t } = useLocale();
|
||||
|
@ -25,7 +26,7 @@ export const BookingStatusLineChart = () => {
|
|||
if (!isSuccess) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardInsights>
|
||||
<Title>{t("event_trends")}</Title>
|
||||
<LineChart
|
||||
className="mt-4 h-80"
|
||||
|
@ -35,6 +36,6 @@ export const BookingStatusLineChart = () => {
|
|||
colors={["gray", "green", "blue", "red"]}
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
</Card>
|
||||
</CardInsights>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { Card } from "@tremor/react";
|
||||
|
||||
interface ICardProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CardInsights = (props: ICardProps) => {
|
||||
const { children, className = "", ...rest } = props;
|
||||
|
||||
return (
|
||||
<Card className={`shadow-none ring-gray-300 ${className}`} {...rest}>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { Card, Flex, Text, Metric, BadgeDelta } from "@tremor/react";
|
||||
import { Flex, Text, Metric, BadgeDelta } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
|
||||
import { calculateDeltaType, colors, valueFormatter } from "../lib";
|
||||
import { CardInsights } from "./Card";
|
||||
|
||||
export const KPICard = ({
|
||||
title,
|
||||
|
@ -20,7 +21,7 @@ export const KPICard = ({
|
|||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<Card key={title}>
|
||||
<CardInsights key={title}>
|
||||
<Text>{title}</Text>
|
||||
<Flex className="items-baseline justify-start space-x-3 truncate">
|
||||
<Metric>{valueFormatter(previousMetricData.count)}</Metric>
|
||||
|
@ -40,10 +41,12 @@ export const KPICard = ({
|
|||
startDate: previousDateRange.startDate,
|
||||
endDate: previousDateRange.endDate,
|
||||
})}>
|
||||
<small className="relative top-px text-xs text-gray-600">{t("from_last_period")}</small>
|
||||
<small className="relative top-px cursor-pointer text-xs text-gray-600">
|
||||
{t("from_last_period")}
|
||||
</small>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</CardInsights>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
import { Card, Title } from "@tremor/react";
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { CardInsights } from "./Card";
|
||||
import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
|
||||
|
||||
export const LeastBookedTeamMembersTable = () => {
|
||||
const { t } = useLocale();
|
||||
const { filter } = useFilterContext();
|
||||
const { dateRange } = filter;
|
||||
const { dateRange, selectedEventTypeId, selectedTeamId: teamId } = filter;
|
||||
const [startDate, endDate] = dateRange;
|
||||
const { selectedTeamId: teamId } = filter;
|
||||
|
||||
const { data, isSuccess } = trpc.viewer.insights.membersWithLeastBookings.useQuery({
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
teamId,
|
||||
eventTypeId: selectedEventTypeId ?? undefined,
|
||||
});
|
||||
|
||||
if (!isSuccess || !startDate || !endDate || !teamId) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardInsights>
|
||||
<Title>{t("least_booked_members")}</Title>
|
||||
<TotalBookingUsersTable data={data} />
|
||||
</Card>
|
||||
</CardInsights>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Card, Title } from "@tremor/react";
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { CardInsights } from "./Card";
|
||||
import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
|
||||
|
||||
export const MostBookedTeamMembersTable = () => {
|
||||
|
@ -23,9 +24,9 @@ export const MostBookedTeamMembersTable = () => {
|
|||
if (!isSuccess || !startDate || !endDate || !teamId) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardInsights className="shadow-none">
|
||||
<Title>{t("most_booked_members")}</Title>
|
||||
<TotalBookingUsersTable data={data} />
|
||||
</Card>
|
||||
</CardInsights>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Card, Title, Table, TableBody, TableCell, TableRow, Text } from "@tremor/react";
|
||||
import { Table, TableBody, TableCell, TableRow, Text, Title } from "@tremor/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { CardInsights } from "./Card";
|
||||
|
||||
export const PopularEventsTable = () => {
|
||||
const { t } = useLocale();
|
||||
|
@ -19,34 +20,25 @@ export const PopularEventsTable = () => {
|
|||
userId: selectedUserId ?? undefined,
|
||||
});
|
||||
|
||||
if (!startDate || !endDate || !teamId) return null;
|
||||
if (!isSuccess || !startDate || !endDate || !teamId || data?.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardInsights>
|
||||
<Title>{t("popular_events")}</Title>
|
||||
<Table className="mt-5">
|
||||
<TableBody>
|
||||
{isSuccess ? (
|
||||
data?.map((item) => (
|
||||
<TableRow key={item.eventTypeId}>
|
||||
<TableCell>{item.eventTypeName}</TableCell>
|
||||
<TableCell>
|
||||
<Text>
|
||||
<strong>{item.count}</strong>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell>{t("no_event_types_found")}</TableCell>
|
||||
{data.map((item) => (
|
||||
<TableRow key={item.eventTypeId}>
|
||||
<TableCell>{item.eventTypeName}</TableCell>
|
||||
<TableCell>
|
||||
<strong>0</strong>
|
||||
<Text>
|
||||
<strong>{item.count}</strong>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</CardInsights>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
|
@ -6,19 +8,60 @@ import type { FilterContextType } from "./provider";
|
|||
import { FilterProvider } from "./provider";
|
||||
|
||||
export function FiltersProvider({ children }: { children: React.ReactNode }) {
|
||||
// useRouter to get initial values from query params
|
||||
const router = useRouter();
|
||||
const { startTime, endTime, teamId, userId, eventTypeId, filter } = router.query;
|
||||
const querySchema = z.object({
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
teamId: z.coerce.number().optional(),
|
||||
userId: z.coerce.number().optional(),
|
||||
eventTypeId: z.coerce.number().optional(),
|
||||
filter: z.enum(["event-type", "user"]).optional(),
|
||||
});
|
||||
|
||||
let startTimeParsed, endTimeParsed, teamIdParsed, userIdParsed, eventTypeIdParsed, filterParsed;
|
||||
|
||||
const safe = querySchema.safeParse({
|
||||
startTime,
|
||||
endTime,
|
||||
teamId,
|
||||
userId,
|
||||
eventTypeId,
|
||||
filter,
|
||||
});
|
||||
|
||||
if (!safe.success) {
|
||||
console.error("Failed to parse query params");
|
||||
} else {
|
||||
startTimeParsed = safe.data.startTime;
|
||||
endTimeParsed = safe.data.endTime;
|
||||
teamIdParsed = safe.data.teamId;
|
||||
userIdParsed = safe.data.userId;
|
||||
eventTypeIdParsed = safe.data.eventTypeId;
|
||||
filterParsed = safe.data.filter;
|
||||
}
|
||||
|
||||
// TODO: Sync insight filters with URL parameters
|
||||
const [selectedTimeView, setSelectedTimeView] =
|
||||
useState<FilterContextType["filter"]["selectedTimeView"]>("week");
|
||||
const [selectedUserId, setSelectedUserId] = useState<FilterContextType["filter"]["selectedUserId"]>(null);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<FilterContextType["filter"]["selectedTeamId"]>(null);
|
||||
const [selectedEventTypeId, setSelectedEventTypeId] =
|
||||
useState<FilterContextType["filter"]["selectedEventTypeId"]>(null);
|
||||
const [selectedFilter, setSelectedFilter] = useState<FilterContextType["filter"]["selectedFilter"]>(null);
|
||||
const [selectedUserId, setSelectedUserId] = useState<FilterContextType["filter"]["selectedUserId"]>(
|
||||
userIdParsed || null
|
||||
);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<FilterContextType["filter"]["selectedTeamId"]>(
|
||||
teamIdParsed || null
|
||||
);
|
||||
const [selectedEventTypeId, setSelectedEventTypeId] = useState<
|
||||
FilterContextType["filter"]["selectedEventTypeId"]
|
||||
>(eventTypeIdParsed || null);
|
||||
const [selectedFilter, setSelectedFilter] = useState<FilterContextType["filter"]["selectedFilter"]>(
|
||||
filterParsed ? [filterParsed] : null
|
||||
);
|
||||
const [selectedTeamName, setSelectedTeamName] =
|
||||
useState<FilterContextType["filter"]["selectedTeamName"]>(null);
|
||||
const [dateRange, setDateRange] = useState<FilterContextType["filter"]["dateRange"]>([
|
||||
dayjs().subtract(1, "month"),
|
||||
dayjs(),
|
||||
startTimeParsed ? dayjs(startTimeParsed) : dayjs().subtract(1, "month"),
|
||||
endTimeParsed ? dayjs(endTimeParsed) : dayjs(),
|
||||
"t",
|
||||
]);
|
||||
return (
|
||||
|
@ -33,13 +76,58 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
|
|||
selectedEventTypeId,
|
||||
selectedFilter,
|
||||
},
|
||||
setSelectedFilter: (filter) => setSelectedFilter(filter),
|
||||
setDateRange: (dateRange) => setDateRange(dateRange),
|
||||
setSelectedFilter: (filter) => {
|
||||
setSelectedFilter(filter);
|
||||
const userId = filter?.[0] === "user" ? selectedUserId : undefined;
|
||||
const eventTypeId = filter?.[0] === "event-type" ? selectedEventTypeId : undefined;
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
filter: filter?.[0],
|
||||
userId,
|
||||
eventTypeId,
|
||||
},
|
||||
});
|
||||
},
|
||||
setDateRange: (dateRange) => {
|
||||
setDateRange(dateRange);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
startTime: dateRange[0].toISOString(),
|
||||
endTime: dateRange[1].toISOString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
setSelectedTimeView: (selectedTimeView) => setSelectedTimeView(selectedTimeView),
|
||||
setSelectedUserId: (selectedUserId) => setSelectedUserId(selectedUserId),
|
||||
setSelectedTeamId: (selectedTeamId) => setSelectedTeamId(selectedTeamId),
|
||||
setSelectedUserId: (selectedUserId) => {
|
||||
setSelectedUserId(selectedUserId);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
userId: selectedUserId,
|
||||
},
|
||||
});
|
||||
},
|
||||
setSelectedTeamId: (selectedTeamId) => {
|
||||
setSelectedTeamId(selectedTeamId);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
teamId: selectedTeamId,
|
||||
},
|
||||
});
|
||||
},
|
||||
setSelectedTeamName: (selectedTeamName) => setSelectedTeamName(selectedTeamName),
|
||||
setSelectedEventTypeId: (selectedEventTypeId) => setSelectedEventTypeId(selectedEventTypeId),
|
||||
setSelectedEventTypeId: (selectedEventTypeId) => {
|
||||
setSelectedEventTypeId(selectedEventTypeId);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
eventTypeId: selectedEventTypeId,
|
||||
},
|
||||
});
|
||||
},
|
||||
}}>
|
||||
{children}
|
||||
</FilterProvider>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tremor-DateRangePicker-calendarButton, .tremor-DateRangePicker-dropdownButton {
|
||||
@apply dark:bg-darkgray-100 dark:border-darkgray-300 border-gray-300 bg-white text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-0 focus-within:ring-gray-800 hover:border-gray-400 dark:focus-within:ring-darkgray-900
|
||||
}
|
|
@ -4,6 +4,7 @@ import dayjs from "@calcom/dayjs";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import "./DateSelect.css";
|
||||
|
||||
type RangeType = "tdy" | "w" | "t" | "m" | "y" | undefined | null;
|
||||
|
||||
|
@ -15,44 +16,49 @@ export const DateSelect = () => {
|
|||
const startValue = startDate?.toDate() || null;
|
||||
const endValue = endDate?.toDate() || null;
|
||||
return (
|
||||
<DateRangePicker
|
||||
value={[startValue, endValue, range]}
|
||||
defaultValue={[startValue, endValue, range]}
|
||||
onValueChange={(datesArray) => {
|
||||
const [selected, ...rest] = datesArray;
|
||||
const [start, end, range] = datesArray;
|
||||
// If range has value and it's of type RangeType
|
||||
<div className="custom-date">
|
||||
<DateRangePicker
|
||||
value={[startValue, endValue, range]}
|
||||
defaultValue={[startValue, endValue, range]}
|
||||
onValueChange={(datesArray) => {
|
||||
const [selected, ...rest] = datesArray;
|
||||
const [start, end, range] = datesArray;
|
||||
// If range has value and it's of type RangeType
|
||||
|
||||
if (range && (range === "tdy" || range === "w" || range === "t" || range === "m" || range === "y")) {
|
||||
setDateRange([dayjs(start), dayjs(end), range]);
|
||||
return;
|
||||
} else if (start && !end) {
|
||||
// If only start time has value that means selected date should push to dateRange with last value null
|
||||
const currentDates = filter.dateRange;
|
||||
// remove last position of array
|
||||
currentDates.pop();
|
||||
// push new value to array
|
||||
currentDates.push(dayjs(selected));
|
||||
// if lenght > 2 then remove first value
|
||||
if (currentDates.length > 2) {
|
||||
currentDates.shift();
|
||||
if (
|
||||
range &&
|
||||
(range === "tdy" || range === "w" || range === "t" || range === "m" || range === "y")
|
||||
) {
|
||||
setDateRange([dayjs(start), dayjs(end), range]);
|
||||
return;
|
||||
} else if (start && !end) {
|
||||
// If only start time has value that means selected date should push to dateRange with last value null
|
||||
const currentDates = filter.dateRange;
|
||||
// remove last position of array
|
||||
currentDates.pop();
|
||||
// push new value to array
|
||||
currentDates.push(dayjs(selected));
|
||||
// if lenght > 2 then remove first value
|
||||
if (currentDates.length > 2) {
|
||||
currentDates.shift();
|
||||
}
|
||||
|
||||
setDateRange([currentDates[0], currentDates[1], null]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setDateRange([currentDates[0], currentDates[1], null]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If range has value and it's of type RangeType
|
||||
}}
|
||||
options={undefined}
|
||||
enableDropdown={true}
|
||||
placeholder={t("select_date_range")}
|
||||
enableYearPagination={true}
|
||||
minDate={currentDate.subtract(2, "year").toDate()}
|
||||
maxDate={currentDate.toDate()}
|
||||
color="blue"
|
||||
className="h-[42px] max-w-sm"
|
||||
/>
|
||||
// If range has value and it's of type RangeType
|
||||
}}
|
||||
options={undefined}
|
||||
enableDropdown={true}
|
||||
placeholder={t("select_date_range")}
|
||||
enableYearPagination={true}
|
||||
minDate={currentDate.subtract(2, "year").toDate()}
|
||||
maxDate={currentDate.toDate()}
|
||||
color="gray"
|
||||
className="h-[42px] max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,8 @@ type Option = { value: "event-type" | "user"; label: string };
|
|||
|
||||
export const FilterType = () => {
|
||||
const { t } = useLocale();
|
||||
const { setSelectedFilter, setSelectedUserId, setSelectedEventTypeId } = useFilterContext();
|
||||
const { setSelectedFilter, setSelectedUserId, setSelectedEventTypeId, filter } = useFilterContext();
|
||||
const { selectedFilter } = filter;
|
||||
|
||||
const filterOptions: Option[] = [
|
||||
{
|
||||
|
@ -21,20 +22,19 @@ export const FilterType = () => {
|
|||
},
|
||||
];
|
||||
|
||||
const filterValue = selectedFilter
|
||||
? filterOptions.find((option) => option.value === selectedFilter[0])
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Select<Option>
|
||||
isMulti={false}
|
||||
isSearchable={false}
|
||||
options={filterOptions}
|
||||
onChange={(input) => {
|
||||
if (input) {
|
||||
value={filterValue}
|
||||
defaultValue={filterValue}
|
||||
onChange={(newValue) => {
|
||||
if (newValue) {
|
||||
// This can multiple values, but for now we only want to have one filter active at a time
|
||||
setSelectedFilter([input.value]);
|
||||
if (input.value === "event-type") {
|
||||
setSelectedUserId(null);
|
||||
} else if (input.value === "user") {
|
||||
setSelectedEventTypeId(null);
|
||||
}
|
||||
setSelectedFilter([newValue.value]);
|
||||
}
|
||||
}}
|
||||
className="w-32 min-w-[130px]"
|
||||
|
|
|
@ -1,12 +1,43 @@
|
|||
import { useFilterContext } from "@calcom/features/insights/context/provider";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Tooltip } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
import { DateSelect } from "./DateSelect";
|
||||
import { EventTypeListInTeam } from "./EventTypeListInTeam";
|
||||
import { FilterType } from "./FilterType";
|
||||
import { TeamList } from "./TeamList";
|
||||
import { UserListInTeam } from "./UsersListInTeam";
|
||||
|
||||
const ClearFilters = () => {
|
||||
const { t } = useLocale();
|
||||
const { filter, setSelectedUserId, setSelectedFilter, setSelectedEventTypeId } = useFilterContext();
|
||||
const { selectedFilter } = filter;
|
||||
|
||||
if (!selectedFilter || selectedFilter?.length < 1) return null;
|
||||
|
||||
return (
|
||||
<Tooltip content={t("clear_filters")}>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
StartIcon={FiX}
|
||||
className="h-[38px]"
|
||||
onClick={() => {
|
||||
setSelectedFilter(null);
|
||||
setSelectedUserId(null);
|
||||
setSelectedEventTypeId(null);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const Filters = () => {
|
||||
return (
|
||||
<div className="mt-2 flex flex-col flex-wrap gap-2 md:flex-row md:flex-nowrap">
|
||||
<div className="mt-2 flex flex-col flex-wrap gap-2 md:flex-row lg:flex-nowrap">
|
||||
<TeamList />
|
||||
|
||||
<FilterType />
|
||||
|
@ -17,6 +48,8 @@ export const Filters = () => {
|
|||
|
||||
<DateSelect />
|
||||
|
||||
<ClearFilters />
|
||||
|
||||
{/* @NOTE: To be released in next iteration */}
|
||||
{/* <ButtonGroup combined containerProps={{ className: "hidden lg:flex mr-2" }}>
|
||||
<Tooltip content={t("settings")}>
|
||||
|
|
|
@ -377,7 +377,6 @@ export const insightsRouter = router({
|
|||
};
|
||||
|
||||
if (userId) {
|
||||
delete whereConditional.eventType;
|
||||
whereConditional["userId"] = userId;
|
||||
}
|
||||
|
||||
|
@ -707,6 +706,12 @@ export const insightsRouter = router({
|
|||
}
|
||||
|
||||
const eventTypes = await ctx.prisma.eventType.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
teamId: true,
|
||||
title: true,
|
||||
},
|
||||
where: {
|
||||
teamId: input.teamId,
|
||||
},
|
||||
|
|
|
@ -536,7 +536,7 @@ const navigation: NavigationItemType[] = [
|
|||
icon: FiZap,
|
||||
},
|
||||
{
|
||||
name: "Insights",
|
||||
name: "insights",
|
||||
href: "/insights",
|
||||
icon: FiBarChart,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
-- Insert initial feature flags with their default values
|
||||
INSERT INTO
|
||||
"Feature" (slug, enabled, description, "type")
|
||||
VALUES
|
||||
(
|
||||
'insights',
|
||||
true,
|
||||
'Enable insights for this instance',
|
||||
'OPERATIONAL'
|
||||
) ON CONFLICT (slug) DO NOTHING;
|
|
@ -17,7 +17,7 @@
|
|||
"post-install": "yarn generate-schemas",
|
||||
"seed-app-store": "ts-node --transpile-only ./seed-app-store.ts",
|
||||
"delete-app": "ts-node --transpile-only ./delete-app.ts",
|
||||
"seed-analytics": "ts-node --transpile-only ./seed-analytics.ts"
|
||||
"seed-insights": "ts-node --transpile-only ./seed-insights.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"npm-run-all": "^4.1.5"
|
||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -4084,7 +4084,6 @@ __metadata:
|
|||
"@calcom/ui": "*"
|
||||
"@headlessui/react": ^1.5.0
|
||||
"@heroicons/react": ^1.0.6
|
||||
"@next/font": ^13.1.6
|
||||
"@prisma/client": ^4.11.0
|
||||
"@tailwindcss/forms": ^0.5.2
|
||||
"@types/node": 16.9.1
|
||||
|
@ -4094,7 +4093,7 @@ __metadata:
|
|||
client-only: ^0.0.1
|
||||
eslint: ^8.34.0
|
||||
next: ^13.2.1
|
||||
next-auth: ^4.18.8
|
||||
next-auth: ^4.20.1
|
||||
next-i18next: ^11.3.0
|
||||
postcss: ^8.4.18
|
||||
prisma: ^4.11.0
|
||||
|
@ -4102,7 +4101,7 @@ __metadata:
|
|||
react: ^18.2.0
|
||||
react-chartjs-2: ^4.0.1
|
||||
react-dom: ^18.2.0
|
||||
react-hook-form: ^7.34.2
|
||||
react-hook-form: ^7.43.3
|
||||
react-live-chat-loader: ^2.7.3
|
||||
swr: ^1.2.2
|
||||
tailwindcss: ^3.2.1
|
||||
|
@ -7943,13 +7942,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/font@npm:^13.1.6":
|
||||
version: 13.2.4
|
||||
resolution: "@next/font@npm:13.2.4"
|
||||
checksum: f5319c895b6a6633b181f1c17d52dd98753dd7706db27644bf12383e0fa164f5223b8c5c70eb28edb9b9a113cdfb04a995accc16dba1655b850c9db8a05036b7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-android-arm-eabi@npm:13.2.3":
|
||||
version: 13.2.3
|
||||
resolution: "@next/swc-android-arm-eabi@npm:13.2.3"
|
||||
|
@ -29242,7 +29234,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"next-auth@npm:^4.18.8, next-auth@npm:^4.20.1":
|
||||
"next-auth@npm:^4.20.1":
|
||||
version: 4.20.1
|
||||
resolution: "next-auth@npm:4.20.1"
|
||||
dependencies:
|
||||
|
@ -32723,15 +32715,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-hook-form@npm:^7.34.2":
|
||||
version: 7.43.8
|
||||
resolution: "react-hook-form@npm:7.43.8"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
checksum: 1042b68950e1756dd907e7eb904c6efdf04f126a48797b25e8793705d930f7bbff2cc07f0ec7ea4c52272e808a359ae157742c551defa3b3fd0952ff414bd3e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-hook-form@npm:^7.43.3":
|
||||
version: 7.43.3
|
||||
resolution: "react-hook-form@npm:7.43.3"
|
||||
|
|
Loading…
Reference in New Issue