Updated reports.

This commit is contained in:
Mike Cao 2025-06-08 22:21:28 -07:00
parent 28e872f219
commit 01bd21c5b4
75 changed files with 1373 additions and 980 deletions

View file

@ -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<GoalData>(type, {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters,
});
const isPage = parameters?.type === 'page';
return (
<LoadingPanel isEmpty={!data} isLoading={isLoading} error={error}>
<Grid gap>
<Grid columns="1fr auto" gap>
<Column gap>
<Row>
<Text size="4" weight="bold">
{name}
</Text>
</Row>
</Column>
<Column>
<ReportEditButton id={id} name={name} type={type}>
{({ close }) => {
return (
<Dialog
title={formatMessage(labels.goal)}
variant="modal"
style={{ minHeight: 300, minWidth: 400 }}
>
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
</Dialog>
);
}}
</ReportEditButton>
</Column>
</Grid>
<Row alignItems="center" justifyContent="space-between" gap>
<Text color="muted">
{formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)}
</Text>
<Text color="muted">{formatMessage(labels.conversionRate)}</Text>
</Row>
<Row alignItems="center" justifyContent="space-between" gap>
<Row alignItems="center" gap>
<Icon>{parameters.type === 'page' ? <File /> : <Lightning />}</Icon>
<Text>{parameters.value}</Text>
</Row>
<Row alignItems="center" gap>
<Icon>
<User />
</Icon>
<Text title={`${data?.num} / ${data?.total}`}>{`${formatLongNumber(
data?.num,
)} / ${formatLongNumber(data?.total)}`}</Text>
</Row>
</Row>
<Row alignItems="center" gap="6">
<ProgressBar
value={data?.num || 0}
minValue={0}
maxValue={data?.total || 1}
style={{ width: '100%' }}
/>
<Text weight="bold" size="7">
{data?.total ? Math.round((+data?.num / +data?.total) * 100) : '0'}%
</Text>
</Row>
</Grid>
</LoadingPanel>
);
}

View file

@ -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 (
<DialogTrigger>
<Button variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.goal)}</Text>
</Button>
<Modal>
<Dialog
variant="modal"
title={formatMessage(labels.goal)}
style={{ minHeight: 375, minWidth: 400 }}
>
{({ close }) => <GoalEditForm websiteId={websiteId} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
);
}

View file

@ -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 <Loading position="page" />;
}
const defaultValues = {
name: data?.name || '',
type: data?.parameters?.type || 'page',
value: data?.parameters?.value || '',
};
return (
<Form onSubmit={handleSubmit} error={error?.message} defaultValues={defaultValues}>
{({ watch }) => {
const watchType = watch('type');
return (
<>
<FormField
name="name"
label={formatMessage(labels.name)}
rules={{ required: formatMessage(labels.required) }}
>
<TextField />
</FormField>
<FormField
name="type"
label={formatMessage(labels.type)}
rules={{ required: formatMessage(labels.required) }}
>
<RadioGroup orientation="horizontal" variant="box">
<Grid columns="1fr 1fr" flexGrow={1} gap>
<Radio value="page">
<Icon>
<File />
</Icon>
<Text>{formatMessage(labels.page)}</Text>
</Radio>
<Radio value="event">
<Icon>
<Lightning />
</Icon>
<Text>{formatMessage(labels.event)}</Text>
</Radio>
</Grid>
</RadioGroup>
</FormField>
<FormField
name="value"
label={formatMessage(watchType === 'event' ? labels.eventName : labels.path)}
rules={{ required: formatMessage(labels.required) }}
>
<TextField />
</FormField>
<FormButtons>
<Button onPress={onClose} isDisabled={isPending}>
{formatMessage(labels.cancel)}
</Button>
<FormSubmitButton>{formatMessage(labels.save)}</FormSubmitButton>
</FormButtons>
</>
);
}}
</Form>
);
}

View file

@ -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 (
<Column gap>
<WebsiteControls websiteId={websiteId} />
<LoadingPanel isEmpty={!result?.data} isLoading={!result}>
<SectionHeader>
<GoalAddButton websiteId={websiteId} />
</SectionHeader>
<Grid columns="1fr 1fr" gap>
{result?.data?.map((report: any) => (
<Panel key={report.id}>
<Goal {...report} reportId={report.id} startDate={startDate} endDate={endDate} />
</Panel>
))}
</Grid>
</LoadingPanel>
</Column>
);
}

View file

@ -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 <GoalsPage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Goals',
};