Funnel Table/Chart hooked up.

This commit is contained in:
Brian Cao 2023-05-14 21:38:03 -07:00
parent 1130bca195
commit 07cb9f621d
13 changed files with 170 additions and 87 deletions

View File

@ -9,7 +9,7 @@ import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export function DateFilter({ websiteId, value, className, onChange, isForm, alignment }) { export function DateFilter({ websiteId, value, className, onChange, alignment }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { get } = useApi(); const { get } = useApi();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
@ -23,7 +23,7 @@ export function DateFilter({ websiteId, value, className, onChange, isForm, alig
if (data) { if (data) {
const websiteRange = { value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }; const websiteRange = { value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) };
if (!isForm) { if (!onChange) {
setDateRange(websiteRange); setDateRange(websiteRange);
} }
@ -32,15 +32,13 @@ export function DateFilter({ websiteId, value, className, onChange, isForm, alig
} }
} }
} else if (value !== 'all') { } else if (value !== 'all') {
if (!isForm) { if (!onChange) {
setDateRange(value); setDateRange(value);
} }
if (onChange) { if (onChange) {
onChange(value); onChange(value);
} }
console.log(value);
} }
} }

View File

@ -15,6 +15,7 @@ export function SettingsLayout({ children }) {
const items = [ const items = [
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' }, { key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
{ key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' }, { key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
{ key: 'reports', label: 'Reports', url: '/settings/reports/funnel' },
user.isAdmin && { key: 'users', label: formatMessage(labels.users), url: '/settings/users' }, user.isAdmin && { key: 'users', label: formatMessage(labels.users), url: '/settings/users' },
{ key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' }, { key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' },
].filter(n => n); ].filter(n => n);

View File

@ -18,7 +18,9 @@ export const labels = defineMessages({
admin: { id: 'label.admin', defaultMessage: 'Administrator' }, admin: { id: 'label.admin', defaultMessage: 'Administrator' },
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' }, confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
details: { id: 'label.details', defaultMessage: 'Details' }, details: { id: 'label.details', defaultMessage: 'Details' },
website: { id: 'label.website', defaultMessage: 'Website' },
websites: { id: 'label.websites', defaultMessage: 'Websites' }, websites: { id: 'label.websites', defaultMessage: 'Websites' },
reports: { id: 'label.reports', defaultMessage: 'Reports' },
created: { id: 'label.created', defaultMessage: 'Created' }, created: { id: 'label.created', defaultMessage: 'Created' },
edit: { id: 'label.edit', defaultMessage: 'Edit' }, edit: { id: 'label.edit', defaultMessage: 'Edit' },
name: { id: 'label.name', defaultMessage: 'Name' }, name: { id: 'label.name', defaultMessage: 'Name' },
@ -183,6 +185,10 @@ export const messages = defineMessages({
id: 'message.delete-website-warning', id: 'message.delete-website-warning',
defaultMessage: 'All website data will be deleted.', defaultMessage: 'All website data will be deleted.',
}, },
noResultsFound: {
id: 'messages.no-results-found',
defaultMessage: 'No results were found.',
},
noWebsitesConfigured: { noWebsitesConfigured: {
id: 'messages.no-websites-configured', id: 'messages.no-websites-configured',
defaultMessage: 'You do not have any websites configured.', defaultMessage: 'You do not have any websites configured.',

View File

@ -1,43 +1,40 @@
import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph'; import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useMessages from 'hooks/useMessages';
export default function FunnelChart() { export default function FunnelChart({ data }) {
const { formatMessage, labels, messages } = useMessages();
const funnel = useRef(null); const funnel = useRef(null);
useEffect(() => { useEffect(() => {
funnel.current.innerHTML = ''; if (data && data.length > 0) {
funnel.current.innerHTML = '';
const data = { const chartData = {
labels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], labels: data.map(a => a.url),
subLabels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], colors: ['#147af3', '#e0f2ff'],
colors: [ values: data.map(a => a.count),
['#FFB178', '#FF78B1', '#FF3C8E'], };
['#FFB178', '#FF78B1', '#FF3C8E'],
['#A0BBFF', '#EC77FF'],
['#A0F9FF', '#7795FF'],
['#FFB178', '#FF78B1', '#FF3C8E'],
],
values: [[3500], [3300], [2000], [600], [330]],
};
const graph = new FunnelGraph({ const graph = new FunnelGraph({
container: '.funnel', container: '.funnel',
gradientDirection: 'horizontal', gradientDirection: 'horizontal',
data: data, data: chartData,
displayPercent: true, displayPercent: true,
direction: 'Vertical', direction: 'Vertical',
width: 1000, width: 1000,
height: 350, height: 350,
subLabelValue: 'values', });
});
graph.draw(); graph.draw();
}, []); }
}, [data]);
return ( return (
<div> <>
FunnelChart {data?.length > 0 && <div className="funnel" ref={funnel} />}
<div className="funnel" ref={funnel} /> {data?.length === 0 && <EmptyPlaceholder message={formatMessage(messages.noResultsFound)} />}
</div> </>
); );
} }

View File

@ -1,12 +1,8 @@
import { useMutation } from '@tanstack/react-query';
import DateFilter from 'components/input/DateFilter'; import DateFilter from 'components/input/DateFilter';
import WebsiteSelect from 'components/input/WebsiteSelect'; import WebsiteSelect from 'components/input/WebsiteSelect';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import useUser from 'hooks/useUser';
import { parseDateRange } from 'lib/date'; import { parseDateRange } from 'lib/date';
import { useRouter } from 'next/router'; import { useState } from 'react';
import { useEffect, useState } from 'react';
import { import {
Button, Button,
Form, Form,
@ -17,15 +13,15 @@ import {
TextField, TextField,
} from 'react-basics'; } from 'react-basics';
import styles from './FunnelForm.module.css'; import styles from './FunnelForm.module.css';
import { getNextInternalQuery } from 'next/dist/server/request-meta';
export function FunnelForm({ onSearch }) { export function FunnelForm({ onSearch }) {
const { formatMessage, labels, getMessage } = useMessages(); const { formatMessage, labels } = useMessages();
const [dateRange, setDateRange] = useState(null); const [dateRange, setDateRange] = useState('');
const [startDate, setStartDate] = useState(null); const [startAt, setStartAt] = useState();
const [endDate, setEndDate] = useState(null); const [endAt, setEndAt] = useState();
const [urls, setUrls] = useState(['']); const [urls, setUrls] = useState(['']);
const [websiteId, setWebsiteId] = useState(''); const [websiteId, setWebsiteId] = useState('');
const [window, setWindow] = useState(60);
const handleSubmit = async data => { const handleSubmit = async data => {
onSearch(data); onSearch(data);
@ -35,14 +31,16 @@ export function FunnelForm({ onSearch }) {
const { startDate, endDate } = parseDateRange(value); const { startDate, endDate } = parseDateRange(value);
setDateRange(value); setDateRange(value);
setStartDate(startDate); setStartAt(startDate.getTime());
setEndDate(endDate); setEndAt(endDate.getTime());
}; };
const handleAddUrl = () => setUrls([...urls, 'meow']); const handleAddUrl = () => setUrls([...urls, '']);
const handleRemoveUrl = i => setUrls(urls.splice(i, 1)); const handleRemoveUrl = i => setUrls(urls.splice(i, 1));
const handleWindowChange = value => setWindow(value.target.value);
const handleUrlChange = (value, i) => { const handleUrlChange = (value, i) => {
const nextUrls = [...urls]; const nextUrls = [...urls];
@ -55,13 +53,14 @@ export function FunnelForm({ onSearch }) {
<Form <Form
values={{ values={{
websiteId, websiteId,
startDate, startAt,
endDate, endAt,
urls, urls,
window,
}} }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<FormRow label="website"> <FormRow label={formatMessage(labels.website)}>
<WebsiteSelect websiteId={websiteId} onSelect={value => setWebsiteId(value)} /> <WebsiteSelect websiteId={websiteId} onSelect={value => setWebsiteId(value)} />
<FormInput name="websiteId" rules={{ required: formatMessage(labels.required) }}> <FormInput name="websiteId" rules={{ required: formatMessage(labels.required) }}>
<TextField value={websiteId} className={styles.hiddenInput} /> <TextField value={websiteId} className={styles.hiddenInput} />
@ -73,28 +72,39 @@ export function FunnelForm({ onSearch }) {
value={dateRange} value={dateRange}
alignment="start" alignment="start"
onChange={handleDateChange} onChange={handleDateChange}
isF
/> />
<FormInput <FormInput
name="startDate" name="startAt"
className={styles.hiddenInput} className={styles.hiddenInput}
rules={{ required: formatMessage(labels.required) }} rules={{ required: formatMessage(labels.required) }}
> >
<TextField value={startDate} /> <TextField value={startAt} />
</FormInput> </FormInput>
<FormInput name="endDate" rules={{ required: formatMessage(labels.required) }}> <FormInput name="endAt" rules={{ required: formatMessage(labels.required) }}>
<TextField value={endDate} className={styles.hiddenInput} /> <TextField value={endAt} className={styles.hiddenInput} />
</FormInput>
</FormRow>
<FormRow label="Window (minutes)">
<FormInput
name="window"
rules={{ required: formatMessage(labels.required), pattern: /[0-9]+/ }}
>
<TextField value={window} onChange={handleWindowChange} />
</FormInput> </FormInput>
</FormRow> </FormRow>
<Button onClick={handleAddUrl}>Add URL</Button> <Button onClick={handleAddUrl}>Add URL</Button>
{urls.map((a, i) => ( {urls.map((a, i) => (
<FormRow key={`url${i}`} label={`URL ${i + 1}`}> <FormRow className={styles.urlFormRow} key={`url${i}`} label={`URL ${i + 1}`}>
<Button onClick={() => handleRemoveUrl(i)}>Remove URL</Button>
<TextField value={urls[i]} onChange={value => handleUrlChange(value, i)} /> <TextField value={urls[i]} onChange={value => handleUrlChange(value, i)} />
<Button onClick={() => handleRemoveUrl(i)}>Remove URL</Button>
</FormRow> </FormRow>
))} ))}
<FormButtons> <FormButtons>
<SubmitButton variant="primary">Search</SubmitButton> <SubmitButton variant="primary" disabled={false}>
Search
</SubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
</> </>

View File

@ -17,3 +17,12 @@
min-height: 0px; min-height: 0px;
max-height: 0px; max-height: 0px;
} }
.urlFormRow {
flex-direction: row;
gap: 0em;
}
.urlFormRow label {
min-width: 80px;
}

View File

@ -1,26 +1,38 @@
import { useMutation } from '@tanstack/react-query';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import useApi from 'hooks/useApi';
import { useState } from 'react';
import FunnelChart from './FunnelChart'; import FunnelChart from './FunnelChart';
import FunnelTable from './FunnelTable';
import FunnelForm from './FunnelForm'; import FunnelForm from './FunnelForm';
import styles from './FunnelPage.module.css';
export default function FunnelPage() { export default function FunnelPage() {
function handleOnSearch() { const { post } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/reports/funnel', data));
const [data, setData] = useState();
function handleOnSearch(data) {
// do API CALL to api/reports/funnel to get funnelData // do API CALL to api/reports/funnel to get funnelData
// Get DATA // Get DATA
mutate(data, {
onSuccess: async data => {
setData(data);
},
});
} }
return ( return (
<Page> <Page>
funnelPage <PageHeader title="Funnel Report"></PageHeader>
<FunnelChart data={data} />
<FunnelTable data={data} />
{/* <ReportForm /> */} {/* <ReportForm /> */}
<FunnelForm onSearchClick={handleOnSearch} /> website / start/endDate urls: [] <div>
<FunnelChart /> <h2>Filters</h2>
{/* {!chartLoaded && <Loading icon="dots" style={{ minHeight: 300 }} />} <FunnelForm onSearch={handleOnSearch} />
{chartLoaded && ( </div>
<>
{!view && <WebsiteTableView websiteId={websiteId} />}
{view && <WebsiteMenuView websiteId={websiteId} />}
</>
)} */}
</Page> </Page>
); );
} }

View File

@ -0,0 +1,10 @@
.filters {
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid var(--base400);
border-radius: var(--border-radius);
line-height: 32px;
padding: 10px;
overflow: hidden;
}

View File

@ -0,0 +1,17 @@
import DataTable from 'components/metrics/DataTable';
import useMessages from 'hooks/useMessages';
import { useState } from 'react';
export function DevicesTable({ ...props }) {
const { formatMessage, labels } = useMessages();
const { data } = props;
const tableData =
data?.map(a => ({ x: a.url, y: a.count, z: (a.count / data[0].count) * 100 })) || [];
console.log(tableData);
return <DataTable data={tableData} title="Url" type="device" />;
}
export default DevicesTable;

View File

@ -17,9 +17,9 @@ model User {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
website Website[] website Website[]
teamUser TeamUser[] teamUser TeamUser[]
ReportTemplate UserReport[] Report Report[]
@@map("user") @@map("user")
} }
@ -60,6 +60,7 @@ model Website {
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
teamWebsite TeamWebsite[] teamWebsite TeamWebsite[]
eventData EventData[] eventData EventData[]
Report Report[]
@@index([userId]) @@index([userId])
@@index([createdAt]) @@index([createdAt])
@ -156,18 +157,21 @@ model TeamWebsite {
@@map("team_website") @@map("team_website")
} }
model UserReport { model Report {
id String @id() @unique() @map("report_id") @db.Uuid id String @id() @unique() @map("report_id") @db.Uuid
userId String @map("user_id") @db.Uuid userId String @map("user_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid websiteId String @map("website_id") @db.Uuid
reportName String @map("report_name") @db.VarChar(200) type String @map("type") @db.VarChar(200)
templateName String @map("template_name") @db.VarChar(200) name String @map("name") @db.VarChar(200)
parameters String @map("parameters") @db.VarChar(6000) description String @map("description") @db.VarChar(500)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) parameters String @map("parameters") @db.VarChar(6000)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
website Website @relation(fields: [websiteId], references: [id])
@@index([userId]) @@index([userId])
@@map("user_report") @@index([websiteId])
@@map("report")
} }

View File

@ -40,7 +40,7 @@ export default async (
startDate, startDate,
endDate, endDate,
urls, urls,
windowMinutes: window, windowMinutes: +window,
}); });
return ok(res, data); return ok(res, data);

View File

@ -0,0 +1,16 @@
import AppLayout from 'components/layout/AppLayout';
import SettingsLayout from 'components/layout/SettingsLayout';
import FunnelPage from 'components/pages/reports/FunnelPage';
import useMessages from 'hooks/useMessages';
export default function DetailsPage() {
const { formatMessage, labels } = useMessages();
return (
<AppLayout title={`${formatMessage(labels.settings)} - ${formatMessage(labels.reports)}`}>
<SettingsLayout>
<FunnelPage />
</SettingsLayout>
</AppLayout>
);
}

View File

@ -2,6 +2,7 @@ import { useRouter } from 'next/router';
import AppLayout from 'components/layout/AppLayout'; import AppLayout from 'components/layout/AppLayout';
import FunnelPage from 'components/pages/reports/FunnelPage'; import FunnelPage from 'components/pages/reports/FunnelPage';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import SettingsLayout from 'components/layout/SettingsLayout';
export default function DetailsPage() { export default function DetailsPage() {
// const { formatMessage, labels } = useMessages(); // const { formatMessage, labels } = useMessages();
@ -15,8 +16,10 @@ export default function DetailsPage() {
// return <AppLayout title={formatMessage(labels.websites)}>{/* <FunnelPage /> */}</AppLayout>; // return <AppLayout title={formatMessage(labels.websites)}>{/* <FunnelPage /> */}</AppLayout>;
return ( return (
<div> <AppLayout>
<FunnelPage /> <SettingsLayout>
</div> <FunnelPage />
</SettingsLayout>
</AppLayout>
); );
} }