mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +01:00
Added revenue screen.
This commit is contained in:
parent
bce6737f29
commit
7662b77ce3
15 changed files with 351 additions and 29 deletions
|
|
@ -78,7 +78,7 @@
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@tanstack/react-query": "^5.28.6",
|
"@tanstack/react-query": "^5.28.6",
|
||||||
"@umami/react-zen": "^0.116.0",
|
"@umami/react-zen": "^0.117.0",
|
||||||
"@umami/redis-client": "^0.27.0",
|
"@umami/redis-client": "^0.27.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
|
|
|
||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -42,8 +42,8 @@ importers:
|
||||||
specifier: ^5.28.6
|
specifier: ^5.28.6
|
||||||
version: 5.76.1(react@19.1.0)
|
version: 5.76.1(react@19.1.0)
|
||||||
'@umami/react-zen':
|
'@umami/react-zen':
|
||||||
specifier: ^0.116.0
|
specifier: ^0.117.0
|
||||||
version: 0.116.0(@babel/core@7.27.1)(@types/react@19.1.4)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
|
version: 0.117.0(@babel/core@7.27.1)(@types/react@19.1.4)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||||
'@umami/redis-client':
|
'@umami/redis-client':
|
||||||
specifier: ^0.27.0
|
specifier: ^0.27.0
|
||||||
version: 0.27.0
|
version: 0.27.0
|
||||||
|
|
@ -3035,8 +3035,8 @@ packages:
|
||||||
resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
|
resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@umami/react-zen@0.116.0':
|
'@umami/react-zen@0.117.0':
|
||||||
resolution: {integrity: sha512-/OjfYgwA9/4JpfKjf/b3HinVoeEoyOfLHJk8Uv0opBx+Jy2I6WPM4ZwvaRVxIbNwzpw/JZekC46TJs6bQNzbGg==}
|
resolution: {integrity: sha512-vo1i25cBpMsAWNHJ4RPFDJzlaH93NXwx8VotmnoAWgP39Yy+fdUwmi+dDXNBOKvWzoHO9BFf0nIYiT16klnR6g==}
|
||||||
|
|
||||||
'@umami/redis-client@0.27.0':
|
'@umami/redis-client@0.27.0':
|
||||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||||
|
|
@ -7459,8 +7459,8 @@ packages:
|
||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
zustand@5.0.4:
|
zustand@5.0.5:
|
||||||
resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==}
|
resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '>=18.0.0'
|
'@types/react': '>=18.0.0'
|
||||||
|
|
@ -10860,7 +10860,7 @@ snapshots:
|
||||||
'@typescript-eslint/types': 8.32.1
|
'@typescript-eslint/types': 8.32.1
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
|
|
||||||
'@umami/react-zen@0.116.0(@babel/core@7.27.1)(@types/react@19.1.4)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
'@umami/react-zen@0.117.0(@babel/core@7.27.1)(@types/react@19.1.4)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fontsource/jetbrains-mono': 5.2.5
|
'@fontsource/jetbrains-mono': 5.2.5
|
||||||
'@internationalized/date': 3.8.1
|
'@internationalized/date': 3.8.1
|
||||||
|
|
@ -10877,7 +10877,7 @@ snapshots:
|
||||||
react-hook-form: 7.56.4(react@19.1.0)
|
react-hook-form: 7.56.4(react@19.1.0)
|
||||||
react-icons: 5.5.0(react@19.1.0)
|
react-icons: 5.5.0(react@19.1.0)
|
||||||
thenby: 1.3.4
|
thenby: 1.3.4
|
||||||
zustand: 5.0.4(@types/react@19.1.4)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
|
zustand: 5.0.5(@types/react@19.1.4)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- '@opentelemetry/api'
|
- '@opentelemetry/api'
|
||||||
|
|
@ -16046,7 +16046,7 @@ snapshots:
|
||||||
immer: 9.0.21
|
immer: 9.0.21
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
zustand@5.0.4(@types/react@19.1.4)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)):
|
zustand@5.0.5(@types/react@19.1.4)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.4
|
'@types/react': 19.1.4
|
||||||
immer: 9.0.21
|
immer: 9.0.21
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,6 @@ export function SideNav(props: any) {
|
||||||
href: renderTeamUrl('/dashboard'),
|
href: renderTeamUrl('/dashboard'),
|
||||||
icon: <Lucide.Copy />,
|
icon: <Lucide.Copy />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: formatMessage(labels.reports),
|
|
||||||
href: renderTeamUrl('/reports'),
|
|
||||||
icon: <Lucide.ChartArea />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.websites),
|
label: formatMessage(labels.websites),
|
||||||
href: renderTeamUrl('/websites'),
|
href: renderTeamUrl('/websites'),
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,19 @@ import { FilterBar } from '@/components/input/FilterBar';
|
||||||
|
|
||||||
export function WebsiteControls({
|
export function WebsiteControls({
|
||||||
websiteId,
|
websiteId,
|
||||||
showFilter = true,
|
allowFilter = true,
|
||||||
showCompare,
|
allowCompare,
|
||||||
}: {
|
}: {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
showFilter?: boolean;
|
allowFilter?: boolean;
|
||||||
showCompare?: boolean;
|
allowCompare?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<Row alignItems="center" justifyContent="space-between" gap="3">
|
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
{allowFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||||
<Row alignItems="center" gap="3">
|
<Row alignItems="center" gap="3">
|
||||||
<WebsiteDateFilter websiteId={websiteId} showCompare={showCompare} />
|
<WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<FilterBar />
|
<FilterBar />
|
||||||
|
|
|
||||||
13
src/app/(main)/websites/[websiteId]/revenue/RevenuePage.tsx
Normal file
13
src/app/(main)/websites/[websiteId]/revenue/RevenuePage.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use client';
|
||||||
|
import { Column } from '@umami/react-zen';
|
||||||
|
import { RevenueView } from './RevenueView';
|
||||||
|
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||||
|
|
||||||
|
export function RevenuePage({ websiteId }: { websiteId: string }) {
|
||||||
|
return (
|
||||||
|
<Column gap>
|
||||||
|
<WebsiteControls websiteId={websiteId} allowCompare={false} />
|
||||||
|
<RevenueView websiteId={websiteId} />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/app/(main)/websites/[websiteId]/revenue/RevenueTable.tsx
Normal file
27
src/app/(main)/websites/[websiteId]/revenue/RevenueTable.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { DataColumn, DataTable } from '@umami/react-zen';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { formatLongCurrency } from '@/lib/format';
|
||||||
|
|
||||||
|
export function RevenueTable({ data = [] }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable data={data}>
|
||||||
|
<DataColumn id="currency" label={formatMessage(labels.currency)} align="end">
|
||||||
|
{(row: any) => row.currency}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="currency" label={formatMessage(labels.total)} align="end">
|
||||||
|
{(row: any) => formatLongCurrency(row.sum, row.currency)}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="currency" label={formatMessage(labels.average)} align="end">
|
||||||
|
{(row: any) => formatLongCurrency(row.count ? row.sum / row.count : 0, row.currency)}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="currency" label={formatMessage(labels.transactions)} align="end">
|
||||||
|
{(row: any) => row.count}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="currency" label={formatMessage(labels.uniqueCustomers)} align="end">
|
||||||
|
{(row: any) => row.unique_count}
|
||||||
|
</DataColumn>
|
||||||
|
</DataTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
169
src/app/(main)/websites/[websiteId]/revenue/RevenueView.tsx
Normal file
169
src/app/(main)/websites/[websiteId]/revenue/RevenueView.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
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 }) => (
|
||||||
|
<span className={classNames(locale)}>
|
||||||
|
<TypeIcon type="country" value={code?.toLowerCase()} />
|
||||||
|
{countryNames[code]}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
[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 (
|
||||||
|
<>
|
||||||
|
<Column gap>
|
||||||
|
<Panel>
|
||||||
|
<MetricsBar isFetched={!!data}>
|
||||||
|
{metricData?.map(({ label, value, formatValue }) => {
|
||||||
|
return (
|
||||||
|
<MetricCard key={label} value={value} label={label} formatValue={formatValue} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MetricsBar>
|
||||||
|
</Panel>
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<Panel>
|
||||||
|
<BarChart
|
||||||
|
minDate={dateRange?.startDate}
|
||||||
|
maxDate={dateRange?.endDate}
|
||||||
|
data={chartData}
|
||||||
|
unit={dateRange?.unit}
|
||||||
|
stacked={true}
|
||||||
|
currency={currency}
|
||||||
|
renderXLabel={renderDateLabels(dateRange?.unit, locale)}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<GridRow layout="two">
|
||||||
|
<ListTable
|
||||||
|
metric={formatMessage(labels.country)}
|
||||||
|
data={data?.country.map(({ name, value }) => ({
|
||||||
|
x: name,
|
||||||
|
y: Number(value),
|
||||||
|
z: (value / data?.total.sum) * 100,
|
||||||
|
}))}
|
||||||
|
renderLabel={renderCountryName}
|
||||||
|
/>
|
||||||
|
<PieChart type="doughnut" data={countryData} />
|
||||||
|
</GridRow>
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Panel>
|
||||||
|
<RevenueTable data={data?.table} />
|
||||||
|
</Panel>
|
||||||
|
</Column>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/app/(main)/websites/[websiteId]/revenue/page.tsx
Normal file
12
src/app/(main)/websites/[websiteId]/revenue/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
import { RevenuePage } from './RevenuePage';
|
||||||
|
|
||||||
|
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||||
|
const { websiteId } = await params;
|
||||||
|
|
||||||
|
return <RevenuePage websiteId={websiteId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Revenue UTM Parameters',
|
||||||
|
};
|
||||||
|
|
@ -6,7 +6,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
export function UTMPage({ websiteId }: { websiteId: string }) {
|
export function UTMPage({ websiteId }: { websiteId: string }) {
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} allowCompare={false} />
|
||||||
<UTMView websiteId={websiteId} />
|
<UTMView websiteId={websiteId} />
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
67
src/app/api/websites/[websiteId]/revenue/route.ts
Normal file
67
src/app/api/websites/[websiteId]/revenue/route.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -28,8 +28,8 @@ export interface BarChartProps extends ChartProps {
|
||||||
renderYLabel?: (label: string, index: number, values: any[]) => string;
|
renderYLabel?: (label: string, index: number, values: any[]) => string;
|
||||||
XAxisType?: string;
|
XAxisType?: string;
|
||||||
YAxisType?: string;
|
YAxisType?: string;
|
||||||
minDate?: number | string;
|
minDate?: Date;
|
||||||
maxDate?: number | string;
|
maxDate?: Date;
|
||||||
isAllTime?: boolean;
|
isAllTime?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { Loading, Box, Column } from '@umami/react-zen';
|
import { Loading, Box, Column, BoxProps } from '@umami/react-zen';
|
||||||
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
|
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
|
||||||
import { Legend } from '@/components/metrics/Legend';
|
import { Legend } from '@/components/metrics/Legend';
|
||||||
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
|
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
|
||||||
import type { BoxProps } from '@umami/react-zen/Box';
|
|
||||||
|
|
||||||
export interface ChartProps extends BoxProps {
|
export interface ChartProps extends BoxProps {
|
||||||
type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter';
|
type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter';
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export * from './queries/useRealtimeQuery';
|
||||||
export * from './queries/useReportQuery';
|
export * from './queries/useReportQuery';
|
||||||
export * from './queries/useReportsQuery';
|
export * from './queries/useReportsQuery';
|
||||||
export * from './queries/useRetentionQuery';
|
export * from './queries/useRetentionQuery';
|
||||||
|
export * from './queries/useRevenueQuery';
|
||||||
export * from './queries/useSessionActivityQuery';
|
export * from './queries/useSessionActivityQuery';
|
||||||
export * from './queries/useSessionDataQuery';
|
export * from './queries/useSessionDataQuery';
|
||||||
export * from './queries/useSessionDataPropertiesQuery';
|
export * from './queries/useSessionDataPropertiesQuery';
|
||||||
|
|
|
||||||
39
src/components/hooks/queries/useRevenueQuery.ts
Normal file
39
src/components/hooks/queries/useRevenueQuery.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { useFilterParams } from '../useFilterParams';
|
||||||
|
import { UseQueryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
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<RevenueData, Error, RevenueData, any[]> & { onDataLoad?: (data: any) => void },
|
||||||
|
'queryKey' | 'queryFn'
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const filterParams = useFilterParams(websiteId);
|
||||||
|
const currency = 'USD';
|
||||||
|
|
||||||
|
return useQuery<RevenueData, Error, RevenueData, any[]>({
|
||||||
|
queryKey: ['revenue', websiteId, { ...filterParams, ...queryParams }],
|
||||||
|
queryFn: () =>
|
||||||
|
get(`/websites/${websiteId}/revenue`, {
|
||||||
|
currency,
|
||||||
|
...filterParams,
|
||||||
|
...queryParams,
|
||||||
|
}),
|
||||||
|
enabled: !!websiteId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -19,13 +19,13 @@ export function WebsiteDateFilter({
|
||||||
websiteId,
|
websiteId,
|
||||||
showAllTime = true,
|
showAllTime = true,
|
||||||
showButtons = true,
|
showButtons = true,
|
||||||
showCompare = true,
|
allowCompare = true,
|
||||||
}: {
|
}: {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
compare?: string;
|
compare?: string;
|
||||||
showAllTime?: boolean;
|
showAllTime?: boolean;
|
||||||
showButtons?: boolean;
|
showButtons?: boolean;
|
||||||
showCompare?: boolean;
|
allowCompare?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { dateRange, saveDateRange } = useDateRange(websiteId);
|
const { dateRange, saveDateRange } = useDateRange(websiteId);
|
||||||
const { value, startDate, endDate, offset } = dateRange;
|
const { value, startDate, endDate, offset } = dateRange;
|
||||||
|
|
@ -92,7 +92,7 @@ export function WebsiteDateFilter({
|
||||||
</Select>
|
</Select>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{!isAllTime && showCompare && (
|
{!isAllTime && allowCompare && (
|
||||||
<TooltipTrigger delay={0}>
|
<TooltipTrigger delay={0}>
|
||||||
<Button variant="quiet" onPress={handleCompare}>
|
<Button variant="quiet" onPress={handleCompare}>
|
||||||
<Icon fillColor>{compare ? <Icons.Close /> : <Icons.Compare />}</Icon>
|
<Icon fillColor>{compare ? <Icons.Close /> : <Icons.Compare />}</Icon>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue