From 01bd21c5b4e203cb5bb626f03608019e8de07919 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 8 Jun 2025 22:21:28 -0700 Subject: [PATCH] Updated reports. --- .../[websiteId]/WebsiteFilterButton.tsx | 2 +- .../websites/[websiteId]/WebsiteHeader.tsx | 2 +- .../websites/[websiteId]/WebsiteLayout.tsx | 18 +- .../websites/[websiteId]/WebsiteTabs.tsx | 64 ++++++ .../[websiteId]/realtime/RealtimeHome.tsx | 31 --- .../[websiteId]/reports/ReportsLayout.tsx | 15 ++ .../[websiteId]/reports/ReportsNav.tsx | 81 +++++++ .../{goals => reports/attribution}/Goal.tsx | 0 .../attribution}/GoalAddButton.tsx | 0 .../attribution}/GoalEditForm.tsx | 0 .../attribution}/GoalsPage.tsx | 2 +- .../{goals => reports/attribution}/page.tsx | 0 .../{ => reports}/funnels/Funnel.tsx | 0 .../{ => reports}/funnels/FunnelAddButton.tsx | 0 .../{ => reports}/funnels/FunnelEditForm.tsx | 0 .../{ => reports}/funnels/FunnelsPage.tsx | 12 +- .../{ => reports}/funnels/page.tsx | 0 .../[websiteId]/reports/goals/Goal.tsx | 98 +++++++++ .../reports/goals/GoalAddButton.tsx | 28 +++ .../reports/goals/GoalEditForm.tsx | 114 ++++++++++ .../[websiteId]/reports/goals/GoalsPage.tsx | 34 +++ .../[websiteId]/reports/goals/page.tsx | 12 ++ .../[websiteId]/reports/insights/Goal.tsx | 98 +++++++++ .../reports/insights/GoalAddButton.tsx | 28 +++ .../reports/insights/GoalEditForm.tsx | 114 ++++++++++ .../reports/insights/GoalsPage.tsx | 38 ++++ .../[websiteId]/reports/insights/page.tsx | 12 ++ .../{ => reports}/journeys/Journey.module.css | 0 .../{ => reports}/journeys/Journey.tsx | 2 +- .../{ => reports}/journeys/JourneysPage.tsx | 2 +- .../{ => reports}/journeys/page.tsx | 0 .../websites/[websiteId]/reports/layout.tsx | 21 ++ .../websites/[websiteId]/reports/page.tsx | 3 + .../reports/retention/Retention.tsx | 123 +++++++++++ .../{ => reports}/retention/RetentionPage.tsx | 12 +- .../{ => reports}/retention/page.tsx | 0 .../[websiteId]/reports/revenue/Revenue.tsx | 200 ++++++++++++++++++ .../{ => reports}/revenue/RevenuePage.tsx | 9 +- .../{ => reports}/revenue/RevenueTable.tsx | 0 .../{ => reports}/revenue/page.tsx | 2 +- .../websites/[websiteId]/reports/utm/UTM.tsx | 81 +++++++ .../[websiteId]/{ => reports}/utm/UTMPage.tsx | 9 +- .../[websiteId]/{ => reports}/utm/page.tsx | 0 .../[websiteId]/retention/RetentionTable.tsx | 103 --------- .../[websiteId]/revenue/RevenueView.tsx | 169 --------------- .../websites/[websiteId]/utm/UTMView.tsx | 74 ------- src/app/api/reports/insights/route.ts | 42 +--- src/app/api/reports/retention/route.ts | 12 +- src/app/api/reports/revenue/route.ts | 35 +-- src/app/api/reports/utm/route.ts | 12 +- .../websites/[websiteId]/retention/route.ts | 42 ---- .../api/websites/[websiteId]/revenue/route.ts | 67 ------ src/app/api/websites/[websiteId]/utm/route.ts | 42 ---- src/components/common/Empty.module.css | 12 -- src/components/common/Empty.tsx | 10 +- src/components/common/EmptyPlaceholder.tsx | 21 +- src/components/common/FilterEditForm.tsx | 7 +- src/components/common/LoadingPanel.module.css | 16 -- src/components/common/LoadingPanel.tsx | 14 +- src/components/common/PageBody.tsx | 2 +- src/components/common/PageHeader.tsx | 4 +- src/components/hooks/index.ts | 3 - src/components/hooks/queries/useGoalQuery.ts | 18 -- src/components/hooks/queries/useGoalsQuery.ts | 20 -- .../hooks/queries/useRetentionQuery.ts | 20 -- .../hooks/queries/useRevenueQuery.ts | 39 ---- .../hooks/queries/useRevenueValuesQuery.ts | 16 -- src/components/icons.ts | 1 + src/components/messages.ts | 2 + src/lib/constants.ts | 53 +++++ src/lib/schema.ts | 25 +++ src/queries/sql/reports/getRetention.ts | 40 ++-- src/queries/sql/reports/getRevenue.ts | 52 ++--- src/queries/sql/reports/getRevenueValues.ts | 75 ------- src/queries/sql/reports/getUTM.ts | 38 +--- 75 files changed, 1373 insertions(+), 980 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx delete mode 100644 src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/ReportsLayout.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/ReportsNav.tsx rename src/app/(main)/websites/[websiteId]/{goals => reports/attribution}/Goal.tsx (100%) rename src/app/(main)/websites/[websiteId]/{goals => reports/attribution}/GoalAddButton.tsx (100%) rename src/app/(main)/websites/[websiteId]/{goals => reports/attribution}/GoalEditForm.tsx (100%) rename src/app/(main)/websites/[websiteId]/{goals => reports/attribution}/GoalsPage.tsx (93%) rename src/app/(main)/websites/[websiteId]/{goals => reports/attribution}/page.tsx (100%) rename src/app/(main)/websites/[websiteId]/{ => reports}/funnels/Funnel.tsx (100%) rename src/app/(main)/websites/[websiteId]/{ => reports}/funnels/FunnelAddButton.tsx (100%) rename src/app/(main)/websites/[websiteId]/{ => reports}/funnels/FunnelEditForm.tsx (100%) rename src/app/(main)/websites/[websiteId]/{ => reports}/funnels/FunnelsPage.tsx (85%) rename src/app/(main)/websites/[websiteId]/{ => reports}/funnels/page.tsx (100%) create mode 100644 src/app/(main)/websites/[websiteId]/reports/goals/Goal.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/goals/GoalAddButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/goals/GoalsPage.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/goals/page.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/insights/Goal.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/insights/GoalAddButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/insights/GoalEditForm.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/insights/GoalsPage.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/insights/page.tsx rename src/app/(main)/websites/[websiteId]/{ => reports}/journeys/Journey.module.css (100%) rename src/app/(main)/websites/[websiteId]/{ => reports}/journeys/Journey.tsx (99%) rename src/app/(main)/websites/[websiteId]/{ => reports}/journeys/JourneysPage.tsx (98%) rename src/app/(main)/websites/[websiteId]/{ => reports}/journeys/page.tsx (100%) create mode 100644 src/app/(main)/websites/[websiteId]/reports/layout.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/page.tsx create mode 100644 src/app/(main)/websites/[websiteId]/reports/retention/Retention.tsx rename src/app/(main)/websites/[websiteId]/{ => reports}/retention/RetentionPage.tsx (54%) rename src/app/(main)/websites/[websiteId]/{ => reports}/retention/page.tsx (100%) create mode 100644 src/app/(main)/websites/[websiteId]/reports/revenue/Revenue.tsx rename src/app/(main)/websites/[websiteId]/{ => reports}/revenue/RevenuePage.tsx (56%) rename src/app/(main)/websites/[websiteId]/{ => reports}/revenue/RevenueTable.tsx (100%) rename src/app/(main)/websites/[websiteId]/{ => reports}/revenue/page.tsx (89%) create mode 100644 src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx rename src/app/(main)/websites/[websiteId]/{ => reports}/utm/UTMPage.tsx (57%) rename src/app/(main)/websites/[websiteId]/{ => reports}/utm/page.tsx (100%) delete mode 100644 src/app/(main)/websites/[websiteId]/retention/RetentionTable.tsx delete mode 100644 src/app/(main)/websites/[websiteId]/revenue/RevenueView.tsx delete mode 100644 src/app/(main)/websites/[websiteId]/utm/UTMView.tsx delete mode 100644 src/app/api/websites/[websiteId]/retention/route.ts delete mode 100644 src/app/api/websites/[websiteId]/revenue/route.ts delete mode 100644 src/app/api/websites/[websiteId]/utm/route.ts delete mode 100644 src/components/common/Empty.module.css delete mode 100644 src/components/common/LoadingPanel.module.css delete mode 100644 src/components/hooks/queries/useGoalQuery.ts delete mode 100644 src/components/hooks/queries/useGoalsQuery.ts delete mode 100644 src/components/hooks/queries/useRetentionQuery.ts delete mode 100644 src/components/hooks/queries/useRevenueQuery.ts delete mode 100644 src/components/hooks/queries/useRevenueValuesQuery.ts delete mode 100644 src/queries/sql/reports/getRevenueValues.ts diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index 572112c5..5f4ba943 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -32,7 +32,7 @@ export function WebsiteFilterButton({ return ( - + + + {({ close }) => } + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx b/src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx new file mode 100644 index 00000000..b42545de --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx @@ -0,0 +1,114 @@ +import { + Form, + FormField, + TextField, + Grid, + FormButtons, + FormSubmitButton, + Button, + RadioGroup, + Radio, + Text, + Icon, + Loading, +} from '@umami/react-zen'; +import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks'; +import { File, Lightning } from '@/components/icons'; + +export function GoalEditForm({ + id, + websiteId, + onSave, + onClose, +}: { + id?: string; + websiteId: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { touch } = useModified(); + const { post, useMutation } = useApi(); + const { data } = useReportQuery(id); + const { mutate, error, isPending } = useMutation({ + mutationFn: (params: any) => post(`/reports${id ? `/${id}` : ''}`, params), + }); + + const handleSubmit = async ({ name, ...parameters }) => { + mutate( + { ...data, id, name, type: 'goal', websiteId, parameters }, + { + onSuccess: async () => { + if (id) touch(`report:${id}`); + touch('reports:goal'); + onSave?.(); + onClose?.(); + }, + }, + ); + }; + + if (id && !data) { + return ; + } + + const defaultValues = { + name: data?.name || '', + type: data?.parameters?.type || 'page', + value: data?.parameters?.value || '', + }; + + return ( +
+ {({ watch }) => { + const watchType = watch('type'); + return ( + <> + + + + + + + + + + + {formatMessage(labels.page)} + + + + + + {formatMessage(labels.event)} + + + + + + + + + + {formatMessage(labels.save)} + + + ); + }} +
+ ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/goals/GoalsPage.tsx b/src/app/(main)/websites/[websiteId]/reports/goals/GoalsPage.tsx new file mode 100644 index 00000000..3ee32f15 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/goals/GoalsPage.tsx @@ -0,0 +1,34 @@ +'use client'; +import { Grid, Column } from '@umami/react-zen'; +import { SectionHeader } from '@/components/common/SectionHeader'; +import { Goal } from './Goal'; +import { GoalAddButton } from './GoalAddButton'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { useDateRange, useReportsQuery } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { Panel } from '@/components/common/Panel'; + +export function GoalsPage({ websiteId }: { websiteId: string }) { + const { result } = useReportsQuery({ websiteId, type: 'goal' }); + const { + dateRange: { startDate, endDate }, + } = useDateRange(websiteId); + + return ( + + + + + + + + {result?.data?.map((report: any) => ( + + + + ))} + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/goals/page.tsx b/src/app/(main)/websites/[websiteId]/reports/goals/page.tsx new file mode 100644 index 00000000..e59b2c7c --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/goals/page.tsx @@ -0,0 +1,12 @@ +import { Metadata } from 'next'; +import { GoalsPage } from './GoalsPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Goals', +}; diff --git a/src/app/(main)/websites/[websiteId]/reports/insights/Goal.tsx b/src/app/(main)/websites/[websiteId]/reports/insights/Goal.tsx new file mode 100644 index 00000000..8e2d453a --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/insights/Goal.tsx @@ -0,0 +1,98 @@ +import { Grid, Row, Column, Text, Icon, ProgressBar, Dialog } from '@umami/react-zen'; +import { ReportEditButton } from '@/components/input/ReportEditButton'; +import { useMessages, useResultQuery } from '@/components/hooks'; +import { File, Lightning, User } from '@/components/icons'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { formatLongNumber } from '@/lib/format'; +import { GoalEditForm } from './GoalEditForm'; + +export interface GoalProps { + id: string; + name: string; + type: string; + parameters: { + name: string; + type: string; + value: string; + }; + websiteId: string; + startDate: Date; + endDate: Date; +} + +export type GoalData = { num: number; total: number }; + +export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) { + const { formatMessage, labels } = useMessages(); + const { data, error, isLoading } = useResultQuery(type, { + websiteId, + dateRange: { + startDate, + endDate, + }, + parameters, + }); + const isPage = parameters?.type === 'page'; + + return ( + + + + + + + {name} + + + + + + {({ close }) => { + return ( + + + + ); + }} + + + + + + {formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)} + + {formatMessage(labels.conversionRate)} + + + + {parameters.type === 'page' ? : } + {parameters.value} + + + + + + {`${formatLongNumber( + data?.num, + )} / ${formatLongNumber(data?.total)}`} + + + + + + {data?.total ? Math.round((+data?.num / +data?.total) * 100) : '0'}% + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/insights/GoalAddButton.tsx b/src/app/(main)/websites/[websiteId]/reports/insights/GoalAddButton.tsx new file mode 100644 index 00000000..cdd7d34a --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/insights/GoalAddButton.tsx @@ -0,0 +1,28 @@ +import { Button, DialogTrigger, Dialog, Icon, Text, Modal } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; +import { GoalEditForm } from './GoalEditForm'; +import { Plus } from '@/components/icons'; + +export function GoalAddButton({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + + + + + {({ close }) => } + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/insights/GoalEditForm.tsx b/src/app/(main)/websites/[websiteId]/reports/insights/GoalEditForm.tsx new file mode 100644 index 00000000..b42545de --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/insights/GoalEditForm.tsx @@ -0,0 +1,114 @@ +import { + Form, + FormField, + TextField, + Grid, + FormButtons, + FormSubmitButton, + Button, + RadioGroup, + Radio, + Text, + Icon, + Loading, +} from '@umami/react-zen'; +import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks'; +import { File, Lightning } from '@/components/icons'; + +export function GoalEditForm({ + id, + websiteId, + onSave, + onClose, +}: { + id?: string; + websiteId: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { touch } = useModified(); + const { post, useMutation } = useApi(); + const { data } = useReportQuery(id); + const { mutate, error, isPending } = useMutation({ + mutationFn: (params: any) => post(`/reports${id ? `/${id}` : ''}`, params), + }); + + const handleSubmit = async ({ name, ...parameters }) => { + mutate( + { ...data, id, name, type: 'goal', websiteId, parameters }, + { + onSuccess: async () => { + if (id) touch(`report:${id}`); + touch('reports:goal'); + onSave?.(); + onClose?.(); + }, + }, + ); + }; + + if (id && !data) { + return ; + } + + const defaultValues = { + name: data?.name || '', + type: data?.parameters?.type || 'page', + value: data?.parameters?.value || '', + }; + + return ( +
+ {({ watch }) => { + const watchType = watch('type'); + return ( + <> + + + + + + + + + + + {formatMessage(labels.page)} + + + + + + {formatMessage(labels.event)} + + + + + + + + + + {formatMessage(labels.save)} + + + ); + }} +
+ ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/insights/GoalsPage.tsx b/src/app/(main)/websites/[websiteId]/reports/insights/GoalsPage.tsx new file mode 100644 index 00000000..522d984d --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/insights/GoalsPage.tsx @@ -0,0 +1,38 @@ +'use client'; +import { Grid, Loading } from '@umami/react-zen'; +import { SectionHeader } from '@/components/common/SectionHeader'; +import { Goal } from './Goal'; +import { GoalAddButton } from './GoalAddButton'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { useDateRange, useReportsQuery } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { Panel } from '@/components/common/Panel'; + +export function GoalsPage({ websiteId }: { websiteId: string }) { + const { result } = useReportsQuery({ websiteId, type: 'goal' }); + const { + dateRange: { startDate, endDate }, + } = useDateRange(websiteId); + + if (!result) { + return ; + } + + return ( + <> + + + + + + + {result?.data?.map((report: any) => ( + + + + ))} + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/insights/page.tsx b/src/app/(main)/websites/[websiteId]/reports/insights/page.tsx new file mode 100644 index 00000000..e59b2c7c --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/insights/page.tsx @@ -0,0 +1,12 @@ +import { Metadata } from 'next'; +import { GoalsPage } from './GoalsPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Goals', +}; diff --git a/src/app/(main)/websites/[websiteId]/journeys/Journey.module.css b/src/app/(main)/websites/[websiteId]/reports/journeys/Journey.module.css similarity index 100% rename from src/app/(main)/websites/[websiteId]/journeys/Journey.module.css rename to src/app/(main)/websites/[websiteId]/reports/journeys/Journey.module.css diff --git a/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx b/src/app/(main)/websites/[websiteId]/reports/journeys/Journey.tsx similarity index 99% rename from src/app/(main)/websites/[websiteId]/journeys/Journey.tsx rename to src/app/(main)/websites/[websiteId]/reports/journeys/Journey.tsx index 8cff3207..72e138ca 100644 --- a/src/app/(main)/websites/[websiteId]/journeys/Journey.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/journeys/Journey.tsx @@ -166,7 +166,7 @@ export function Journey({ }; return ( - +
{columns.map(({ visitorCount, nodes }, columnIndex) => { diff --git a/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/reports/journeys/JourneysPage.tsx similarity index 98% rename from src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx rename to src/app/(main)/websites/[websiteId]/reports/journeys/JourneysPage.tsx index 93bdf9b1..d7582822 100644 --- a/src/app/(main)/websites/[websiteId]/journeys/JourneysPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/journeys/JourneysPage.tsx @@ -19,7 +19,7 @@ export function JourneysPage({ websiteId }: { websiteId: string }) { const [endStep, setEndStep] = useState(''); return ( - + + {CURRENCIES.map(({ id, name }) => { + if (search && !`${id}${name}`.toLowerCase().includes(search)) { + return null; + } + + return ( + + {id} — {name} + + ); + }).filter(n => n)} + + + + + + + + {metricData?.map(({ label, value, formatValue }) => { + return ( + + ); + })} + + + {data && ( + <> + + + + + + ({ + x: name, + y: Number(value), + z: (value / data?.total.sum) * 100, + }))} + renderLabel={renderCountryName} + /> + + + + + )} + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/revenue/RevenuePage.tsx b/src/app/(main)/websites/[websiteId]/reports/revenue/RevenuePage.tsx similarity index 56% rename from src/app/(main)/websites/[websiteId]/revenue/RevenuePage.tsx rename to src/app/(main)/websites/[websiteId]/reports/revenue/RevenuePage.tsx index 0c2a0c6c..4c5998d5 100644 --- a/src/app/(main)/websites/[websiteId]/revenue/RevenuePage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/revenue/RevenuePage.tsx @@ -1,13 +1,18 @@ 'use client'; import { Column } from '@umami/react-zen'; -import { RevenueView } from './RevenueView'; +import { Revenue } from './Revenue'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { useDateRange } from '@/components/hooks'; export function RevenuePage({ websiteId }: { websiteId: string }) { + const { + dateRange: { startDate, endDate }, + } = useDateRange(websiteId); + return ( - + ); } diff --git a/src/app/(main)/websites/[websiteId]/revenue/RevenueTable.tsx b/src/app/(main)/websites/[websiteId]/reports/revenue/RevenueTable.tsx similarity index 100% rename from src/app/(main)/websites/[websiteId]/revenue/RevenueTable.tsx rename to src/app/(main)/websites/[websiteId]/reports/revenue/RevenueTable.tsx diff --git a/src/app/(main)/websites/[websiteId]/revenue/page.tsx b/src/app/(main)/websites/[websiteId]/reports/revenue/page.tsx similarity index 89% rename from src/app/(main)/websites/[websiteId]/revenue/page.tsx rename to src/app/(main)/websites/[websiteId]/reports/revenue/page.tsx index 485d3f25..4bc56331 100644 --- a/src/app/(main)/websites/[websiteId]/revenue/page.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/revenue/page.tsx @@ -8,5 +8,5 @@ export default async function ({ params }: { params: Promise<{ websiteId: string } export const metadata: Metadata = { - title: 'Revenue UTM Parameters', + title: 'Revenue', }; diff --git a/src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx b/src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx new file mode 100644 index 00000000..02d0b100 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/utm/UTM.tsx @@ -0,0 +1,81 @@ +import { Grid, Column, Heading, Text } from '@umami/react-zen'; +import { firstBy } from 'thenby'; +import { CHART_COLORS, UTM_PARAMS } from '@/lib/constants'; +import { useResultQuery } from '@/components/hooks'; +import { PieChart } from '@/components/charts/PieChart'; +import { ListTable } from '@/components/metrics/ListTable'; +import { useMessages } from '@/components/hooks'; +import { Panel } from '@/components/common/Panel'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; + +export interface UTMProps { + websiteId: string; + startDate: Date; + endDate: Date; +} + +export function UTM({ websiteId, startDate, endDate }: UTMProps) { + const { formatMessage, labels } = useMessages(); + const { data, error, isLoading } = useResultQuery('utm', { + websiteId, + dateRange: { + startDate, + endDate, + }, + }); + const isEmpty = !Object.keys(data || {})?.length; + + return ( + + + {UTM_PARAMS.map(param => { + const items = toArray(data?.[param]); + const chartData = { + labels: items.map(({ name }) => name), + datasets: [ + { + data: items.map(({ value }) => value), + backgroundColor: CHART_COLORS, + borderWidth: 0, + }, + ], + }; + const total = items.reduce((sum, { value }) => { + return +sum + +value; + }, 0); + + return ( + + + + + {param.replace(/^utm_/, '')} + + ({ + x: name, + y: value, + z: (value / total) * 100, + }))} + /> + + + + + + + ); + })} + + + ); +} + +function toArray(data: { [key: string]: number } = {}) { + return Object.keys(data) + .map(key => { + return { name: key, value: data[key] }; + }) + .sort(firstBy('value', -1)); +} diff --git a/src/app/(main)/websites/[websiteId]/utm/UTMPage.tsx b/src/app/(main)/websites/[websiteId]/reports/utm/UTMPage.tsx similarity index 57% rename from src/app/(main)/websites/[websiteId]/utm/UTMPage.tsx rename to src/app/(main)/websites/[websiteId]/reports/utm/UTMPage.tsx index 8fb6cab5..084b73b7 100644 --- a/src/app/(main)/websites/[websiteId]/utm/UTMPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/utm/UTMPage.tsx @@ -1,13 +1,18 @@ 'use client'; import { Column } from '@umami/react-zen'; -import { UTMView } from './UTMView'; +import { useDateRange } from '@/components/hooks'; +import { UTM } from './UTM'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; export function UTMPage({ websiteId }: { websiteId: string }) { + const { + dateRange: { startDate, endDate }, + } = useDateRange(websiteId); + return ( - + ); } diff --git a/src/app/(main)/websites/[websiteId]/utm/page.tsx b/src/app/(main)/websites/[websiteId]/reports/utm/page.tsx similarity index 100% rename from src/app/(main)/websites/[websiteId]/utm/page.tsx rename to src/app/(main)/websites/[websiteId]/reports/utm/page.tsx diff --git a/src/app/(main)/websites/[websiteId]/retention/RetentionTable.tsx b/src/app/(main)/websites/[websiteId]/retention/RetentionTable.tsx deleted file mode 100644 index aeecc26a..00000000 --- a/src/app/(main)/websites/[websiteId]/retention/RetentionTable.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { ReactNode } from 'react'; -import { Grid, Row, Column, Text, Loading, Icon } from '@umami/react-zen'; -import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder'; -import { Users } from '@/components/icons'; -import { useMessages, useLocale, useRetentionQuery } from '@/components/hooks'; -import { formatDate } from '@/lib/date'; -import { formatLongNumber } from '@/lib/format'; - -const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28]; - -export function RetentionTable({ websiteId, days = DAYS }: { websiteId: string; days?: number[] }) { - const { formatMessage, labels } = useMessages(); - const { locale } = useLocale(); - const { data: x, isLoading } = useRetentionQuery(websiteId); - const data = x as any; - - if (isLoading) { - return ; - } - - if (!data) { - return ; - } - - const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => { - const { date, visitors, day } = row; - if (day === 0) { - return arr.concat({ - date, - visitors, - records: days - .reduce((arr, day) => { - arr[day] = data.find(x => x.date === date && x.day === day); - return arr; - }, []) - .filter(n => n), - }); - } - return arr; - }, []); - - const totalDays = rows.length; - - return ( - - - - {formatMessage(labels.cohort)} - - {days.map(n => ( - - - {formatMessage(labels.day)} {n} - - - ))} - - {rows.map(({ date, visitors, records }: any, rowIndex: number) => { - return ( - - - {formatDate(date, 'PP', locale)} - - - - - {formatLongNumber(visitors)} - - - {days.map(day => { - if (totalDays - rowIndex < day) { - return null; - } - const percentage = records.filter(a => a.day === day)[0]?.percentage; - return {percentage ? `${Number(percentage).toFixed(2)}%` : ''}; - })} - - ); - })} - - ); -} - -const Cell = ({ children }: { children: ReactNode }) => { - return ( - - {children} - - ); -}; diff --git a/src/app/(main)/websites/[websiteId]/revenue/RevenueView.tsx b/src/app/(main)/websites/[websiteId]/revenue/RevenueView.tsx deleted file mode 100644 index 4c19a693..00000000 --- a/src/app/(main)/websites/[websiteId]/revenue/RevenueView.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import classNames from 'classnames'; -import { colord } from 'colord'; -import { BarChart } from '@/components/charts/BarChart'; -import { PieChart } from '@/components/charts/PieChart'; -import { TypeIcon } from '@/components/common/TypeIcon'; -import { - useCountryNames, - useLocale, - useMessages, - useRevenueQuery, - useDateRange, -} from '@/components/hooks'; -import { GridRow } from '@/components/common/GridRow'; -import { ListTable } from '@/components/metrics/ListTable'; -import { MetricCard } from '@/components/metrics/MetricCard'; -import { MetricsBar } from '@/components/metrics/MetricsBar'; -import { renderDateLabels } from '@/lib/charts'; -import { CHART_COLORS } from '@/lib/constants'; -import { formatLongCurrency, formatLongNumber } from '@/lib/format'; -import { useCallback, useMemo } from 'react'; -import { RevenueTable } from './RevenueTable'; -import { Panel } from '@/components/common/Panel'; -import { Column } from '@umami/react-zen'; - -export interface RevenueViewProps { - websiteId: string; - isLoading?: boolean; -} - -export function RevenueView({ websiteId, isLoading }: RevenueViewProps) { - const { formatMessage, labels } = useMessages(); - const { locale } = useLocale(); - const { countryNames } = useCountryNames(locale); - - const { data } = useRevenueQuery(websiteId); - const currency = 'USD'; - const { dateRange } = useDateRange(websiteId); - - const renderCountryName = useCallback( - ({ x: code }) => ( - - - {countryNames[code]} - - ), - [countryNames, locale], - ); - - const chartData = useMemo(() => { - if (!data) return []; - - const map = (data.chart as any[]).reduce((obj, { x, t, y }) => { - if (!obj[x]) { - obj[x] = []; - } - - obj[x].push({ x: t, y }); - - return obj; - }, {}); - - return { - datasets: Object.keys(map).map((key, index) => { - const color = colord(CHART_COLORS[index % CHART_COLORS.length]); - return { - label: key, - data: map[key], - lineTension: 0, - backgroundColor: color.alpha(0.6).toRgbString(), - borderColor: color.alpha(0.7).toRgbString(), - borderWidth: 1, - }; - }), - }; - }, [data]); - - const countryData = useMemo(() => { - if (!data) return []; - - const labels = data.country.map(({ name }) => name); - const datasets = [ - { - data: data.country.map(({ value }) => value), - backgroundColor: CHART_COLORS, - borderWidth: 0, - }, - ]; - - return { labels, datasets }; - }, [data]); - - const metricData = useMemo(() => { - if (!data) return []; - - const { sum, count, unique_count } = data.total; - - return [ - { - value: sum, - label: formatMessage(labels.total), - formatValue: n => formatLongCurrency(n, currency), - }, - { - value: count ? sum / count : 0, - label: formatMessage(labels.average), - formatValue: n => formatLongCurrency(n, currency), - }, - { - value: count, - label: formatMessage(labels.transactions), - formatValue: formatLongNumber, - }, - { - value: unique_count, - label: formatMessage(labels.uniqueCustomers), - formatValue: formatLongNumber, - }, - ] as any; - }, [data, locale]); - - return ( - <> - - - - {metricData?.map(({ label, value, formatValue }) => { - return ( - - ); - })} - - - {data && ( - <> - - - - - - ({ - x: name, - y: Number(value), - z: (value / data?.total.sum) * 100, - }))} - renderLabel={renderCountryName} - /> - - - - - )} - - - - - - ); -} diff --git a/src/app/(main)/websites/[websiteId]/utm/UTMView.tsx b/src/app/(main)/websites/[websiteId]/utm/UTMView.tsx deleted file mode 100644 index 8548695e..00000000 --- a/src/app/(main)/websites/[websiteId]/utm/UTMView.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Column, Heading, Text, Loading } from '@umami/react-zen'; -import { firstBy } from 'thenby'; -import { CHART_COLORS, UTM_PARAMS } from '@/lib/constants'; -import { useUTMQuery } from '@/components/hooks'; -import { PieChart } from '@/components/charts/PieChart'; -import { ListTable } from '@/components/metrics/ListTable'; -import { useMessages } from '@/components/hooks'; -import { Panel } from '@/components/common/Panel'; -import { GridRow } from '@/components/common/GridRow'; - -function toArray(data: { [key: string]: number } = {}) { - return Object.keys(data) - .map(key => { - return { name: key, value: data[key] }; - }) - .sort(firstBy('value', -1)); -} - -export function UTMView({ websiteId }: { websiteId: string }) { - const { formatMessage, labels } = useMessages(); - const { data, isLoading } = useUTMQuery(websiteId); - - if (isLoading) { - return ; - } - - if (!data) { - return null; - } - - return ( - - {UTM_PARAMS.map(param => { - const items = toArray(data[param]); - const chartData = { - labels: items.map(({ name }) => name), - datasets: [ - { - data: items.map(({ value }) => value), - backgroundColor: CHART_COLORS, - borderWidth: 0, - }, - ], - }; - const total = items.reduce((sum, { value }) => { - return +sum + +value; - }, 0); - - return ( - - - - - {param.replace(/^utm_/, '')} - - ({ - x: name, - y: value, - z: (value / total) * 100, - }))} - /> - - - - - - - ); - })} - - ); -} diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts index b3569cba..a49db1fa 100644 --- a/src/app/api/reports/insights/route.ts +++ b/src/app/api/reports/insights/route.ts @@ -1,41 +1,11 @@ -import { z } from 'zod'; import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; import { parseRequest } from '@/lib/request'; import { getInsights } from '@/queries'; -import { reportParms } from '@/lib/schema'; - -function convertFilters(filters: any[]) { - return filters.reduce((obj, filter) => { - obj[filter.name] = filter; - - return obj; - }, {}); -} +import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { - const schema = z.object({ - ...reportParms, - fields: z - .array( - z.object({ - name: z.string(), - type: z.string(), - label: z.string(), - }), - ) - .min(1), - filters: z.array( - z.object({ - name: z.string(), - type: z.string(), - operator: z.string(), - value: z.string(), - }), - ), - }); - - const { auth, body, error } = await parseRequest(request, schema); + const { auth, body, error } = await parseRequest(request, reportResultSchema); if (error) { return error(); @@ -60,3 +30,11 @@ export async function POST(request: Request) { return json(data); } + +function convertFilters(filters: any[]) { + return filters.reduce((obj, filter) => { + obj[filter.name] = filter; + + return obj; + }, {}); +} diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts index 83220bb4..04842121 100644 --- a/src/app/api/reports/retention/route.ts +++ b/src/app/api/reports/retention/route.ts @@ -1,17 +1,11 @@ -import { z } from 'zod'; import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; import { parseRequest } from '@/lib/request'; import { getRetention } from '@/queries'; -import { reportParms, timezoneParam } from '@/lib/schema'; +import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { - const schema = z.object({ - ...reportParms, - timezone: timezoneParam, - }); - - const { auth, body, error } = await parseRequest(request, schema); + const { auth, body, error } = await parseRequest(request, reportResultSchema); if (error) { return error(); @@ -20,7 +14,6 @@ export async function POST(request: Request) { const { websiteId, dateRange: { startDate, endDate }, - timezone, } = body; if (!(await canViewWebsite(auth, websiteId))) { @@ -30,7 +23,6 @@ export async function POST(request: Request) { const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), - timezone, }); return json(data); diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts index 13a34f38..15d7a5fd 100644 --- a/src/app/api/reports/revenue/route.ts +++ b/src/app/api/reports/revenue/route.ts @@ -1,40 +1,11 @@ -import { z } from 'zod'; import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; import { parseRequest } from '@/lib/request'; -import { reportParms, timezoneParam } from '@/lib/schema'; +import { reportResultSchema } from '@/lib/schema'; import { getRevenue } from '@/queries/sql/reports/getRevenue'; -import { getRevenueValues } from '@/queries/sql/reports/getRevenueValues'; - -export async function GET(request: Request) { - const { auth, query, error } = await parseRequest(request); - - if (error) { - return error(); - } - - const { websiteId, startDate, endDate } = query; - - if (!(await canViewWebsite(auth, websiteId))) { - return unauthorized(); - } - - const data = await getRevenueValues(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return json(data); -} export async function POST(request: Request) { - const schema = z.object({ - currency: z.string(), - ...reportParms, - timezone: timezoneParam, - }); - - const { auth, body, error } = await parseRequest(request, schema); + const { auth, body, error } = await parseRequest(request, reportResultSchema); if (error) { return error(); @@ -43,7 +14,6 @@ export async function POST(request: Request) { const { websiteId, currency, - timezone, dateRange: { startDate, endDate, unit }, } = body; @@ -55,7 +25,6 @@ export async function POST(request: Request) { startDate: new Date(startDate), endDate: new Date(endDate), unit, - timezone, currency, }); diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts index 38e88a6d..36a434ad 100644 --- a/src/app/api/reports/utm/route.ts +++ b/src/app/api/reports/utm/route.ts @@ -1,16 +1,11 @@ -import { z } from 'zod'; import { canViewWebsite } from '@/lib/auth'; import { unauthorized, json } from '@/lib/response'; import { parseRequest } from '@/lib/request'; import { getUTM } from '@/queries'; -import { reportParms } from '@/lib/schema'; +import { reportResultSchema } from '@/lib/schema'; export async function POST(request: Request) { - const schema = z.object({ - ...reportParms, - }); - - const { auth, body, error } = await parseRequest(request, schema); + const { auth, body, error } = await parseRequest(request, reportResultSchema); if (error) { return error(); @@ -18,7 +13,7 @@ export async function POST(request: Request) { const { websiteId, - dateRange: { startDate, endDate, timezone }, + dateRange: { startDate, endDate }, } = body; if (!(await canViewWebsite(auth, websiteId))) { @@ -28,7 +23,6 @@ export async function POST(request: Request) { const data = await getUTM(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), - timezone, }); return json(data); diff --git a/src/app/api/websites/[websiteId]/retention/route.ts b/src/app/api/websites/[websiteId]/retention/route.ts deleted file mode 100644 index 2f8bde18..00000000 --- a/src/app/api/websites/[websiteId]/retention/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { z } from 'zod'; -import { canViewWebsite } from '@/lib/auth'; -import { unauthorized, json } from '@/lib/response'; -import { getRequestDateRange, parseRequest } from '@/lib/request'; -import { getRetention } from '@/queries'; -import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; - -export async function GET( - request: Request, - { params }: { params: Promise<{ websiteId: string }> }, -) { - const schema = z.object({ - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), - unit: unitParam, - timezone: timezoneParam, - ...filterParams, - }); - - const { auth, query, error } = await parseRequest(request, schema); - - if (error) { - return error(); - } - - const { websiteId } = await params; - const { timezone } = query; - - if (!(await canViewWebsite(auth, websiteId))) { - return unauthorized(); - } - - const { startDate, endDate } = await getRequestDateRange(query); - - const data = await getRetention(websiteId, { - startDate, - endDate, - timezone, - }); - - return json(data); -} diff --git a/src/app/api/websites/[websiteId]/revenue/route.ts b/src/app/api/websites/[websiteId]/revenue/route.ts deleted file mode 100644 index 11a6e5fc..00000000 --- a/src/app/api/websites/[websiteId]/revenue/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { z } from 'zod'; -import { canViewWebsite } from '@/lib/auth'; -import { unauthorized, json } from '@/lib/response'; -import { getRequestDateRange, parseRequest } from '@/lib/request'; -import { filterParams, unitParam, timezoneParam } from '@/lib/schema'; -import { getRevenue } from '@/queries/sql/reports/getRevenue'; -import { getRevenueValues } from '@/queries/sql/reports/getRevenueValues'; - -export async function __GET(request: Request) { - const { auth, query, error } = await parseRequest(request); - - if (error) { - return error(); - } - - const { websiteId, startDate, endDate } = query; - - if (!(await canViewWebsite(auth, websiteId))) { - return unauthorized(); - } - - const data = await getRevenueValues(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return json(data); -} - -export async function GET( - request: Request, - { params }: { params: Promise<{ websiteId: string }> }, -) { - const schema = z.object({ - currency: z.string(), - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), - unit: unitParam, - timezone: timezoneParam, - ...filterParams, - }); - - const { auth, query, error } = await parseRequest(request, schema); - - if (error) { - return error(); - } - - const { websiteId } = await params; - const { currency, timezone, unit } = query; - - if (!(await canViewWebsite(auth, websiteId))) { - return unauthorized(); - } - - const { startDate, endDate } = await getRequestDateRange(query); - - const data = await getRevenue(websiteId, { - startDate, - endDate, - unit, - timezone, - currency, - }); - - return json(data); -} diff --git a/src/app/api/websites/[websiteId]/utm/route.ts b/src/app/api/websites/[websiteId]/utm/route.ts deleted file mode 100644 index 9cebb144..00000000 --- a/src/app/api/websites/[websiteId]/utm/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { z } from 'zod'; -import { canViewWebsite } from '@/lib/auth'; -import { unauthorized, json } from '@/lib/response'; -import { getRequestDateRange, parseRequest } from '@/lib/request'; -import { getUTM } from '@/queries'; -import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; - -export async function GET( - request: Request, - { params }: { params: Promise<{ websiteId: string }> }, -) { - const schema = z.object({ - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), - unit: unitParam, - timezone: timezoneParam, - ...filterParams, - }); - - const { auth, query, error } = await parseRequest(request, schema); - - if (error) { - return error(); - } - - const { websiteId } = await params; - const { timezone } = query; - - if (!(await canViewWebsite(auth, websiteId))) { - return unauthorized(); - } - - const { startDate, endDate } = await getRequestDateRange(query); - - const data = await getUTM(websiteId, { - startDate, - endDate, - timezone, - }); - - return json(data); -} diff --git a/src/components/common/Empty.module.css b/src/components/common/Empty.module.css deleted file mode 100644 index 3dccb68e..00000000 --- a/src/components/common/Empty.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.container { - color: var(--base500); - font-size: var(--font-size-md); - position: relative; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - width: 100%; - height: 100%; - min-height: 70px; -} diff --git a/src/components/common/Empty.tsx b/src/components/common/Empty.tsx index f1661e2d..6d84b3e6 100644 --- a/src/components/common/Empty.tsx +++ b/src/components/common/Empty.tsx @@ -1,18 +1,16 @@ -import classNames from 'classnames'; +import { Row } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; -import styles from './Empty.module.css'; export interface EmptyProps { message?: string; - className?: string; } -export function Empty({ message, className }: EmptyProps) { +export function Empty({ message }: EmptyProps) { const { formatMessage, messages } = useMessages(); return ( -
+ {message || formatMessage(messages.noDataAvailable)} -
+ ); } diff --git a/src/components/common/EmptyPlaceholder.tsx b/src/components/common/EmptyPlaceholder.tsx index e13e02ca..16f6cb90 100644 --- a/src/components/common/EmptyPlaceholder.tsx +++ b/src/components/common/EmptyPlaceholder.tsx @@ -1,19 +1,28 @@ import { ReactNode } from 'react'; import { Icon, Text, Column } from '@umami/react-zen'; -import { Logo } from '@/components/icons'; export interface EmptyPlaceholderProps { - message?: string; + title?: string; + description?: string; icon?: ReactNode; children?: ReactNode; } -export function EmptyPlaceholder({ message, icon, children }: EmptyPlaceholderProps) { +export function EmptyPlaceholder({ title, description, icon, children }: EmptyPlaceholderProps) { return ( - {icon || } - {message} -
{children}
+ {icon && ( + + {icon} + + )} + {title && ( + + {title} + + )} + {description && {description}} + {children}
); } diff --git a/src/components/common/FilterEditForm.tsx b/src/components/common/FilterEditForm.tsx index c15e6ff0..a8b5d063 100644 --- a/src/components/common/FilterEditForm.tsx +++ b/src/components/common/FilterEditForm.tsx @@ -1,7 +1,8 @@ import { useState, Key } from 'react'; -import { Grid, Row, Column, Label, List, ListItem, Button, Heading, Text } from '@umami/react-zen'; +import { Grid, Row, Column, Label, List, ListItem, Button, Heading } from '@umami/react-zen'; import { useFilters, useMessages } from '@/components/hooks'; import { FilterRecord } from '@/components/common/FilterRecord'; +import { Empty } from '@/components/common/Empty'; export interface FilterEditFormProps { websiteId?: string; @@ -11,7 +12,7 @@ export interface FilterEditFormProps { } export function FilterEditForm({ data = [], onChange, onClose }: FilterEditFormProps) { - const { formatMessage, labels } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const [filters, setFilters] = useState(data); const { fields } = useFilters(); @@ -72,7 +73,7 @@ export function FilterEditForm({ data = [], onChange, onClose }: FilterEditFormP /> ); })} - {!filters.length && {formatMessage(labels.none)}} + {!filters.length && }
diff --git a/src/components/common/LoadingPanel.module.css b/src/components/common/LoadingPanel.module.css deleted file mode 100644 index 00d6cbb4..00000000 --- a/src/components/common/LoadingPanel.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.panel { - display: flex; - flex-direction: column; - position: relative; - flex: 1; - height: 100%; -} - -.loading { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; -} diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx index dfb2dabf..4f5be375 100644 --- a/src/components/common/LoadingPanel.tsx +++ b/src/components/common/LoadingPanel.tsx @@ -1,9 +1,7 @@ import { ReactNode } from 'react'; -import classNames from 'classnames'; -import { Spinner, Dots } from '@umami/react-zen'; +import { Spinner, Dots, Column, type ColumnProps } from '@umami/react-zen'; import { ErrorMessage } from '@/components/common/ErrorMessage'; import { Empty } from '@/components/common/Empty'; -import styles from './LoadingPanel.module.css'; export function LoadingPanel({ error, @@ -12,25 +10,23 @@ export function LoadingPanel({ isLoading, loadingIcon = 'dots', renderEmpty = () => , - className, children, + ...props }: { - data?: any; error?: Error; isEmpty?: boolean; isFetched?: boolean; isLoading?: boolean; loadingIcon?: 'dots' | 'spinner'; renderEmpty?: () => ReactNode; - className?: string; children: ReactNode; -}) { +} & ColumnProps) { return ( -
+ {isLoading && !isFetched && (loadingIcon === 'dots' ? : )} {error && } {!error && !isLoading && isEmpty && renderEmpty()} {!error && !isLoading && !isEmpty && children} -
+ ); } diff --git a/src/components/common/PageBody.tsx b/src/components/common/PageBody.tsx index 3ccb6058..638ff610 100644 --- a/src/components/common/PageBody.tsx +++ b/src/components/common/PageBody.tsx @@ -4,7 +4,7 @@ import { AlertBanner, Loading, Column } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; export function PageBody({ - maxWidth = '1600px', + maxWidth = '1320px', error, isLoading, children, diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index 83f9ff7f..26cf3c54 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -5,11 +5,13 @@ export function PageHeader({ title, description, icon, + showBorder = true, children, }: { title: string; description?: string; icon?: ReactNode; + showBorder?: boolean; allowEdit?: boolean; className?: string; children?: ReactNode; @@ -19,7 +21,7 @@ export function PageHeader({ justifyContent="space-between" alignItems="center" paddingY="6" - border="bottom" + border={showBorder ? 'bottom' : undefined} width="100%" > diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 72d20ccd..b815a28e 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -3,14 +3,11 @@ export * from './queries/useActiveUsersQuery'; export * from './queries/useEventDataEventsQuery'; export * from './queries/useEventDataPropertiesQuery'; export * from './queries/useEventDataValuesQuery'; -export * from './queries/useGoalsQuery'; export * from './queries/useLoginQuery'; export * from './queries/useRealtimeQuery'; export * from './queries/useResultQuery'; export * from './queries/useReportQuery'; export * from './queries/useReportsQuery'; -export * from './queries/useRetentionQuery'; -export * from './queries/useRevenueQuery'; export * from './queries/useSessionActivityQuery'; export * from './queries/useSessionDataQuery'; export * from './queries/useSessionDataPropertiesQuery'; diff --git a/src/components/hooks/queries/useGoalQuery.ts b/src/components/hooks/queries/useGoalQuery.ts deleted file mode 100644 index 5a65b986..00000000 --- a/src/components/hooks/queries/useGoalQuery.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useApi } from '../useApi'; -import { usePagedQuery } from '../usePagedQuery'; - -export function useGoalQuery( - { websiteId, reportId }: { websiteId: string; reportId: string }, - params?: { [key: string]: string | number }, -) { - const { post } = useApi(); - - return usePagedQuery({ - queryKey: ['goal', { websiteId, reportId, ...params }], - queryFn: () => { - return post(`/reports/goals`, { - ...params, - }); - }, - }); -} diff --git a/src/components/hooks/queries/useGoalsQuery.ts b/src/components/hooks/queries/useGoalsQuery.ts deleted file mode 100644 index 307353da..00000000 --- a/src/components/hooks/queries/useGoalsQuery.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useApi } from '../useApi'; -import { usePagedQuery } from '../usePagedQuery'; -import { useModified } from '../useModified'; - -export function useGoalsQuery( - { websiteId }: { websiteId: string }, - params?: { [key: string]: string | number }, -) { - const { get } = useApi(); - const { modified } = useModified(`goals`); - - return usePagedQuery({ - queryKey: ['goals', { websiteId, modified, ...params }], - queryFn: () => { - return get(`/websites/${websiteId}/goals`, { - ...params, - }); - }, - }); -} diff --git a/src/components/hooks/queries/useRetentionQuery.ts b/src/components/hooks/queries/useRetentionQuery.ts deleted file mode 100644 index eadd8020..00000000 --- a/src/components/hooks/queries/useRetentionQuery.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; -import { UseQueryOptions } from '@tanstack/react-query'; - -export function useRetentionQuery( - websiteId: string, - queryParams?: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number }, - options?: Omit void }, 'queryKey' | 'queryFn'>, -) { - const { get, useQuery } = useApi(); - const filterParams = useFilterParams(websiteId); - - return useQuery({ - queryKey: ['retention', websiteId, { ...filterParams, ...queryParams }], - queryFn: () => - get(`/websites/${websiteId}/retention`, { websiteId, ...filterParams, ...queryParams }), - enabled: !!websiteId, - ...options, - }); -} diff --git a/src/components/hooks/queries/useRevenueQuery.ts b/src/components/hooks/queries/useRevenueQuery.ts deleted file mode 100644 index 34f59f9d..00000000 --- a/src/components/hooks/queries/useRevenueQuery.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useApi } from '../useApi'; -import { useFilterParams } from '../useFilterParams'; -import { UseQueryOptions } from '@tanstack/react-query'; - -export interface RevenueData { - chart: any[]; - country: any[]; - total: { - sum: number; - count: number; - unique_count: number; - }; - table: any[]; -} - -export function useRevenueQuery( - websiteId: string, - queryParams?: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number }, - options?: Omit< - UseQueryOptions & { onDataLoad?: (data: any) => void }, - 'queryKey' | 'queryFn' - >, -) { - const { get, useQuery } = useApi(); - const filterParams = useFilterParams(websiteId); - const currency = 'USD'; - - return useQuery({ - queryKey: ['revenue', websiteId, { ...filterParams, ...queryParams }], - queryFn: () => - get(`/websites/${websiteId}/revenue`, { - currency, - ...filterParams, - ...queryParams, - }), - enabled: !!websiteId, - ...options, - }); -} diff --git a/src/components/hooks/queries/useRevenueValuesQuery.ts b/src/components/hooks/queries/useRevenueValuesQuery.ts deleted file mode 100644 index cd29dbbc..00000000 --- a/src/components/hooks/queries/useRevenueValuesQuery.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useApi } from '../useApi'; - -export function useRevenueValuesQuery(websiteId: string, startDate: Date, endDate: Date) { - const { get, useQuery } = useApi(); - - return useQuery({ - queryKey: ['revenue:values', { websiteId, startDate, endDate }], - queryFn: () => - get(`/reports/revenue`, { - websiteId, - startDate, - endDate, - }), - enabled: !!(websiteId && startDate && endDate), - }); -} diff --git a/src/components/icons.ts b/src/components/icons.ts index 883ad681..7d6f4a4c 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -2,6 +2,7 @@ export { AlertTriangle as Alert, ArrowRight as Arrow, Calendar, + ChartPie, ChevronRight as Chevron, Clock, X as Close, diff --git a/src/components/messages.ts b/src/components/messages.ts index d5d7063e..2b1b2d7c 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -132,6 +132,7 @@ export const labels = defineMessages({ selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' }, selectRole: { id: 'label.select-role', defaultMessage: 'Select role' }, selectDate: { id: 'label.select-date', defaultMessage: 'Select date' }, + selectFilter: { id: 'label.select-filter', defaultMessage: 'Select filter' }, all: { id: 'label.all', defaultMessage: 'All' }, session: { id: 'label.session', defaultMessage: 'Session' }, sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, @@ -331,6 +332,7 @@ export const messages = defineMessages({ noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' }, userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted.' }, noDataAvailable: { id: 'message.no-data-available', defaultMessage: 'No data available.' }, + nothingSelected: { id: 'message.nothing-selected', defaultMessage: 'Nothing selected.' }, confirmReset: { id: 'message.confirm-reset', defaultMessage: 'Are you sure you want to reset {target}?', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f3513a5c..f5a109ec 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -654,3 +654,56 @@ export const ISO_COUNTRIES = { ZWE: 'ZW', XKX: 'XK', }; + +export const CURRENCIES = [ + { id: 'USD', name: 'US Dollar' }, + { id: 'EUR', name: 'Euro' }, + { id: 'GBP', name: 'British Pound' }, + { id: 'JPY', name: 'Japanese Yen' }, + { id: 'CNY', name: 'Chinese Renminbi (Yuan)' }, + { id: 'CAD', name: 'Canadian Dollar' }, + { id: 'HKD', name: 'Hong Kong Dollar' }, + { id: 'AUD', name: 'Australian Dollar' }, + { id: 'SGD', name: 'Singapore Dollar' }, + { id: 'CHF', name: 'Swiss Franc' }, + { id: 'SEK', name: 'Swedish Krona' }, + { id: 'PLN', name: 'Polish Złoty' }, + { id: 'NOK', name: 'Norwegian Krone' }, + { id: 'DKK', name: 'Danish Krone' }, + { id: 'NZD', name: 'New Zealand Dollar' }, + { id: 'ZAR', name: 'South African Rand' }, + { id: 'MXN', name: 'Mexican Peso' }, + { id: 'THB', name: 'Thai Baht' }, + { id: 'HUF', name: 'Hungarian Forint' }, + { id: 'MYR', name: 'Malaysian Ringgit' }, + { id: 'INR', name: 'Indian Rupee' }, + { id: 'KRW', name: 'South Korean Won' }, + { id: 'BRL', name: 'Brazilian Real' }, + { id: 'TRY', name: 'Turkish Lira' }, + { id: 'CZK', name: 'Czech Koruna' }, + { id: 'ILS', name: 'Israeli New Shekel' }, + { id: 'RUB', name: 'Russian Ruble' }, + { id: 'AED', name: 'United Arab Emirates Dirham' }, + { id: 'IDR', name: 'Indonesian Rupiah' }, + { id: 'PHP', name: 'Philippine Peso' }, + { id: 'RON', name: 'Romanian Leu' }, + { id: 'COP', name: 'Colombian Peso' }, + { id: 'SAR', name: 'Saudi Riyal' }, + { id: 'ARS', name: 'Argentine Peso' }, + { id: 'VND', name: 'Vietnamese Dong' }, + { id: 'CLP', name: 'Chilean Peso' }, + { id: 'EGP', name: 'Egyptian Pound' }, + { id: 'KWD', name: 'Kuwaiti Dinar' }, + { id: 'PKR', name: 'Pakistani Rupee' }, + { id: 'QAR', name: 'Qatari Riyal' }, + { id: 'BHD', name: 'Bahraini Dinar' }, + { id: 'UAH', name: 'Ukrainian Hryvnia' }, + { id: 'PEN', name: 'Peruvian Sol' }, + { id: 'BDT', name: 'Bangladeshi Taka' }, + { id: 'MAD', name: 'Moroccan Dirham' }, + { id: 'KES', name: 'Kenyan Shilling' }, + { id: 'NGN', name: 'Nigerian Naira' }, + { id: 'TND', name: 'Tunisian Dinar' }, + { id: 'OMR', name: 'Omani Rial' }, + { id: 'GHS', name: 'Ghanaian Cedi' }, +]; diff --git a/src/lib/schema.ts b/src/lib/schema.ts index c07efe16..8361acc8 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -119,6 +119,26 @@ export const journeyReportSchema = z.object({ }), }); +export const retentionReportSchema = z.object({ + type: z.literal('retention'), +}); + +export const utmReportSchema = z.object({ + type: z.literal('utm'), +}); + +export const revenueReportSchema = z.object({ + type: z.literal('revenue'), +}); + +export const attributionReportSchema = z.object({ + type: z.literal('attribution'), +}); + +export const insightsReportSchema = z.object({ + type: z.literal('insights'), +}); + export const reportBaseSchema = z.object({ websiteId: z.string().uuid(), type: reportTypeParam, @@ -130,6 +150,11 @@ export const reportTypeSchema = z.discriminatedUnion('type', [ goalReportSchema, funnelReportSchema, journeyReportSchema, + retentionReportSchema, + utmReportSchema, + revenueReportSchema, + attributionReportSchema, + insightsReportSchema, ]); export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema); diff --git a/src/queries/sql/reports/getRetention.ts b/src/queries/sql/reports/getRetention.ts index 23854b60..61d6fc77 100644 --- a/src/queries/sql/reports/getRetention.ts +++ b/src/queries/sql/reports/getRetention.ts @@ -2,16 +2,12 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -export async function getRetention( - ...args: [ - websiteId: string, - filters: { - startDate: Date; - endDate: Date; - timezone?: string; - }, - ] -) { +export interface RetentionCriteria { + startDate: Date; + endDate: Date; +} + +export async function getRetention(...args: [websiteId: string, criteria: RetentionCriteria]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -20,11 +16,7 @@ export async function getRetention( async function relationalQuery( websiteId: string, - filters: { - startDate: Date; - endDate: Date; - timezone?: string; - }, + criteria: RetentionCriteria, ): Promise< { date: string; @@ -34,7 +26,7 @@ async function relationalQuery( percentage: number; }[] > { - const { startDate, endDate, timezone = 'UTC' } = filters; + const { startDate, endDate } = criteria; const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma; const unit = 'day'; @@ -42,7 +34,7 @@ async function relationalQuery( ` WITH cohort_items AS ( select session_id, - ${getDateSQL('created_at', unit, timezone)} as cohort_date + ${getDateSQL('created_at', unit)} as cohort_date from session where website_id = {{websiteId::uuid}} and created_at between {{startDate}} and {{endDate}} @@ -50,7 +42,7 @@ async function relationalQuery( user_activities AS ( select distinct w.session_id, - ${getDayDiffQuery(getDateSQL('created_at', unit, timezone), 'c.cohort_date')} as day_number + ${getDayDiffQuery(getDateSQL('created_at', unit), 'c.cohort_date')} as day_number from website_event w join cohort_items c on w.session_id = c.session_id @@ -95,11 +87,7 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - filters: { - startDate: Date; - endDate: Date; - timezone?: string; - }, + criteria: RetentionCriteria, ): Promise< { date: string; @@ -109,7 +97,7 @@ async function clickhouseQuery( percentage: number; }[] > { - const { startDate, endDate, timezone = 'UTC' } = filters; + const { startDate, endDate } = criteria; const { getDateSQL, rawQuery } = clickhouse; const unit = 'day'; @@ -117,7 +105,7 @@ async function clickhouseQuery( ` WITH cohort_items AS ( select - min(${getDateSQL('created_at', unit, timezone)}) as cohort_date, + min(${getDateSQL('created_at', unit)}) as cohort_date, session_id from website_event where website_id = {websiteId:UUID} @@ -127,7 +115,7 @@ async function clickhouseQuery( user_activities AS ( select distinct w.session_id, - (${getDateSQL('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number + (${getDateSQL('created_at', unit)} - c.cohort_date) / 86400 as day_number from website_event w join cohort_items c on w.session_id = c.session_id diff --git a/src/queries/sql/reports/getRevenue.ts b/src/queries/sql/reports/getRevenue.ts index f1fb1d73..a7062f47 100644 --- a/src/queries/sql/reports/getRevenue.ts +++ b/src/queries/sql/reports/getRevenue.ts @@ -2,18 +2,14 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -export async function getRevenue( - ...args: [ - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - unit: string; - timezone: string; - currency: string; - }, - ] -) { +export interface RevenueCriteria { + startDate: Date; + endDate: Date; + unit: string; + currency: string; +} + +export async function getRevenue(...args: [websiteId: string, criteria: RevenueCriteria]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -22,13 +18,7 @@ export async function getRevenue( async function relationalQuery( websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - unit: string; - timezone: string; - currency: string; - }, + criteria: RevenueCriteria, ): Promise<{ chart: { x: string; t: string; y: number }[]; country: { name: string; value: number }[]; @@ -40,7 +30,7 @@ async function relationalQuery( unique_count: number; }[]; }> { - const { startDate, endDate, timezone = 'UTC', unit = 'day', currency } = criteria; + const { startDate, endDate, unit = 'day', currency } = criteria; const { getDateSQL, rawQuery } = prisma; const db = getDatabaseType(); const like = db === POSTGRESQL ? 'ilike' : 'like'; @@ -49,7 +39,7 @@ async function relationalQuery( ` select we.event_name x, - ${getDateSQL('ed.created_at', unit, timezone)} t, + ${getDateSQL('ed.created_at', unit)} t, sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) y from event_data ed join website_event we @@ -67,7 +57,7 @@ async function relationalQuery( group by x, t order by t `, - { websiteId, startDate, endDate, unit, timezone, currency }, + { websiteId, startDate, endDate, unit, currency }, ); const countryRes = await rawQuery( @@ -140,7 +130,7 @@ async function relationalQuery( group by c.currency order by sum desc; `, - { websiteId, startDate, endDate, unit, timezone, currency }, + { websiteId, startDate, endDate, unit, currency }, ); return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes }; @@ -148,13 +138,7 @@ async function relationalQuery( async function clickhouseQuery( websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - unit: string; - timezone: string; - currency: string; - }, + criteria: RevenueCriteria, ): Promise<{ chart: { x: string; t: string; y: number }[]; country: { name: string; value: number }[]; @@ -166,7 +150,7 @@ async function clickhouseQuery( unique_count: number; }[]; }> { - const { startDate, endDate, timezone = 'UTC', unit = 'day', currency } = criteria; + const { startDate, endDate, unit = 'day', currency } = criteria; const { getDateSQL, rawQuery } = clickhouse; const chartRes = await rawQuery< @@ -179,7 +163,7 @@ async function clickhouseQuery( ` select event_name x, - ${getDateSQL('created_at', unit, timezone)} t, + ${getDateSQL('created_at', unit)} t, sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) y from event_data join (select event_id @@ -195,7 +179,7 @@ async function clickhouseQuery( group by x, t order by t `, - { websiteId, startDate, endDate, unit, timezone, currency }, + { websiteId, startDate, endDate, unit, currency }, ); const countryRes = await rawQuery< @@ -283,7 +267,7 @@ async function clickhouseQuery( group by c.currency order by sum desc; `, - { websiteId, startDate, endDate, unit, timezone, currency }, + { websiteId, startDate, endDate, unit, currency }, ); return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes }; diff --git a/src/queries/sql/reports/getRevenueValues.ts b/src/queries/sql/reports/getRevenueValues.ts deleted file mode 100644 index a46bf0bf..00000000 --- a/src/queries/sql/reports/getRevenueValues.ts +++ /dev/null @@ -1,75 +0,0 @@ -import prisma from '@/lib/prisma'; -import clickhouse from '@/lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA, getDatabaseType, POSTGRESQL } from '@/lib/db'; - -export async function getRevenueValues( - ...args: [ - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - }, - ] -) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery( - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - }, -) { - const { rawQuery } = prisma; - const { startDate, endDate } = criteria; - - const db = getDatabaseType(); - const like = db === POSTGRESQL ? 'ilike' : 'like'; - - return rawQuery( - ` - select distinct string_value as currency - from event_data - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} - and data_key ${like} '%currency%' - order by currency - `, - { - websiteId, - startDate, - endDate, - }, - ); -} - -async function clickhouseQuery( - websiteId: string, - criteria: { - startDate: Date; - endDate: Date; - }, -) { - const { rawQuery } = clickhouse; - const { startDate, endDate } = criteria; - - return rawQuery( - ` - select distinct string_value as currency - from event_data - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and positionCaseInsensitive(data_key, 'currency') > 0 - order by currency - `, - { - websiteId, - startDate, - endDate, - }, - ); -} diff --git a/src/queries/sql/reports/getUTM.ts b/src/queries/sql/reports/getUTM.ts index 5463815b..134e14b3 100644 --- a/src/queries/sql/reports/getUTM.ts +++ b/src/queries/sql/reports/getUTM.ts @@ -2,31 +2,20 @@ import clickhouse from '@/lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -export async function getUTM( - ...args: [ - websiteId: string, - filters: { - startDate: Date; - endDate: Date; - timezone?: string; - }, - ] -) { +export interface UTMCriteria { + startDate: Date; + endDate: Date; +} + +export async function getUTM(...args: [websiteId: string, criteria: UTMCriteria]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery( - websiteId: string, - filters: { - startDate: Date; - endDate: Date; - timezone?: string; - }, -) { - const { startDate, endDate } = filters; +async function relationalQuery(websiteId: string, criteria: UTMCriteria) { + const { startDate, endDate } = criteria; const { rawQuery } = prisma; return rawQuery( @@ -47,15 +36,8 @@ async function relationalQuery( ).then(result => parseParameters(result as any[])); } -async function clickhouseQuery( - websiteId: string, - filters: { - startDate: Date; - endDate: Date; - timezone?: string; - }, -) { - const { startDate, endDate } = filters; +async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) { + const { startDate, endDate } = criteria; const { rawQuery } = clickhouse; return rawQuery(