mirror of
https://github.com/umami-software/umami.git
synced 2026-02-22 05:25:36 +01:00
Updated reports.
This commit is contained in:
parent
28e872f219
commit
01bd21c5b4
75 changed files with 1373 additions and 980 deletions
98
src/app/(main)/websites/[websiteId]/reports/goals/Goal.tsx
Normal file
98
src/app/(main)/websites/[websiteId]/reports/goals/Goal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
12
src/app/(main)/websites/[websiteId]/reports/goals/page.tsx
Normal file
12
src/app/(main)/websites/[websiteId]/reports/goals/page.tsx
Normal 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',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue