diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js index ecdf9039..4d60627d 100644 --- a/components/input/DateFilter.js +++ b/components/input/DateFilter.js @@ -9,7 +9,7 @@ import useApi from 'hooks/useApi'; import useDateRange from 'hooks/useDateRange'; 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 { get } = useApi(); const [dateRange, setDateRange] = useDateRange(websiteId); @@ -23,7 +23,7 @@ export function DateFilter({ websiteId, value, className, onChange, isForm, alig if (data) { const websiteRange = { value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }; - if (!isForm) { + if (!onChange) { setDateRange(websiteRange); } @@ -32,15 +32,13 @@ export function DateFilter({ websiteId, value, className, onChange, isForm, alig } } } else if (value !== 'all') { - if (!isForm) { + if (!onChange) { setDateRange(value); } if (onChange) { onChange(value); } - - console.log(value); } } diff --git a/components/layout/SettingsLayout.js b/components/layout/SettingsLayout.js index c79f0909..d58154ca 100644 --- a/components/layout/SettingsLayout.js +++ b/components/layout/SettingsLayout.js @@ -15,6 +15,7 @@ export function SettingsLayout({ children }) { const items = [ { key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' }, { 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' }, { key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' }, ].filter(n => n); diff --git a/components/messages.js b/components/messages.js index 245e8591..fe4b833a 100644 --- a/components/messages.js +++ b/components/messages.js @@ -18,7 +18,9 @@ export const labels = defineMessages({ admin: { id: 'label.admin', defaultMessage: 'Administrator' }, confirm: { id: 'label.confirm', defaultMessage: 'Confirm' }, details: { id: 'label.details', defaultMessage: 'Details' }, + website: { id: 'label.website', defaultMessage: 'Website' }, websites: { id: 'label.websites', defaultMessage: 'Websites' }, + reports: { id: 'label.reports', defaultMessage: 'Reports' }, created: { id: 'label.created', defaultMessage: 'Created' }, edit: { id: 'label.edit', defaultMessage: 'Edit' }, name: { id: 'label.name', defaultMessage: 'Name' }, @@ -183,6 +185,10 @@ export const messages = defineMessages({ id: 'message.delete-website-warning', defaultMessage: 'All website data will be deleted.', }, + noResultsFound: { + id: 'messages.no-results-found', + defaultMessage: 'No results were found.', + }, noWebsitesConfigured: { id: 'messages.no-websites-configured', defaultMessage: 'You do not have any websites configured.', diff --git a/components/pages/reports/FunnelChart.js b/components/pages/reports/FunnelChart.js index 44c99092..8739e1da 100644 --- a/components/pages/reports/FunnelChart.js +++ b/components/pages/reports/FunnelChart.js @@ -1,43 +1,40 @@ import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph'; 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); useEffect(() => { - funnel.current.innerHTML = ''; + if (data && data.length > 0) { + funnel.current.innerHTML = ''; - const data = { - labels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], - subLabels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], - colors: [ - ['#FFB178', '#FF78B1', '#FF3C8E'], - ['#FFB178', '#FF78B1', '#FF3C8E'], - ['#A0BBFF', '#EC77FF'], - ['#A0F9FF', '#7795FF'], - ['#FFB178', '#FF78B1', '#FF3C8E'], - ], - values: [[3500], [3300], [2000], [600], [330]], - }; + const chartData = { + labels: data.map(a => a.url), + colors: ['#147af3', '#e0f2ff'], + values: data.map(a => a.count), + }; - const graph = new FunnelGraph({ - container: '.funnel', - gradientDirection: 'horizontal', - data: data, - displayPercent: true, - direction: 'Vertical', - width: 1000, - height: 350, - subLabelValue: 'values', - }); + const graph = new FunnelGraph({ + container: '.funnel', + gradientDirection: 'horizontal', + data: chartData, + displayPercent: true, + direction: 'Vertical', + width: 1000, + height: 350, + }); - graph.draw(); - }, []); + graph.draw(); + } + }, [data]); return ( -
- FunnelChart -
-
+ <> + {data?.length > 0 &&
} + {data?.length === 0 && } + ); } diff --git a/components/pages/reports/FunnelForm.js b/components/pages/reports/FunnelForm.js index 081105f1..56be6732 100644 --- a/components/pages/reports/FunnelForm.js +++ b/components/pages/reports/FunnelForm.js @@ -1,12 +1,8 @@ -import { useMutation } from '@tanstack/react-query'; import DateFilter from 'components/input/DateFilter'; import WebsiteSelect from 'components/input/WebsiteSelect'; -import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; -import useUser from 'hooks/useUser'; import { parseDateRange } from 'lib/date'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Button, Form, @@ -17,15 +13,15 @@ import { TextField, } from 'react-basics'; import styles from './FunnelForm.module.css'; -import { getNextInternalQuery } from 'next/dist/server/request-meta'; export function FunnelForm({ onSearch }) { - const { formatMessage, labels, getMessage } = useMessages(); - const [dateRange, setDateRange] = useState(null); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); + const { formatMessage, labels } = useMessages(); + const [dateRange, setDateRange] = useState(''); + const [startAt, setStartAt] = useState(); + const [endAt, setEndAt] = useState(); const [urls, setUrls] = useState(['']); const [websiteId, setWebsiteId] = useState(''); + const [window, setWindow] = useState(60); const handleSubmit = async data => { onSearch(data); @@ -35,14 +31,16 @@ export function FunnelForm({ onSearch }) { const { startDate, endDate } = parseDateRange(value); setDateRange(value); - setStartDate(startDate); - setEndDate(endDate); + setStartAt(startDate.getTime()); + setEndAt(endDate.getTime()); }; - const handleAddUrl = () => setUrls([...urls, 'meow']); + const handleAddUrl = () => setUrls([...urls, '']); const handleRemoveUrl = i => setUrls(urls.splice(i, 1)); + const handleWindowChange = value => setWindow(value.target.value); + const handleUrlChange = (value, i) => { const nextUrls = [...urls]; @@ -55,13 +53,14 @@ export function FunnelForm({ onSearch }) {
- + setWebsiteId(value)} /> @@ -73,28 +72,39 @@ export function FunnelForm({ onSearch }) { value={dateRange} alignment="start" onChange={handleDateChange} + isF /> - + - - + + + + + + + {urls.map((a, i) => ( - - + handleUrlChange(value, i)} /> + ))} - Search + + Search + diff --git a/components/pages/reports/FunnelForm.module.css b/components/pages/reports/FunnelForm.module.css index 9a8d924b..2706a99a 100644 --- a/components/pages/reports/FunnelForm.module.css +++ b/components/pages/reports/FunnelForm.module.css @@ -17,3 +17,12 @@ min-height: 0px; max-height: 0px; } + +.urlFormRow { + flex-direction: row; + gap: 0em; +} + +.urlFormRow label { + min-width: 80px; +} diff --git a/components/pages/reports/FunnelPage.js b/components/pages/reports/FunnelPage.js index 3cfa63a8..c715d857 100644 --- a/components/pages/reports/FunnelPage.js +++ b/components/pages/reports/FunnelPage.js @@ -1,26 +1,38 @@ +import { useMutation } from '@tanstack/react-query'; 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 FunnelTable from './FunnelTable'; import FunnelForm from './FunnelForm'; +import styles from './FunnelPage.module.css'; 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 // Get DATA + mutate(data, { + onSuccess: async data => { + setData(data); + }, + }); } return ( - funnelPage + + + {/* */} - website / start/endDate urls: [] - - {/* {!chartLoaded && } - {chartLoaded && ( - <> - {!view && } - {view && } - - )} */} +
+

Filters

+ +
); } diff --git a/components/pages/reports/FunnelPage.module.css b/components/pages/reports/FunnelPage.module.css new file mode 100644 index 00000000..aed66b74 --- /dev/null +++ b/components/pages/reports/FunnelPage.module.css @@ -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; +} diff --git a/components/pages/reports/FunnelTable.js b/components/pages/reports/FunnelTable.js new file mode 100644 index 00000000..fa40fd13 --- /dev/null +++ b/components/pages/reports/FunnelTable.js @@ -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 ; +} + +export default DevicesTable; diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index ee5ff4b4..318d455d 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -17,9 +17,9 @@ model User { updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - website Website[] - teamUser TeamUser[] - ReportTemplate UserReport[] + website Website[] + teamUser TeamUser[] + Report Report[] @@map("user") } @@ -60,6 +60,7 @@ model Website { user User? @relation(fields: [userId], references: [id]) teamWebsite TeamWebsite[] eventData EventData[] + Report Report[] @@index([userId]) @@index([createdAt]) @@ -156,18 +157,21 @@ model TeamWebsite { @@map("team_website") } -model UserReport { - id String @id() @unique() @map("report_id") @db.Uuid - userId String @map("user_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - reportName String @map("report_name") @db.VarChar(200) - templateName String @map("template_name") @db.VarChar(200) - parameters String @map("parameters") @db.VarChar(6000) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) +model Report { + id String @id() @unique() @map("report_id") @db.Uuid + userId String @map("user_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + type String @map("type") @db.VarChar(200) + name String @map("name") @db.VarChar(200) + description String @map("description") @db.VarChar(500) + parameters String @map("parameters") @db.VarChar(6000) + 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]) - @@map("user_report") + @@index([websiteId]) + @@map("report") } diff --git a/pages/api/reports/funnel.ts b/pages/api/reports/funnel.ts index ee450eb6..6e3eb602 100644 --- a/pages/api/reports/funnel.ts +++ b/pages/api/reports/funnel.ts @@ -40,7 +40,7 @@ export default async ( startDate, endDate, urls, - windowMinutes: window, + windowMinutes: +window, }); return ok(res, data); diff --git a/pages/settings/reports/funnel.js b/pages/settings/reports/funnel.js new file mode 100644 index 00000000..d8d7a5b8 --- /dev/null +++ b/pages/settings/reports/funnel.js @@ -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 ( + + + + + + ); +} diff --git a/pages/reports/funnel.js b/pages/settings/reports/index.js similarity index 76% rename from pages/reports/funnel.js rename to pages/settings/reports/index.js index d4bf7dd2..ce0a3726 100644 --- a/pages/reports/funnel.js +++ b/pages/settings/reports/index.js @@ -2,6 +2,7 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; import FunnelPage from 'components/pages/reports/FunnelPage'; import useMessages from 'hooks/useMessages'; +import SettingsLayout from 'components/layout/SettingsLayout'; export default function DetailsPage() { // const { formatMessage, labels } = useMessages(); @@ -15,8 +16,10 @@ export default function DetailsPage() { // return {/* */}; return ( -
- -
+ + + + + ); }