✨ (analytics) Add time dropdown to filter analytics with a time range
This commit is contained in:
@@ -6,6 +6,8 @@ import { canReadTypebots } from '@/helpers/databaseRules'
|
||||
import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
|
||||
import { parseGroups } from '@typebot.io/schemas'
|
||||
import { isInputBlock } from '@typebot.io/lib'
|
||||
import { defaultTimeFilter, timeFilterValues } from '../constants'
|
||||
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'
|
||||
|
||||
export const getTotalAnswers = authenticatedProcedure
|
||||
.meta({
|
||||
@@ -20,10 +22,11 @@ export const getTotalAnswers = authenticatedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
|
||||
})
|
||||
)
|
||||
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
|
||||
.query(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: canReadTypebots(typebotId, user),
|
||||
select: { publishedTypebot: true },
|
||||
@@ -34,11 +37,18 @@ export const getTotalAnswers = authenticatedProcedure
|
||||
message: 'Published typebot not found',
|
||||
})
|
||||
|
||||
const date = parseDateFromTimeFilter(timeFilter)
|
||||
|
||||
const totalAnswersPerBlock = await prisma.answer.groupBy({
|
||||
by: ['blockId'],
|
||||
where: {
|
||||
result: {
|
||||
typebotId: typebot.publishedTypebot.typebotId,
|
||||
createdAt: date
|
||||
? {
|
||||
gte: date,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
blockId: {
|
||||
in: parseGroups(typebot.publishedTypebot.groups, {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { canReadTypebots } from '@/helpers/databaseRules'
|
||||
import { totalVisitedEdgesSchema } from '@typebot.io/schemas'
|
||||
import { defaultTimeFilter, timeFilterValues } from '../constants'
|
||||
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'
|
||||
|
||||
export const getTotalVisitedEdges = authenticatedProcedure
|
||||
.meta({
|
||||
@@ -18,6 +20,7 @@ export const getTotalVisitedEdges = authenticatedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
@@ -25,7 +28,7 @@ export const getTotalVisitedEdges = authenticatedProcedure
|
||||
totalVisitedEdges: z.array(totalVisitedEdgesSchema),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: canReadTypebots(typebotId, user),
|
||||
select: { id: true },
|
||||
@@ -36,11 +39,18 @@ export const getTotalVisitedEdges = authenticatedProcedure
|
||||
message: 'Published typebot not found',
|
||||
})
|
||||
|
||||
const date = parseDateFromTimeFilter(timeFilter)
|
||||
|
||||
const edges = await prisma.visitedEdge.groupBy({
|
||||
by: ['edgeId'],
|
||||
where: {
|
||||
result: {
|
||||
typebotId: typebot.id,
|
||||
createdAt: date
|
||||
? {
|
||||
gte: date,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
_count: { resultId: true },
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { Stats } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { StatsCards } from './StatsCards'
|
||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||
import { Graph } from '@/features/graph/components/Graph'
|
||||
@@ -15,14 +15,18 @@ import { useTranslate } from '@tolgee/react'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
|
||||
import { defaultTimeFilter, timeFilterValues } from '../constants'
|
||||
|
||||
export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
||||
const { t } = useTranslate()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { typebot, publishedTypebot } = useTypebot()
|
||||
const [timeFilter, setTimeFilter] =
|
||||
useState<(typeof timeFilterValues)[number]>(defaultTimeFilter)
|
||||
const { data } = trpc.analytics.getTotalAnswers.useQuery(
|
||||
{
|
||||
typebotId: typebot?.id as string,
|
||||
timeFilter,
|
||||
},
|
||||
{ enabled: isDefined(publishedTypebot) }
|
||||
)
|
||||
@@ -30,6 +34,7 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
||||
const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery(
|
||||
{
|
||||
typebotId: typebot?.id as string,
|
||||
timeFilter,
|
||||
},
|
||||
{ enabled: isDefined(publishedTypebot) }
|
||||
)
|
||||
@@ -76,7 +81,12 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
||||
type={t('billing.limitMessage.analytics')}
|
||||
excludedPlans={['STARTER']}
|
||||
/>
|
||||
<StatsCards stats={stats} pos="absolute" />
|
||||
<StatsCards
|
||||
stats={stats}
|
||||
pos="absolute"
|
||||
timeFilter={timeFilter}
|
||||
setTimeFilter={setTimeFilter}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { Stats } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { timeFilterLabels, timeFilterValues } from '../constants'
|
||||
|
||||
const computeCompletionRate =
|
||||
(notAvailableLabel: string) =>
|
||||
@@ -20,13 +22,24 @@ const computeCompletionRate =
|
||||
|
||||
export const StatsCards = ({
|
||||
stats,
|
||||
timeFilter,
|
||||
setTimeFilter,
|
||||
...props
|
||||
}: { stats?: Stats } & GridProps) => {
|
||||
}: {
|
||||
stats?: Stats
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
setTimeFilter: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||
} & GridProps) => {
|
||||
const { t } = useTranslate()
|
||||
const bg = useColorModeValue('white', 'gray.900')
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing="6" {...props}>
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, md: 4 }}
|
||||
spacing="6"
|
||||
alignItems="center"
|
||||
{...props}
|
||||
>
|
||||
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
|
||||
<StatLabel>{t('analytics.viewsLabel')}</StatLabel>
|
||||
{stats ? (
|
||||
@@ -56,6 +69,18 @@ export const StatsCards = ({
|
||||
<Skeleton w="50%" h="10px" mt="2" />
|
||||
)}
|
||||
</Stat>
|
||||
<DropdownList
|
||||
items={Object.entries(timeFilterLabels).map(([value, label]) => ({
|
||||
label,
|
||||
value,
|
||||
}))}
|
||||
currentItem={timeFilter}
|
||||
onItemSelect={(val) =>
|
||||
setTimeFilter(val as (typeof timeFilterValues)[number])
|
||||
}
|
||||
backgroundColor="white"
|
||||
boxShadow="md"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
)
|
||||
}
|
||||
|
||||
20
apps/builder/src/features/analytics/constants.ts
Normal file
20
apps/builder/src/features/analytics/constants.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const timeFilterValues = [
|
||||
'today',
|
||||
'last7Days',
|
||||
'last30Days',
|
||||
'yearToDate',
|
||||
'allTime',
|
||||
] as const
|
||||
|
||||
export const timeFilterLabels: Record<
|
||||
(typeof timeFilterValues)[number],
|
||||
string
|
||||
> = {
|
||||
today: 'Today',
|
||||
last7Days: 'Last 7 days',
|
||||
last30Days: 'Last 30 days',
|
||||
yearToDate: 'Year to date',
|
||||
allTime: 'All time',
|
||||
}
|
||||
|
||||
export const defaultTimeFilter = 'today' as const
|
||||
@@ -0,0 +1,18 @@
|
||||
import { timeFilterValues } from '../constants'
|
||||
|
||||
export const parseDateFromTimeFilter = (
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
): Date | undefined => {
|
||||
switch (timeFilter) {
|
||||
case 'today':
|
||||
return new Date(new Date().setHours(0, 0, 0, 0))
|
||||
case 'last7Days':
|
||||
return new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||
case 'last30Days':
|
||||
return new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
case 'yearToDate':
|
||||
return new Date(new Date().getFullYear(), 0, 1)
|
||||
case 'allTime':
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user