🚸 (results) Add time filter to results table as…
This commit is contained in:
@ -79,7 +79,7 @@ export const DropdownList = <T extends Item>({
|
||||
)}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Menu isLazy placement="bottom-end" matchWidth>
|
||||
<Menu isLazy>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
|
@ -213,9 +213,9 @@ export const VariableSearchInput = ({
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
initialFocusRef={inputRef}
|
||||
matchWidth
|
||||
isLazy
|
||||
offset={[0, 2]}
|
||||
placement="auto-start"
|
||||
>
|
||||
<PopoverAnchor>
|
||||
<Input
|
||||
@ -239,6 +239,7 @@ export const VariableSearchInput = ({
|
||||
shadow="lg"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
minW="250px"
|
||||
>
|
||||
{isCreateVariableButtonDisplayed && (
|
||||
<Button
|
||||
|
89
apps/builder/src/features/analytics/api/getStats.ts
Normal file
89
apps/builder/src/features/analytics/api/getStats.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { canReadTypebots } from '@/helpers/databaseRules'
|
||||
import { Stats, statsSchema } from '@typebot.io/schemas'
|
||||
import { defaultTimeFilter, timeFilterValues } from '../constants'
|
||||
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'
|
||||
|
||||
export const getStats = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/v1/typebots/{typebotId}/analytics/stats',
|
||||
protect: true,
|
||||
summary: 'Get results stats',
|
||||
tags: ['Analytics'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
|
||||
})
|
||||
)
|
||||
.output(z.object({ stats: statsSchema }))
|
||||
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: canReadTypebots(typebotId, user),
|
||||
select: { publishedTypebot: true, id: true },
|
||||
})
|
||||
if (!typebot?.publishedTypebot)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Published typebot not found',
|
||||
})
|
||||
|
||||
const date = parseDateFromTimeFilter(timeFilter)
|
||||
|
||||
const [totalViews, totalStarts, totalCompleted] = await prisma.$transaction(
|
||||
[
|
||||
prisma.result.count({
|
||||
where: {
|
||||
typebotId: typebot.id,
|
||||
isArchived: false,
|
||||
createdAt: date
|
||||
? {
|
||||
gte: date,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
prisma.result.count({
|
||||
where: {
|
||||
typebotId: typebot.id,
|
||||
isArchived: false,
|
||||
hasStarted: true,
|
||||
createdAt: date
|
||||
? {
|
||||
gte: date,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
prisma.result.count({
|
||||
where: {
|
||||
typebotId: typebot.id,
|
||||
isArchived: false,
|
||||
isCompleted: true,
|
||||
createdAt: date
|
||||
? {
|
||||
gte: date,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
|
||||
const stats: Stats = {
|
||||
totalViews,
|
||||
totalStarts,
|
||||
totalCompleted,
|
||||
}
|
||||
|
||||
return {
|
||||
stats,
|
||||
}
|
||||
})
|
@ -1,8 +1,10 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { getTotalAnswers } from './getTotalAnswers'
|
||||
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
|
||||
import { getStats } from './getStats'
|
||||
|
||||
export const analyticsRouter = router({
|
||||
getTotalAnswers,
|
||||
getTotalVisitedEdges,
|
||||
getStats,
|
||||
})
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { Stats } from '@typebot.io/schemas'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { StatsCards } from './StatsCards'
|
||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||
import { Graph } from '@/features/graph/components/Graph'
|
||||
@ -15,14 +15,22 @@ 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'
|
||||
import { timeFilterValues } from '../constants'
|
||||
|
||||
export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
||||
type Props = {
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||
stats?: Stats
|
||||
}
|
||||
|
||||
export const AnalyticsGraphContainer = ({
|
||||
timeFilter,
|
||||
onTimeFilterChange,
|
||||
stats,
|
||||
}: Props) => {
|
||||
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,
|
||||
@ -85,7 +93,7 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
||||
stats={stats}
|
||||
pos="absolute"
|
||||
timeFilter={timeFilter}
|
||||
setTimeFilter={setTimeFilter}
|
||||
onTimeFilterChange={onTimeFilterChange}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
|
@ -10,8 +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'
|
||||
import { timeFilterValues } from '../constants'
|
||||
import { TimeFilterDropdown } from './TimeFilterDropdown'
|
||||
|
||||
const computeCompletionRate =
|
||||
(notAvailableLabel: string) =>
|
||||
@ -23,12 +23,12 @@ const computeCompletionRate =
|
||||
export const StatsCards = ({
|
||||
stats,
|
||||
timeFilter,
|
||||
setTimeFilter,
|
||||
onTimeFilterChange,
|
||||
...props
|
||||
}: {
|
||||
stats?: Stats
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
setTimeFilter: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||
} & GridProps) => {
|
||||
const { t } = useTranslate()
|
||||
const bg = useColorModeValue('white', 'gray.900')
|
||||
@ -69,15 +69,9 @@ 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])
|
||||
}
|
||||
<TimeFilterDropdown
|
||||
timeFilter={timeFilter}
|
||||
onTimeFilterChange={onTimeFilterChange}
|
||||
backgroundColor="white"
|
||||
boxShadow="md"
|
||||
/>
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { timeFilterLabels, timeFilterValues } from '../constants'
|
||||
import { ButtonProps } from '@chakra-ui/react'
|
||||
|
||||
type Props = {
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||
} & ButtonProps
|
||||
|
||||
export const TimeFilterDropdown = ({
|
||||
timeFilter,
|
||||
onTimeFilterChange,
|
||||
...props
|
||||
}: Props) => (
|
||||
<DropdownList
|
||||
items={Object.entries(timeFilterLabels).map(([value, label]) => ({
|
||||
label,
|
||||
value,
|
||||
}))}
|
||||
currentItem={timeFilter}
|
||||
onItemSelect={(val) =>
|
||||
onTimeFilterChange(val as (typeof timeFilterValues)[number])
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
@ -14,6 +14,7 @@ import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/consta
|
||||
import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader'
|
||||
import { convertResultsToTableData } from '@typebot.io/lib/results/convertResultsToTableData'
|
||||
import { parseCellContent } from './helpers/parseCellContent'
|
||||
import { timeFilterValues } from '../analytics/constants'
|
||||
|
||||
const resultsContext = createContext<{
|
||||
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
||||
@ -30,11 +31,13 @@ const resultsContext = createContext<{
|
||||
}>({})
|
||||
|
||||
export const ResultsProvider = ({
|
||||
timeFilter,
|
||||
children,
|
||||
typebotId,
|
||||
totalResults,
|
||||
onDeleteResults,
|
||||
}: {
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
children: ReactNode
|
||||
typebotId: string
|
||||
totalResults: number
|
||||
@ -43,6 +46,7 @@ export const ResultsProvider = ({
|
||||
const { publishedTypebot } = useTypebot()
|
||||
const { showToast } = useToast()
|
||||
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
|
||||
timeFilter,
|
||||
typebotId,
|
||||
onError: (error) => {
|
||||
showToast({ description: error })
|
||||
|
@ -4,6 +4,11 @@ import { TRPCError } from '@trpc/server'
|
||||
import { resultWithAnswersSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
||||
import {
|
||||
timeFilterValues,
|
||||
defaultTimeFilter,
|
||||
} from '@/features/analytics/constants'
|
||||
import { parseDateFromTimeFilter } from '@/features/analytics/helpers/parseDateFromTimeFilter'
|
||||
|
||||
const maxLimit = 100
|
||||
|
||||
@ -26,6 +31,7 @@ export const getResults = authenticatedProcedure
|
||||
),
|
||||
limit: z.coerce.number().min(1).max(maxLimit).default(50),
|
||||
cursor: z.string().optional(),
|
||||
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
@ -70,6 +76,9 @@ export const getResults = authenticatedProcedure
|
||||
})
|
||||
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
|
||||
const date = parseDateFromTimeFilter(input.timeFilter)
|
||||
|
||||
const results = await prisma.result.findMany({
|
||||
take: limit + 1,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
@ -77,6 +86,11 @@ export const getResults = authenticatedProcedure
|
||||
typebotId: typebot.id,
|
||||
hasStarted: true,
|
||||
isArchived: false,
|
||||
createdAt: date
|
||||
? {
|
||||
gte: date,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
|
@ -14,11 +14,15 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
import { useStats } from '../hooks/useStats'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { ResultsProvider } from '../ResultsProvider'
|
||||
import { ResultsTableContainer } from './ResultsTableContainer'
|
||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import {
|
||||
defaultTimeFilter,
|
||||
timeFilterValues,
|
||||
} from '@/features/analytics/constants'
|
||||
|
||||
export const ResultsPage = () => {
|
||||
const router = useRouter()
|
||||
@ -32,18 +36,25 @@ export const ResultsPage = () => {
|
||||
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
|
||||
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
|
||||
)
|
||||
const [timeFilter, setTimeFilter] =
|
||||
useState<(typeof timeFilterValues)[number]>(defaultTimeFilter)
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
const { stats, mutate } = useStats({
|
||||
typebotId: publishedTypebot?.typebotId,
|
||||
onError: (err) => showToast({ title: err.name, description: err.message }),
|
||||
})
|
||||
const { data: { stats } = {}, refetch } = trpc.analytics.getStats.useQuery(
|
||||
{
|
||||
typebotId: publishedTypebot?.typebotId as string,
|
||||
timeFilter,
|
||||
},
|
||||
{
|
||||
enabled: !!publishedTypebot,
|
||||
onError: (err) => showToast({ description: err.message }),
|
||||
}
|
||||
)
|
||||
|
||||
const handleDeletedResults = (total: number) => {
|
||||
const handleDeletedResults = () => {
|
||||
if (!stats) return
|
||||
mutate({
|
||||
stats: { ...stats, totalStarts: stats.totalStarts - total },
|
||||
})
|
||||
refetch()
|
||||
}
|
||||
|
||||
if (is404) return <TypebotNotFoundPage />
|
||||
@ -100,14 +111,22 @@ export const ResultsPage = () => {
|
||||
{workspace &&
|
||||
publishedTypebot &&
|
||||
(isAnalytics ? (
|
||||
<AnalyticsGraphContainer stats={stats} />
|
||||
<AnalyticsGraphContainer
|
||||
stats={stats}
|
||||
timeFilter={timeFilter}
|
||||
onTimeFilterChange={setTimeFilter}
|
||||
/>
|
||||
) : (
|
||||
<ResultsProvider
|
||||
timeFilter={timeFilter}
|
||||
typebotId={publishedTypebot.typebotId}
|
||||
totalResults={stats?.totalStarts ?? 0}
|
||||
onDeleteResults={handleDeletedResults}
|
||||
>
|
||||
<ResultsTableContainer />
|
||||
<ResultsTableContainer
|
||||
timeFilter={timeFilter}
|
||||
onTimeFilterChange={setTimeFilter}
|
||||
/>
|
||||
</ResultsProvider>
|
||||
))}
|
||||
</Flex>
|
||||
|
@ -6,8 +6,16 @@ import { useResults } from '../ResultsProvider'
|
||||
import { ResultModal } from './ResultModal'
|
||||
import { ResultsTable } from './table/ResultsTable'
|
||||
import { useRouter } from 'next/router'
|
||||
import { timeFilterValues } from '@/features/analytics/constants'
|
||||
|
||||
export const ResultsTableContainer = () => {
|
||||
type Props = {
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||
}
|
||||
export const ResultsTableContainer = ({
|
||||
timeFilter,
|
||||
onTimeFilterChange,
|
||||
}: Props) => {
|
||||
const { query } = useRouter()
|
||||
const {
|
||||
flatResults: results,
|
||||
@ -61,8 +69,10 @@ export const ResultsTableContainer = () => {
|
||||
data={tableData}
|
||||
onScrollToBottom={fetchNextPage}
|
||||
hasMore={hasNextPage}
|
||||
timeFilter={timeFilter}
|
||||
onLogOpenIndex={handleLogOpenIndex}
|
||||
onResultExpandIndex={handleResultExpandIndex}
|
||||
onTimeFilterChange={onTimeFilterChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
@ -31,12 +31,16 @@ import { IndeterminateCheckbox } from './IndeterminateCheckbox'
|
||||
import { colors } from '@/lib/theme'
|
||||
import { HeaderIcon } from '../HeaderIcon'
|
||||
import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder'
|
||||
import { TimeFilterDropdown } from '@/features/analytics/components/TimeFilterDropdown'
|
||||
import { timeFilterValues } from '@/features/analytics/constants'
|
||||
|
||||
type ResultsTableProps = {
|
||||
resultHeader: ResultHeaderCell[]
|
||||
data: TableData[]
|
||||
hasMore?: boolean
|
||||
preferences?: ResultsTablePreferences
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||
onScrollToBottom: () => void
|
||||
onLogOpenIndex: (index: number) => () => void
|
||||
onResultExpandIndex: (index: number) => () => void
|
||||
@ -47,6 +51,8 @@ export const ResultsTable = ({
|
||||
data,
|
||||
hasMore,
|
||||
preferences,
|
||||
timeFilter,
|
||||
onTimeFilterChange,
|
||||
onScrollToBottom,
|
||||
onLogOpenIndex,
|
||||
onResultExpandIndex,
|
||||
@ -222,6 +228,11 @@ export const ResultsTable = ({
|
||||
onClearSelection={() => setRowSelection({})}
|
||||
/>
|
||||
)}
|
||||
<TimeFilterDropdown
|
||||
timeFilter={timeFilter}
|
||||
onTimeFilterChange={onTimeFilterChange}
|
||||
size="sm"
|
||||
/>
|
||||
<TableSettingsButton
|
||||
resultHeader={resultHeader}
|
||||
columnVisibility={columnsVisibility}
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { timeFilterValues } from '@/features/analytics/constants'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
export const useResultsQuery = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
type Params = {
|
||||
timeFilter: (typeof timeFilterValues)[number]
|
||||
typebotId: string
|
||||
onError?: (error: string) => void
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const useResultsQuery = ({ timeFilter, typebotId, onError }: Params) => {
|
||||
const { data, error, fetchNextPage, hasNextPage, refetch } =
|
||||
trpc.results.getResults.useInfiniteQuery(
|
||||
{
|
||||
timeFilter,
|
||||
typebotId,
|
||||
},
|
||||
{
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { Stats } from '@typebot.io/schemas'
|
||||
import { fetcher } from '@/helpers/fetcher'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export const useStats = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ stats: Stats }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}/analytics/stats` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
stats: data?.stats,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import { canReadTypebots } from '@/helpers/databaseRules'
|
||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
||||
|
||||
// TODO: Delete, as it has been migrated to tRPC
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req, res)
|
||||
if (!user) return notAuthenticated(res)
|
||||
|
@ -8,7 +8,7 @@ const AnalyticsPage = ResultsPage
|
||||
export const getServerSideProps = async (
|
||||
context: GetServerSidePropsContext
|
||||
) => {
|
||||
if (!env.NEXT_PUBLIC_POSTHOG_KEY)
|
||||
if (!env.NEXT_PUBLIC_POSTHOG_KEY || env.NEXT_PUBLIC_E2E_TEST)
|
||||
return {
|
||||
props: {},
|
||||
}
|
||||
|
4
apps/docs/api-reference/analytics/get-stats.mdx
Normal file
4
apps/docs/api-reference/analytics/get-stats.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: 'Get stats'
|
||||
openapi: GET /v1/typebots/{typebotId}/analytics/stats
|
||||
---
|
@ -288,6 +288,10 @@
|
||||
"api-reference/results/list-logs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Analytics",
|
||||
"pages": ["api-reference/analytics/get-stats"]
|
||||
},
|
||||
{
|
||||
"group": "Folder",
|
||||
"pages": [
|
||||
|
@ -2900,6 +2900,131 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/typebots/{typebotId}/analytics/stats": {
|
||||
"get": {
|
||||
"operationId": "analytics-getStats",
|
||||
"summary": "Get results stats",
|
||||
"tags": [
|
||||
"Analytics"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"Authorization": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "typebotId",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "timeFilter",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"today",
|
||||
"last7Days",
|
||||
"last30Days",
|
||||
"yearToDate",
|
||||
"allTime"
|
||||
],
|
||||
"default": "today"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalViews": {
|
||||
"type": "number"
|
||||
},
|
||||
"totalStarts": {
|
||||
"type": "number"
|
||||
},
|
||||
"totalCompleted": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"totalViews",
|
||||
"totalStarts",
|
||||
"totalCompleted"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"stats"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input data",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Authorization not provided",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Insufficient access",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.NOT_FOUND"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/workspaces": {
|
||||
"get": {
|
||||
"operationId": "workspace-listWorkspaces",
|
||||
@ -10851,6 +10976,21 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "timeFilter",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"today",
|
||||
"last7Days",
|
||||
"last30Days",
|
||||
"yearToDate",
|
||||
"allTime"
|
||||
],
|
||||
"default": "today"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -24,11 +24,13 @@ export const answerInputSchema = answerSchema
|
||||
storageUsed: z.number().nullish(),
|
||||
}) satisfies z.ZodType<Prisma.AnswerUncheckedUpdateInput>
|
||||
|
||||
export type Stats = {
|
||||
totalViews: number
|
||||
totalStarts: number
|
||||
totalCompleted: number
|
||||
}
|
||||
export const statsSchema = z.object({
|
||||
totalViews: z.number(),
|
||||
totalStarts: z.number(),
|
||||
totalCompleted: z.number(),
|
||||
})
|
||||
|
||||
export type Stats = z.infer<typeof statsSchema>
|
||||
|
||||
export type Answer = z.infer<typeof answerSchema>
|
||||
|
||||
|
Reference in New Issue
Block a user