Files
documenso/packages/trpc/server/team-router/get-team-analytics.ts
ephraimduncan ae07df6061 feat(team): add team analytics dashboard
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.
2026-05-29 13:52:29 +00:00

47 lines
1.5 KiB
TypeScript

import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { DEFAULT_ANALYTICS_PERIOD, resolveAnalyticsPeriod } from '@documenso/lib/server-only/team/analytics-period';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { getTeamAnalytics } from '@documenso/lib/server-only/team/get-team-analytics';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { authenticatedProcedure } from '../trpc';
import { ZGetTeamAnalyticsRequestSchema, ZGetTeamAnalyticsResponseSchema } from './get-team-analytics.types';
export const getTeamAnalyticsRoute = authenticatedProcedure
.input(ZGetTeamAnalyticsRequestSchema)
.output(ZGetTeamAnalyticsResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, period, timezone, senderIds } = input;
const { user } = ctx;
ctx.logger.info({
input: {
teamId,
period,
senderIds,
},
});
const team = await getTeamById({ userId: user.id, teamId });
// Analytics are restricted to team admins and managers (MANAGE_TEAM).
if (!canExecuteTeamAction('MANAGE_TEAM', team.currentTeamRole)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to view analytics for this team',
});
}
const { start, end } = resolveAnalyticsPeriod({
period: period ?? DEFAULT_ANALYTICS_PERIOD,
timezone,
});
return await getTeamAnalytics({
userId: user.id,
teamId,
periodStart: start,
periodEnd: end,
senderIds,
});
});