mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Updates to Revenue report.
This commit is contained in:
parent
4995a0e1e4
commit
095d1f2070
19 changed files with 365 additions and 416 deletions
|
|
@ -1,33 +0,0 @@
|
||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
gap: 30px;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
border: 1px solid var(--base400);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0 20px 20px 20px;
|
|
||||||
display: grid;
|
|
||||||
gap: 40px;
|
|
||||||
grid-template-columns: repeat(3, minmax(300px, 1fr));
|
|
||||||
box-shadow: 0 0 0 10px var(--base100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapped {
|
|
||||||
border: 1px solid var(--blue900);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { Button } from '@umami/react-zen';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import Script from 'next/script';
|
|
||||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
|
||||||
import { EventsChart } from '@/components/metrics/EventsChart';
|
|
||||||
import { WebsiteChart } from '../websites/[websiteId]/WebsiteChart';
|
|
||||||
import { useApi, useNavigation } from '@/components/hooks';
|
|
||||||
import styles from './TestConsole.module.css';
|
|
||||||
|
|
||||||
export function TestConsole({ websiteId }: { websiteId: string }) {
|
|
||||||
const { get, useQuery } = useApi();
|
|
||||||
const { data, isLoading, error } = useQuery({
|
|
||||||
queryKey: ['websites:me'],
|
|
||||||
queryFn: () => get('/me/websites'),
|
|
||||||
});
|
|
||||||
const { router } = useNavigation();
|
|
||||||
|
|
||||||
function handleChange(value: string) {
|
|
||||||
router.push(`/console/${value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRunScript() {
|
|
||||||
window['umami'].track(props => ({
|
|
||||||
...props,
|
|
||||||
url: '/page-view',
|
|
||||||
referrer: 'https://www.google.com',
|
|
||||||
}));
|
|
||||||
window['umami'].track('track-event-no-data');
|
|
||||||
window['umami'].track('track-event-with-data', {
|
|
||||||
test: 'test-data',
|
|
||||||
boolean: true,
|
|
||||||
booleanError: 'true',
|
|
||||||
time: new Date(),
|
|
||||||
user: `user${Math.round(Math.random() * 10)}`,
|
|
||||||
number: 1,
|
|
||||||
number2: Math.random() * 100,
|
|
||||||
time2: new Date().toISOString(),
|
|
||||||
nested: {
|
|
||||||
test: 'test-data',
|
|
||||||
number: 1,
|
|
||||||
object: {
|
|
||||||
test: 'test-data',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
array: [1, 2, 3],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRunRevenue() {
|
|
||||||
window['umami'].track(props => ({
|
|
||||||
...props,
|
|
||||||
url: '/checkout-cart',
|
|
||||||
referrer: 'https://www.google.com',
|
|
||||||
}));
|
|
||||||
window['umami'].track('checkout-cart', {
|
|
||||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
|
||||||
currency: 'USD',
|
|
||||||
});
|
|
||||||
window['umami'].track('affiliate-link', {
|
|
||||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
|
||||||
currency: 'USD',
|
|
||||||
});
|
|
||||||
window['umami'].track('promotion-link', {
|
|
||||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
|
||||||
currency: 'USD',
|
|
||||||
});
|
|
||||||
window['umami'].track('checkout-cart', {
|
|
||||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
|
||||||
currency: 'EUR',
|
|
||||||
});
|
|
||||||
window['umami'].track('promotion-link', {
|
|
||||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
|
||||||
currency: 'EUR',
|
|
||||||
});
|
|
||||||
window['umami'].track('affiliate-link', {
|
|
||||||
item1: {
|
|
||||||
productIdentity: 'ABC424',
|
|
||||||
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
|
|
||||||
currency: 'JPY',
|
|
||||||
},
|
|
||||||
item2: {
|
|
||||||
productIdentity: 'ZYW684',
|
|
||||||
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
|
|
||||||
currency: 'JPY',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRunIdentify() {
|
|
||||||
window['umami'].identify({
|
|
||||||
userId: 123,
|
|
||||||
name: 'brian',
|
|
||||||
number: Math.random() * 100,
|
|
||||||
test: 'test-data',
|
|
||||||
boolean: true,
|
|
||||||
booleanError: 'true',
|
|
||||||
time: new Date(),
|
|
||||||
time2: new Date().toISOString(),
|
|
||||||
nested: {
|
|
||||||
test: 'test-data',
|
|
||||||
number: 1,
|
|
||||||
object: {
|
|
||||||
test: 'test-data',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
array: [1, 2, 3],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const website = data?.data.find(({ id }) => websiteId === id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageBody isLoading={isLoading} error={error}>
|
|
||||||
<SectionHeader title="Test console">
|
|
||||||
<WebsiteSelect websiteId={website?.id} onSelect={handleChange} />
|
|
||||||
</SectionHeader>
|
|
||||||
{website && (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Script
|
|
||||||
async
|
|
||||||
data-website-id={websiteId}
|
|
||||||
src={`${process.env.basePath || ''}/script.js`}
|
|
||||||
data-cache="true"
|
|
||||||
/>
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<div className={styles.group}>
|
|
||||||
<div className={styles.header}>Page links</div>
|
|
||||||
<div>
|
|
||||||
<Link href={`/console/${websiteId}/page/1/?q=abc`}>page one</Link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Link href={`/console/${websiteId}/page/2/?q=123 `}>page two</Link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="https://www.google.com" data-umami-event="external-link-direct">
|
|
||||||
external link (direct)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://www.google.com"
|
|
||||||
data-umami-event="external-link-tab"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
external link (tab)
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.group}>
|
|
||||||
<div className={styles.header}>Click events</div>
|
|
||||||
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
|
|
||||||
Send event
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
id="send-event-data-button"
|
|
||||||
data-umami-event="button-click"
|
|
||||||
data-umami-event-name="bob"
|
|
||||||
data-umami-event-id="123"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Send event with data
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
id="generate-revenue-button"
|
|
||||||
data-umami-event="checkout-cart"
|
|
||||||
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
|
|
||||||
data-umami-event-currency="USD"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Generate revenue data
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
id="button-with-div-button"
|
|
||||||
data-umami-event="button-click"
|
|
||||||
data-umami-event-name={'bob'}
|
|
||||||
data-umami-event-id="123"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
<div className={styles.wrapped}>Button with div</div>
|
|
||||||
</Button>
|
|
||||||
<div data-umami-event="div-click" className={styles.wrapped}>
|
|
||||||
DIV with attribute
|
|
||||||
</div>
|
|
||||||
<div data-umami-event="div-click-one" className={styles.wrapped}>
|
|
||||||
<div data-umami-event="div-click-two" className={styles.wrapped}>
|
|
||||||
<div data-umami-event="div-click-three" className={styles.wrapped}>
|
|
||||||
Nested DIV
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.group}>
|
|
||||||
<div className={styles.header}>Javascript events</div>
|
|
||||||
<Button id="manual-button" variant="primary" onClick={handleRunScript}>
|
|
||||||
Run script
|
|
||||||
</Button>
|
|
||||||
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
|
|
||||||
Run identify
|
|
||||||
</Button>
|
|
||||||
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}>
|
|
||||||
Revenue script
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WebsiteChart websiteId={website.id} />
|
|
||||||
<EventsChart websiteId={website.id} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</PageBody>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
202
src/app/(main)/console/[websiteId]/TestConsolePage.tsx
Normal file
202
src/app/(main)/console/[websiteId]/TestConsolePage.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
'use client';
|
||||||
|
import { Button, Grid, Column, Heading } from '@umami/react-zen';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Script from 'next/script';
|
||||||
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
|
import { EventsChart } from '@/components/metrics/EventsChart';
|
||||||
|
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
||||||
|
import { useWebsiteQuery } from '@/components/hooks';
|
||||||
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
|
|
||||||
|
export function TestConsolePage({ websiteId }: { websiteId: string }) {
|
||||||
|
const { data } = useWebsiteQuery(websiteId);
|
||||||
|
|
||||||
|
function handleRunScript() {
|
||||||
|
window['umami'].track(props => ({
|
||||||
|
...props,
|
||||||
|
url: '/page-view',
|
||||||
|
referrer: 'https://www.google.com',
|
||||||
|
}));
|
||||||
|
window['umami'].track('track-event-no-data');
|
||||||
|
window['umami'].track('track-event-with-data', {
|
||||||
|
test: 'test-data',
|
||||||
|
boolean: true,
|
||||||
|
booleanError: 'true',
|
||||||
|
time: new Date(),
|
||||||
|
user: `user${Math.round(Math.random() * 10)}`,
|
||||||
|
number: 1,
|
||||||
|
number2: Math.random() * 100,
|
||||||
|
time2: new Date().toISOString(),
|
||||||
|
nested: {
|
||||||
|
test: 'test-data',
|
||||||
|
number: 1,
|
||||||
|
object: {
|
||||||
|
test: 'test-data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
array: [1, 2, 3],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRunRevenue() {
|
||||||
|
window['umami'].track(props => ({
|
||||||
|
...props,
|
||||||
|
url: '/checkout-cart',
|
||||||
|
referrer: 'https://www.google.com',
|
||||||
|
}));
|
||||||
|
window['umami'].track('checkout-cart', {
|
||||||
|
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||||
|
currency: 'USD',
|
||||||
|
});
|
||||||
|
window['umami'].track('affiliate-link', {
|
||||||
|
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||||
|
currency: 'USD',
|
||||||
|
});
|
||||||
|
window['umami'].track('promotion-link', {
|
||||||
|
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||||
|
currency: 'USD',
|
||||||
|
});
|
||||||
|
window['umami'].track('checkout-cart', {
|
||||||
|
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||||
|
currency: 'EUR',
|
||||||
|
});
|
||||||
|
window['umami'].track('promotion-link', {
|
||||||
|
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||||
|
currency: 'EUR',
|
||||||
|
});
|
||||||
|
window['umami'].track('affiliate-link', {
|
||||||
|
item1: {
|
||||||
|
productIdentity: 'ABC424',
|
||||||
|
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
|
||||||
|
currency: 'JPY',
|
||||||
|
},
|
||||||
|
item2: {
|
||||||
|
productIdentity: 'ZYW684',
|
||||||
|
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
|
||||||
|
currency: 'JPY',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRunIdentify() {
|
||||||
|
window['umami'].identify({
|
||||||
|
userId: 123,
|
||||||
|
name: 'brian',
|
||||||
|
number: Math.random() * 100,
|
||||||
|
test: 'test-data',
|
||||||
|
boolean: true,
|
||||||
|
booleanError: 'true',
|
||||||
|
time: new Date(),
|
||||||
|
time2: new Date().toISOString(),
|
||||||
|
nested: {
|
||||||
|
test: 'test-data',
|
||||||
|
number: 1,
|
||||||
|
object: {
|
||||||
|
test: 'test-data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
array: [1, 2, 3],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageBody>
|
||||||
|
<PageHeader title="Test console">
|
||||||
|
<Column>{data.name}</Column>
|
||||||
|
</PageHeader>
|
||||||
|
<Column gap="6" paddingY="6">
|
||||||
|
<Script
|
||||||
|
async
|
||||||
|
data-website-id={websiteId}
|
||||||
|
src={`${process.env.basePath || ''}/script.js`}
|
||||||
|
data-cache="true"
|
||||||
|
/>
|
||||||
|
<Grid columns="1fr 1fr 1fr" gap>
|
||||||
|
<Column gap>
|
||||||
|
<Heading>Page links</Heading>
|
||||||
|
<div>
|
||||||
|
<Link href={`/console/${websiteId}?page=1`}>page one</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link href={`/console/${websiteId}?page=2 `}>page two</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://www.google.com" data-umami-event="external-link-direct">
|
||||||
|
external link (direct)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="https://www.google.com"
|
||||||
|
data-umami-event="external-link-tab"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
external link (tab)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
<Column gap>
|
||||||
|
<Heading>Click events</Heading>
|
||||||
|
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
|
||||||
|
Send event
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
id="send-event-data-button"
|
||||||
|
data-umami-event="button-click"
|
||||||
|
data-umami-event-name="bob"
|
||||||
|
data-umami-event-id="123"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
Send event with data
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
id="generate-revenue-button"
|
||||||
|
data-umami-event="checkout-cart"
|
||||||
|
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
|
||||||
|
data-umami-event-currency="USD"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
Generate revenue data
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
id="button-with-div-button"
|
||||||
|
data-umami-event="button-click"
|
||||||
|
data-umami-event-name={'bob'}
|
||||||
|
data-umami-event-id="123"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
<div>Button with div</div>
|
||||||
|
</Button>
|
||||||
|
<div data-umami-event="div-click">DIV with attribute</div>
|
||||||
|
<div data-umami-event="div-click-one">
|
||||||
|
<div data-umami-event="div-click-two">
|
||||||
|
<div data-umami-event="div-click-three">Nested DIV</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
<Column gap>
|
||||||
|
<Heading>Javascript events</Heading>
|
||||||
|
<Button id="manual-button" variant="primary" onClick={handleRunScript}>
|
||||||
|
Run script
|
||||||
|
</Button>
|
||||||
|
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
|
||||||
|
Run identify
|
||||||
|
</Button>
|
||||||
|
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}>
|
||||||
|
Revenue script
|
||||||
|
</Button>
|
||||||
|
</Column>
|
||||||
|
</Grid>
|
||||||
|
<Heading>Pageviews</Heading>
|
||||||
|
<WebsiteChart websiteId={websiteId} />
|
||||||
|
<Heading>Events</Heading>
|
||||||
|
<EventsChart websiteId={websiteId} />
|
||||||
|
</Column>
|
||||||
|
</PageBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { TestConsole } from '../TestConsole';
|
import { TestConsolePage } from './TestConsolePage';
|
||||||
|
|
||||||
async function getEnabled() {
|
async function getEnabled() {
|
||||||
return !!process.env.ENABLE_TEST_CONSOLE;
|
return !!process.env.ENABLE_TEST_CONSOLE;
|
||||||
|
|
@ -14,7 +14,7 @@ export default async function ({ params }: { params: Promise<{ websiteId: string
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TestConsole websiteId={websiteId?.[0]} />;
|
return <TestConsolePage websiteId={websiteId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
@ -13,7 +13,7 @@ export function RealtimeCountries({ data }) {
|
||||||
const renderCountryName = useCallback(
|
const renderCountryName = useCallback(
|
||||||
({ x: code }) => (
|
({ x: code }) => (
|
||||||
<span className={classNames(styles.row)}>
|
<span className={classNames(styles.row)}>
|
||||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
<TypeIcon type="country" value={code} />
|
||||||
{countryNames[code]}
|
{countryNames[code]}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,9 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
|
||||||
autoFlow="column"
|
autoFlow="column"
|
||||||
>
|
>
|
||||||
<Column>
|
<Column>
|
||||||
<Text weight="bold">{formatMessage(labels.cohort)}</Text>
|
<Text weight="bold" align="center">
|
||||||
|
{formatMessage(labels.cohort)}
|
||||||
|
</Text>
|
||||||
</Column>
|
</Column>
|
||||||
{days.map(n => (
|
{days.map(n => (
|
||||||
<Column key={n}>
|
<Column key={n}>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Grid, Select, ListItem } from '@umami/react-zen';
|
import { Grid, Row, Text } from '@umami/react-zen';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { colord } from 'colord';
|
import { colord } from 'colord';
|
||||||
import { BarChart } from '@/components/charts/BarChart';
|
import { BarChart } from '@/components/charts/BarChart';
|
||||||
import { PieChart } from '@/components/charts/PieChart';
|
|
||||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||||
import { useCountryNames, useLocale, useMessages, useResultQuery } from '@/components/hooks';
|
import { useCountryNames, useLocale, useMessages, useResultQuery } from '@/components/hooks';
|
||||||
import { ListTable } from '@/components/metrics/ListTable';
|
import { ListTable } from '@/components/metrics/ListTable';
|
||||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
import { renderDateLabels } from '@/lib/charts';
|
import { renderDateLabels } from '@/lib/charts';
|
||||||
import { CHART_COLORS, CURRENCIES } from '@/lib/constants';
|
import { CHART_COLORS } from '@/lib/constants';
|
||||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { parseDateRange } from '@/lib/date';
|
import { getMinimumUnit } from '@/lib/date';
|
||||||
|
import { CurrencySelect } from '@/components/input/CurrencySelect';
|
||||||
|
|
||||||
export interface RevenueProps {
|
export interface RevenueProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
|
@ -26,11 +26,10 @@ export interface RevenueProps {
|
||||||
|
|
||||||
export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
||||||
const [currency, setCurrency] = useState('USD');
|
const [currency, setCurrency] = useState('USD');
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const { countryNames } = useCountryNames(locale);
|
const { countryNames } = useCountryNames(locale);
|
||||||
const { unit } = parseDateRange({ startDate, endDate }, locale);
|
const unit = getMinimumUnit(startDate, endDate);
|
||||||
const { data, error, isLoading } = useResultQuery<any>('revenue', {
|
const { data, error, isLoading } = useResultQuery<any>('revenue', {
|
||||||
websiteId,
|
websiteId,
|
||||||
dateRange: {
|
dateRange: {
|
||||||
|
|
@ -45,10 +44,10 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
||||||
|
|
||||||
const renderCountryName = useCallback(
|
const renderCountryName = useCallback(
|
||||||
({ x: code }) => (
|
({ x: code }) => (
|
||||||
<span className={classNames(locale)}>
|
<Row className={classNames(locale)} gap>
|
||||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
<TypeIcon type="country" value={code} />
|
||||||
{countryNames[code]}
|
<Text>{countryNames[code] || formatMessage(labels.unknown)}</Text>
|
||||||
</span>
|
</Row>
|
||||||
),
|
),
|
||||||
[countryNames, locale],
|
[countryNames, locale],
|
||||||
);
|
);
|
||||||
|
|
@ -79,24 +78,9 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}, [data]);
|
}, [data, startDate, endDate, unit]);
|
||||||
|
|
||||||
const countryData = useMemo(() => {
|
const metrics = 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 [];
|
if (!data) return [];
|
||||||
|
|
||||||
const { sum, count, unique_count } = data.total;
|
const { sum, count, unique_count } = data.total;
|
||||||
|
|
@ -128,69 +112,41 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<Grid columns="280px" gap>
|
<Grid columns="280px" gap>
|
||||||
<Select
|
<CurrencySelect value={currency} onChange={setCurrency} />
|
||||||
items={CURRENCIES}
|
|
||||||
label={formatMessage(labels.currency)}
|
|
||||||
value={currency}
|
|
||||||
defaultValue={currency}
|
|
||||||
onChange={setCurrency}
|
|
||||||
listProps={{ style: { maxHeight: '300px' } }}
|
|
||||||
onSearch={setSearch}
|
|
||||||
allowSearch
|
|
||||||
>
|
|
||||||
{CURRENCIES.map(({ id, name }) => {
|
|
||||||
if (search && !`${id}${name}`.toLowerCase().includes(search)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem key={id} id={id}>
|
|
||||||
{id} — {name}
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}).filter(n => n)}
|
|
||||||
</Select>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
|
<LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<MetricsBar isFetched={!!data}>
|
<MetricsBar isFetched={!!data} isLoading={isLoading}>
|
||||||
{metricData?.map(({ label, value, formatValue }) => {
|
{metrics?.map(({ label, value, formatValue }) => {
|
||||||
return (
|
return (
|
||||||
<MetricCard key={label} value={value} label={label} formatValue={formatValue} />
|
<MetricCard key={label} value={value} label={label} formatValue={formatValue} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</MetricsBar>
|
</MetricsBar>
|
||||||
{data && (
|
<Panel>
|
||||||
<>
|
<BarChart
|
||||||
<Panel>
|
data={chartData}
|
||||||
<BarChart
|
minDate={startDate}
|
||||||
minDate={startDate}
|
maxDate={endDate}
|
||||||
maxDate={endDate}
|
unit={unit}
|
||||||
data={chartData}
|
stacked={true}
|
||||||
unit={unit}
|
currency={currency}
|
||||||
stacked={true}
|
isLoading={isLoading}
|
||||||
currency={currency}
|
renderXLabel={renderDateLabels(unit, locale)}
|
||||||
renderXLabel={renderDateLabels(unit, locale)}
|
/>
|
||||||
isLoading={isLoading}
|
</Panel>
|
||||||
/>
|
<Panel>
|
||||||
</Panel>
|
<ListTable
|
||||||
<Panel>
|
title={formatMessage(labels.country)}
|
||||||
<Grid columns="1fr 1fr">
|
data={data?.country.map(({ name, value }: { name: string; value: number }) => ({
|
||||||
<ListTable
|
x: name,
|
||||||
metric={formatMessage(labels.country)}
|
y: value,
|
||||||
data={data?.country.map(({ name, value }) => ({
|
z: (value / data?.total.sum) * 100,
|
||||||
x: name,
|
}))}
|
||||||
y: Number(value),
|
currency={currency}
|
||||||
z: (value / data?.total.sum) * 100,
|
renderLabel={renderCountryName}
|
||||||
}))}
|
/>
|
||||||
renderLabel={renderCountryName}
|
</Panel>
|
||||||
/>
|
|
||||||
<PieChart type="doughnut" data={countryData} />
|
|
||||||
</Grid>
|
|
||||||
</Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Column>
|
</Column>
|
||||||
</LoadingPanel>
|
</LoadingPanel>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
|
import { Row, Grid, Text } from '@umami/react-zen';
|
||||||
import { format, startOfDay, addHours } from 'date-fns';
|
import { format, startOfDay, addHours } from 'date-fns';
|
||||||
import { useLocale, useMessages, useWebsiteSessionsWeeklyQuery } from '@/components/hooks';
|
import { useLocale, useMessages, useWebsiteSessionsWeeklyQuery } from '@/components/hooks';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { getDayOfWeekAsDate } from '@/lib/date';
|
import { getDayOfWeekAsDate } from '@/lib/date';
|
||||||
import styles from './SessionsWeekly.module.css';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Focusable, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
import { Focusable, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||||
|
|
||||||
export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
||||||
|
|
@ -38,9 +37,9 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingPanel {...(props as any)} data={data}>
|
<LoadingPanel {...(props as any)} data={data}>
|
||||||
<div key={data} className={styles.week}>
|
<Grid columns="repeat(8, 1fr)" gap>
|
||||||
<div className={styles.day}>
|
<Grid rows="repeat(25, 20px)" gap="1">
|
||||||
<div className={styles.header}> </div>
|
<Row> </Row>
|
||||||
{Array(24)
|
{Array(24)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, i) => {
|
.map((_, i) => {
|
||||||
|
|
@ -48,46 +47,55 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
||||||
.replace(/\D00 ?/, '')
|
.replace(/\D00 ?/, '')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
return (
|
return (
|
||||||
<div key={i} className={styles.hour}>
|
<Row key={i} justifyContent="flex-end">
|
||||||
{label}
|
<Text color="muted" weight="bold">
|
||||||
</div>
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</Grid>
|
||||||
{data &&
|
{data &&
|
||||||
daysOfWeek.map((index: number) => {
|
daysOfWeek.map((index: number) => {
|
||||||
const day = data[index];
|
const day = data[index];
|
||||||
return (
|
return (
|
||||||
<div key={index} className={styles.day}>
|
<Grid
|
||||||
<div className={styles.header}>
|
rows="repeat(25, 20px)"
|
||||||
{format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
|
justifyContent="center"
|
||||||
</div>
|
alignItems="center"
|
||||||
{day?.map((hour: number, j) => {
|
key={index}
|
||||||
const pct = hour / max;
|
gap="1"
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
<Text weight="bold" align="center">
|
||||||
|
{format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
{day?.map((count: number, j) => {
|
||||||
|
const pct = count / max;
|
||||||
return (
|
return (
|
||||||
<div key={j} className={classNames(styles.cell)}>
|
<TooltipTrigger key={j} delay={0} isDisabled={count <= 0}>
|
||||||
{hour > 0 && (
|
<Focusable>
|
||||||
<TooltipTrigger delay={0}>
|
<Row backgroundColor="2" width="20px" height="20px" borderRadius="full">
|
||||||
<Focusable>
|
<Row
|
||||||
<div>
|
backgroundColor="primary"
|
||||||
<div
|
width="20px"
|
||||||
className={styles.block}
|
height="20px"
|
||||||
style={{ opacity: pct, transform: `scale(${pct})` }}
|
borderRadius="full"
|
||||||
/>
|
style={{ opacity: pct, transform: `scale(${pct})` }}
|
||||||
</div>
|
/>
|
||||||
</Focusable>
|
</Row>
|
||||||
<Tooltip placement="right">{`${formatMessage(
|
</Focusable>
|
||||||
labels.visitors,
|
<Tooltip placement="right">{`${formatMessage(
|
||||||
)}: ${hour}`}</Tooltip>
|
labels.visitors,
|
||||||
</TooltipTrigger>
|
)}: ${count}`}</Tooltip>
|
||||||
)}
|
</TooltipTrigger>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</Grid>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</Grid>
|
||||||
</LoadingPanel>
|
</LoadingPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
websiteId,
|
websiteId,
|
||||||
currency,
|
|
||||||
dateRange: { startDate, endDate, unit },
|
dateRange: { startDate, endDate, unit },
|
||||||
|
parameters: { currency },
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
if (!(await canViewWebsite(auth, websiteId))) {
|
if (!(await canViewWebsite(auth, websiteId))) {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export function Panel({
|
||||||
borderRadius="3"
|
borderRadius="3"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
position="relative"
|
position="relative"
|
||||||
|
overflowX="hidden"
|
||||||
gap
|
gap
|
||||||
{...props}
|
{...props}
|
||||||
style={{ ...style, ...(isFullscreen ? fullscreenStyles : {}) }}
|
style={{ ...style, ...(isFullscreen ? fullscreenStyles : {}) }}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export function TypeIcon({
|
||||||
<Row gap="3" alignItems="center">
|
<Row gap="3" alignItems="center">
|
||||||
<img
|
<img
|
||||||
src={`${process.env.basePath || ''}/images/${type}/${
|
src={`${process.env.basePath || ''}/images/${type}/${
|
||||||
value?.replaceAll(' ', '-').toLowerCase() || 'xx'
|
value?.replaceAll(' ', '-').toLowerCase() || 'unknown'
|
||||||
}.png`}
|
}.png`}
|
||||||
onError={e => {
|
onError={e => {
|
||||||
e.currentTarget.src = `${process.env.basePath || ''}/images/${type}/unknown.png`;
|
e.currentTarget.src = `${process.env.basePath || ''}/images/${type}/unknown.png`;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export function useFormat() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCountry = (value: string): string => {
|
const formatCountry = (value: string): string => {
|
||||||
return countryNames[value] || value;
|
return countryNames[value] || value || labels.unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatRegion = (value?: string): string => {
|
const formatRegion = (value?: string): string => {
|
||||||
|
|
|
||||||
34
src/components/input/CurrencySelect.tsx
Normal file
34
src/components/input/CurrencySelect.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { CURRENCIES } from '@/lib/constants';
|
||||||
|
import { ListItem, Select } from '@umami/react-zen';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function CurrencySelect({ value, onChange }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
items={CURRENCIES}
|
||||||
|
label={formatMessage(labels.currency)}
|
||||||
|
value={value}
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={onChange}
|
||||||
|
listProps={{ style: { maxHeight: '300px' } }}
|
||||||
|
onSearch={setSearch}
|
||||||
|
allowSearch
|
||||||
|
>
|
||||||
|
{CURRENCIES.map(({ id, name }) => {
|
||||||
|
if (search && !`${id}${name}`.toLowerCase().includes(search)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem key={id} id={id}>
|
||||||
|
{id} — {name}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}).filter(n => n)}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ export function CountriesTable({ ...props }: MetricsTableProps) {
|
||||||
value={(countryNames[code] && code) || code}
|
value={(countryNames[code] && code) || code}
|
||||||
label={formatCountry(code)}
|
label={formatCountry(code)}
|
||||||
>
|
>
|
||||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
<TypeIcon type="country" value={code} />
|
||||||
</FilterLink>
|
</FilterLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@ export function EventsChart({ websiteId, className, focusLabel }: EventsChartPro
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BarChart
|
<BarChart
|
||||||
minDate={startDate.toISOString()}
|
minDate={startDate}
|
||||||
maxDate={endDate.toISOString()}
|
maxDate={endDate}
|
||||||
className={className}
|
className={className}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
|
||||||
|
|
||||||
const startDate = new Date(+startTime);
|
const startDate = new Date(+startTime);
|
||||||
const endDate = new Date(+endTime);
|
const endDate = new Date(+endTime);
|
||||||
|
const unit = getMinimumUnit(startDate, endDate);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startDate,
|
startDate,
|
||||||
|
|
@ -152,7 +153,7 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
|
||||||
value,
|
value,
|
||||||
...parseDateValue(value),
|
...parseDateValue(value),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
unit: getMinimumUnit(startDate, endDate),
|
unit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ export function stringToColor(str: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCurrency(value: number, currency: string, locale = 'en-US') {
|
export function formatCurrency(value: number, currency: string, locale = 'en-US') {
|
||||||
let formattedValue;
|
let formattedValue: Intl.NumberFormat;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
formattedValue = new Intl.NumberFormat(locale, {
|
formattedValue = new Intl.NumberFormat(locale, {
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,9 @@ export const utmReportSchema = z.object({
|
||||||
|
|
||||||
export const revenueReportSchema = z.object({
|
export const revenueReportSchema = z.object({
|
||||||
type: z.literal('revenue'),
|
type: z.literal('revenue'),
|
||||||
|
parameters: z.object({
|
||||||
|
currency: z.string(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const attributionReportSchema = z.object({
|
export const attributionReportSchema = z.object({
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,18 @@ export interface RevenueCriteria {
|
||||||
currency: string;
|
currency: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RevenueResult {
|
||||||
|
chart: { x: string; t: string; y: number }[];
|
||||||
|
country: { name: string; value: number }[];
|
||||||
|
total: { sum: number; count: number; average: number; unique_count: number };
|
||||||
|
table: {
|
||||||
|
currency: string;
|
||||||
|
sum: number;
|
||||||
|
count: number;
|
||||||
|
unique_count: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRevenue(...args: [websiteId: string, criteria: RevenueCriteria]) {
|
export async function getRevenue(...args: [websiteId: string, criteria: RevenueCriteria]) {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
|
|
@ -19,23 +31,13 @@ export async function getRevenue(...args: [websiteId: string, criteria: RevenueC
|
||||||
async function relationalQuery(
|
async function relationalQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
criteria: RevenueCriteria,
|
criteria: RevenueCriteria,
|
||||||
): Promise<{
|
): Promise<RevenueResult> {
|
||||||
chart: { x: string; t: string; y: number }[];
|
|
||||||
country: { name: string; value: number }[];
|
|
||||||
total: { sum: number; count: number; unique_count: number };
|
|
||||||
table: {
|
|
||||||
currency: string;
|
|
||||||
sum: number;
|
|
||||||
count: number;
|
|
||||||
unique_count: number;
|
|
||||||
}[];
|
|
||||||
}> {
|
|
||||||
const { startDate, endDate, unit = 'day', currency } = criteria;
|
const { startDate, endDate, unit = 'day', currency } = criteria;
|
||||||
const { getDateSQL, rawQuery } = prisma;
|
const { getDateSQL, rawQuery } = prisma;
|
||||||
const db = getDatabaseType();
|
const db = getDatabaseType();
|
||||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||||
|
|
||||||
const chartRes = await rawQuery(
|
const chart = await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
event_name x,
|
event_name x,
|
||||||
|
|
@ -44,14 +46,14 @@ async function relationalQuery(
|
||||||
from revenue
|
from revenue
|
||||||
where website_id = {{websiteId::uuid}}
|
where website_id = {{websiteId::uuid}}
|
||||||
and created_at between {{startDate}} and {{endDate}}
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
and currency ${like} {{currency}}
|
and currency like {{currency}}
|
||||||
group by x, t
|
group by x, t
|
||||||
order by t
|
order by t
|
||||||
`,
|
`,
|
||||||
{ websiteId, startDate, endDate, unit, currency },
|
{ websiteId, startDate, endDate, unit, currency },
|
||||||
);
|
);
|
||||||
|
|
||||||
const countryRes = await rawQuery(
|
const country = await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
s.country as name,
|
s.country as name,
|
||||||
|
|
@ -67,7 +69,7 @@ async function relationalQuery(
|
||||||
{ websiteId, startDate, endDate, currency },
|
{ websiteId, startDate, endDate, currency },
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalRes = await rawQuery(
|
const total = await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
sum(revenue) as sum,
|
sum(revenue) as sum,
|
||||||
|
|
@ -81,7 +83,9 @@ async function relationalQuery(
|
||||||
{ websiteId, startDate, endDate, currency },
|
{ websiteId, startDate, endDate, currency },
|
||||||
).then(result => result?.[0]);
|
).then(result => result?.[0]);
|
||||||
|
|
||||||
const tableRes = await rawQuery(
|
total.average = total.count > 0 ? total.sum / total.count : 0;
|
||||||
|
|
||||||
|
const table = await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
currency,
|
currency,
|
||||||
|
|
@ -97,27 +101,17 @@ async function relationalQuery(
|
||||||
{ websiteId, startDate, endDate, unit, currency },
|
{ websiteId, startDate, endDate, unit, currency },
|
||||||
);
|
);
|
||||||
|
|
||||||
return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes };
|
return { chart, country, table, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickhouseQuery(
|
async function clickhouseQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
criteria: RevenueCriteria,
|
criteria: RevenueCriteria,
|
||||||
): Promise<{
|
): Promise<RevenueResult> {
|
||||||
chart: { x: string; t: string; y: number }[];
|
|
||||||
country: { name: string; value: number }[];
|
|
||||||
total: { sum: number; count: number; unique_count: number };
|
|
||||||
table: {
|
|
||||||
currency: string;
|
|
||||||
sum: number;
|
|
||||||
count: number;
|
|
||||||
unique_count: number;
|
|
||||||
}[];
|
|
||||||
}> {
|
|
||||||
const { startDate, endDate, unit = 'day', currency } = criteria;
|
const { startDate, endDate, unit = 'day', currency } = criteria;
|
||||||
const { getDateSQL, rawQuery } = clickhouse;
|
const { getDateSQL, rawQuery } = clickhouse;
|
||||||
|
|
||||||
const chartRes = await rawQuery<
|
const chart = await rawQuery<
|
||||||
{
|
{
|
||||||
x: string;
|
x: string;
|
||||||
t: string;
|
t: string;
|
||||||
|
|
@ -139,7 +133,7 @@ async function clickhouseQuery(
|
||||||
{ websiteId, startDate, endDate, unit, currency },
|
{ websiteId, startDate, endDate, unit, currency },
|
||||||
);
|
);
|
||||||
|
|
||||||
const countryRes = await rawQuery<
|
const country = await rawQuery<
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
|
@ -151,7 +145,7 @@ async function clickhouseQuery(
|
||||||
sum(w.revenue) as value
|
sum(w.revenue) as value
|
||||||
from website_revenue w
|
from website_revenue w
|
||||||
join (select distinct website_id, session_id, country
|
join (select distinct website_id, session_id, country
|
||||||
from website_event_stats_hourly
|
from website_event
|
||||||
where website_id = {websiteId:UUID}) s
|
where website_id = {websiteId:UUID}) s
|
||||||
on w.website_id = s.website_id
|
on w.website_id = s.website_id
|
||||||
and w.session_id = s.session_id
|
and w.session_id = s.session_id
|
||||||
|
|
@ -163,9 +157,8 @@ async function clickhouseQuery(
|
||||||
{ websiteId, startDate, endDate, currency },
|
{ websiteId, startDate, endDate, currency },
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalRes = await rawQuery<{
|
const total = await rawQuery<{
|
||||||
sum: number;
|
sum: number;
|
||||||
avg: number;
|
|
||||||
count: number;
|
count: number;
|
||||||
unique_count: number;
|
unique_count: number;
|
||||||
}>(
|
}>(
|
||||||
|
|
@ -182,11 +175,12 @@ async function clickhouseQuery(
|
||||||
{ websiteId, startDate, endDate, currency },
|
{ websiteId, startDate, endDate, currency },
|
||||||
).then(result => result?.[0]);
|
).then(result => result?.[0]);
|
||||||
|
|
||||||
const tableRes = await rawQuery<
|
total.average = total.count > 0 ? total.sum / total.count : 0;
|
||||||
|
|
||||||
|
const table = await rawQuery<
|
||||||
{
|
{
|
||||||
currency: string;
|
currency: string;
|
||||||
sum: number;
|
sum: number;
|
||||||
avg: number;
|
|
||||||
count: number;
|
count: number;
|
||||||
unique_count: number;
|
unique_count: number;
|
||||||
}[]
|
}[]
|
||||||
|
|
@ -206,5 +200,5 @@ async function clickhouseQuery(
|
||||||
{ websiteId, startDate, endDate, unit, currency },
|
{ websiteId, startDate, endDate, unit, currency },
|
||||||
);
|
);
|
||||||
|
|
||||||
return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes };
|
return { chart, country, table, total };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue