From 202702b1c78aee1d7b6d46d37423b5761119680d Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 20 Aug 2025 15:19:09 +0000 Subject: [PATCH] feat: team analytics --- .../components/analytics/analytics-charts.tsx | 67 +++++++++++ .../analytics/analytics-date-filter.tsx | 88 +++++++++++++++ .../analytics/analytics-metrics.tsx | 105 ++++++++++++++++++ .../components/analytics/analytics-page.tsx | 71 ++++++++++++ .../app/components/general/menu-switcher.tsx | 6 + .../components/general/org-menu-switcher.tsx | 17 ++- .../general/settings-nav-desktop.tsx | 14 +++ .../general/settings-nav-mobile.tsx | 14 +++ apps/remix/app/hooks/use-analytics-filter.ts | 24 ++++ .../_dynamic_personal_routes+/analytics.tsx | 55 +++++++++ .../_authenticated+/t.$teamUrl+/analytics.tsx | 88 +++++++++++++++ .../analytics/get-document-stats.ts | 68 ++++++++++++ .../analytics/get-monthly-stats.ts | 80 +++++++++++++ .../analytics/get-recipient-stats.ts | 75 +++++++++++++ packages/lib/server-only/analytics/index.ts | 4 + packages/lib/server-only/analytics/types.ts | 63 +++++++++++ 16 files changed, 834 insertions(+), 5 deletions(-) create mode 100644 apps/remix/app/components/analytics/analytics-charts.tsx create mode 100644 apps/remix/app/components/analytics/analytics-date-filter.tsx create mode 100644 apps/remix/app/components/analytics/analytics-metrics.tsx create mode 100644 apps/remix/app/components/analytics/analytics-page.tsx create mode 100644 apps/remix/app/hooks/use-analytics-filter.ts create mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/analytics.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/analytics.tsx create mode 100644 packages/lib/server-only/analytics/get-document-stats.ts create mode 100644 packages/lib/server-only/analytics/get-monthly-stats.ts create mode 100644 packages/lib/server-only/analytics/get-recipient-stats.ts create mode 100644 packages/lib/server-only/analytics/index.ts create mode 100644 packages/lib/server-only/analytics/types.ts diff --git a/apps/remix/app/components/analytics/analytics-charts.tsx b/apps/remix/app/components/analytics/analytics-charts.tsx new file mode 100644 index 000000000..d75d99ea5 --- /dev/null +++ b/apps/remix/app/components/analytics/analytics-charts.tsx @@ -0,0 +1,67 @@ +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; + +import type { MonthlyStats } from '@documenso/lib/server-only/analytics'; + +import { MonthlyActiveUsersChart } from '~/components/general/admin-monthly-active-user-charts'; + +export type AnalyticsChartsProps = { + monthlyData?: MonthlyStats; + showActiveUsers?: boolean; +}; + +export const AnalyticsCharts = ({ monthlyData, showActiveUsers = false }: AnalyticsChartsProps) => { + const { _ } = useLingui(); + + if (!monthlyData) { + return null; + } + + // Ensure all data has cume_count for chart compatibility + const formatDataForChart = (data: MonthlyStats) => { + return data.map((item) => ({ + ...item, + count: item.count, + cume_count: item.cume_count || 0, + })); + }; + + return ( +
+

+ Charts +

+
+ {showActiveUsers && ( + <> + + + + )} + + {!showActiveUsers && ( + <> + + ({ ...d, count: d.signed_count || 0 })), + )} + /> + + )} +
+
+ ); +}; diff --git a/apps/remix/app/components/analytics/analytics-date-filter.tsx b/apps/remix/app/components/analytics/analytics-date-filter.tsx new file mode 100644 index 000000000..beef1c918 --- /dev/null +++ b/apps/remix/app/components/analytics/analytics-date-filter.tsx @@ -0,0 +1,88 @@ +import { useMemo } from 'react'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { DateTime } from 'luxon'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; + +export type DateFilterPeriod = 'all' | 'year' | 'month' | 'week' | 'day'; + +export type AnalyticsDateFilterProps = { + value: DateFilterPeriod; + onChange: (value: DateFilterPeriod) => void; + className?: string; +}; + +export const AnalyticsDateFilter = ({ value, onChange, className }: AnalyticsDateFilterProps) => { + const { _ } = useLingui(); + + const options = useMemo( + () => [ + { value: 'all' as const, label: _(msg`All Time`) }, + { value: 'year' as const, label: _(msg`This Year`) }, + { value: 'month' as const, label: _(msg`This Month`) }, + { value: 'week' as const, label: _(msg`This Week`) }, + { value: 'day' as const, label: _(msg`Today`) }, + ], + [_], + ); + + const selectedOption = options.find((option) => option.value === value); + + return ( +
+ +
+ ); +}; + +export const getDateRangeFromPeriod = (period: DateFilterPeriod) => { + const now = DateTime.now(); + + switch (period) { + case 'day': + return { + dateFrom: now.startOf('day').toJSDate(), + dateTo: now.endOf('day').toJSDate(), + }; + case 'week': + return { + dateFrom: now.startOf('week').toJSDate(), + dateTo: now.endOf('week').toJSDate(), + }; + case 'month': + return { + dateFrom: now.startOf('month').toJSDate(), + dateTo: now.endOf('month').toJSDate(), + }; + case 'year': + return { + dateFrom: now.startOf('year').toJSDate(), + dateTo: now.endOf('year').toJSDate(), + }; + case 'all': + default: + return { + dateFrom: undefined, + dateTo: undefined, + }; + } +}; diff --git a/apps/remix/app/components/analytics/analytics-metrics.tsx b/apps/remix/app/components/analytics/analytics-metrics.tsx new file mode 100644 index 000000000..3e654d703 --- /dev/null +++ b/apps/remix/app/components/analytics/analytics-metrics.tsx @@ -0,0 +1,105 @@ +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { + File, + FileCheck, + FileClock, + FileEdit, + Mail, + MailOpen, + PenTool, + UserSquare2, +} from 'lucide-react'; + +import type { DocumentStats, RecipientStats } from '@documenso/lib/server-only/analytics'; +import { cn } from '@documenso/ui/lib/utils'; + +import { CardMetric } from '~/components/general/metric-card'; + +export type AnalyticsMetricsProps = { + docStats: DocumentStats; + recipientStats: RecipientStats; + isLoading?: boolean; + className?: string; +}; + +export const AnalyticsMetrics = ({ + docStats, + recipientStats, + isLoading = false, + className, +}: AnalyticsMetricsProps) => { + const { _ } = useLingui(); + + return ( +
+ {/* Overview Metrics */} +
+ + + + +
+ + {/* Document Metrics Section */} +
+

+ Document metrics +

+ +
+ + + +
+
+ + {/* Recipients Metrics Section */} +
+

+ Recipients metrics +

+ +
+ + + + +
+
+
+ ); +}; diff --git a/apps/remix/app/components/analytics/analytics-page.tsx b/apps/remix/app/components/analytics/analytics-page.tsx new file mode 100644 index 000000000..a7553b547 --- /dev/null +++ b/apps/remix/app/components/analytics/analytics-page.tsx @@ -0,0 +1,71 @@ +import { Trans } from '@lingui/react/macro'; +import { useNavigation } from 'react-router'; + +import type { + DocumentStats, + MonthlyStats, + RecipientStats, +} from '@documenso/lib/server-only/analytics'; + +import { AnalyticsCharts } from './analytics-charts'; +import { AnalyticsDateFilter, type DateFilterPeriod } from './analytics-date-filter'; +import { AnalyticsMetrics } from './analytics-metrics'; + +export type AnalyticsPageData = { + docStats: DocumentStats; + recipientStats: RecipientStats; + monthlyData?: MonthlyStats; + period: DateFilterPeriod; +}; + +export type AnalyticsPageProps = { + title: string; + subtitle: string; + data: AnalyticsPageData; + showCharts?: boolean; + onPeriodChange: (period: DateFilterPeriod) => void; + containerClassName?: string; +}; + +export const AnalyticsPage = ({ + title, + subtitle, + data, + showCharts = true, + onPeriodChange, + containerClassName = 'mx-auto w-full max-w-screen-xl px-4 md:px-8', +}: AnalyticsPageProps) => { + const navigation = useNavigation(); + const { docStats, recipientStats, monthlyData, period } = data; + const isLoading = navigation.state === 'loading'; + + return ( +
+
+
+

+ {title} +

+

+ {subtitle} +

+
+ +
+ + + + {showCharts && ( + + )} +
+ ); +}; diff --git a/apps/remix/app/components/general/menu-switcher.tsx b/apps/remix/app/components/general/menu-switcher.tsx index 726a970eb..5e40da428 100644 --- a/apps/remix/app/components/general/menu-switcher.tsx +++ b/apps/remix/app/components/general/menu-switcher.tsx @@ -97,6 +97,12 @@ export const MenuSwitcher = () => { + + + Personal analytics + + + setLanguageSwitcherOpen(true)} diff --git a/apps/remix/app/components/general/org-menu-switcher.tsx b/apps/remix/app/components/general/org-menu-switcher.tsx index 5ba8dcea4..c4e3062e0 100644 --- a/apps/remix/app/components/general/org-menu-switcher.tsx +++ b/apps/remix/app/components/general/org-menu-switcher.tsx @@ -307,11 +307,18 @@ export const OrgMenuSwitcher = () => { )} {currentTeam && canExecuteTeamAction('MANAGE_TEAM', currentTeam.currentTeamRole) && ( - - - Team settings - - + <> + + + Team settings + + + + + Team analytics + + + )} + + + + {IS_BILLING_ENABLED() && ( + + {IS_BILLING_ENABLED() && (