mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
ae07df6061
Add a team document-usage dashboard at /t/:teamUrl/analytics for team admins and managers, behind the NEXT_PUBLIC_FEATURE_TEAM_ANALYTICS_ENABLED rollout flag (enabled by default, set to "false" to gate it off). Backend: - getTeamAnalytics Kysely query over team-produced documents across all folders, with exact COUNT(*) (no STATS_COUNT_CAP). Each metric uses its own date axis: Sent/Draft/Pending by createdAt, Completed by Envelope.completedAt, Declined by the DOCUMENT_RECIPIENT_REJECTED audit-log timestamp. - resolveAnalyticsPeriod turns calendar presets into half-open [start, end) ranges in the viewer's timezone, falling back to UTC. - team.getAnalytics tRPC route gated to ADMIN/MANAGER. Frontend: - Standalone /t/:teamUrl/analytics route whose loader gates the flag and role, silently redirecting members to documents. - Headline metrics and compact stat tiles, a member multiselect filter, a calendar-preset period selector, and an empty state. - Role- and flag-gated nav entries in the desktop and mobile navigation. Tests: - Unit tests for the period resolver (timezone and preset boundaries). - Integration/E2E tests for the query semantics (date axes, audit-log decline, all-folders aggregation, sender attribution), access control, filters and the empty state.
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
import { ZAnalyticsPeriodSchema } from '@documenso/trpc/server/team-router/get-team-analytics.types';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { useMemo } from 'react';
|
|
import { useLocation, useNavigate, useSearchParams } from 'react-router';
|
|
|
|
const DEFAULT_PERIOD = 'month';
|
|
|
|
const PERIOD_OPTIONS = [
|
|
{ value: 'week', label: msg`This week` },
|
|
{ value: 'month', label: msg`This month` },
|
|
{ value: 'quarter', label: msg`This quarter` },
|
|
{ value: 'year', label: msg`This year` },
|
|
{ value: 'lastMonth', label: msg`Last month` },
|
|
{ value: 'last7Days', label: msg`Last 7 days` },
|
|
{ value: 'last30Days', label: msg`Last 30 days` },
|
|
] as const;
|
|
|
|
export const AnalyticsPeriodSelector = () => {
|
|
const { _ } = useLingui();
|
|
|
|
const { pathname } = useLocation();
|
|
const [searchParams] = useSearchParams();
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const period = useMemo(() => {
|
|
const parsed = ZAnalyticsPeriodSchema.safeParse(searchParams?.get('period') ?? DEFAULT_PERIOD);
|
|
|
|
return parsed.success ? parsed.data : DEFAULT_PERIOD;
|
|
}, [searchParams]);
|
|
|
|
const onPeriodChange = (newPeriod: string) => {
|
|
if (!pathname) {
|
|
return;
|
|
}
|
|
|
|
const params = new URLSearchParams(searchParams?.toString());
|
|
|
|
params.set('period', newPeriod);
|
|
|
|
if (newPeriod === DEFAULT_PERIOD) {
|
|
params.delete('period');
|
|
}
|
|
|
|
void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
|
|
};
|
|
|
|
return (
|
|
<Select value={period} onValueChange={onPeriodChange}>
|
|
<SelectTrigger className="max-w-[200px] text-muted-foreground">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
|
|
<SelectContent position="popper">
|
|
{PERIOD_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{_(option.label)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
};
|