mirror of
https://github.com/documenso/documenso.git
synced 2026-08-26 00:02:43 +10:00
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.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Calendar period presets for the team analytics dashboard. Each resolves to a
|
||||
* half-open `[start, end)` instant range in the viewer's timezone.
|
||||
*
|
||||
* Pure (zod + luxon, no Prisma) so it can be unit-tested without a database and
|
||||
* shared with the request schema without pulling server-only code anywhere it
|
||||
* should not go.
|
||||
*/
|
||||
export const ZAnalyticsPeriodSchema = z.enum([
|
||||
'week',
|
||||
'month',
|
||||
'quarter',
|
||||
'year',
|
||||
'lastMonth',
|
||||
'last7Days',
|
||||
'last30Days',
|
||||
]);
|
||||
|
||||
export type AnalyticsPeriod = z.infer<typeof ZAnalyticsPeriodSchema>;
|
||||
|
||||
export const DEFAULT_ANALYTICS_PERIOD: AnalyticsPeriod = 'month';
|
||||
|
||||
export type AnalyticsPeriodRange = {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a calendar preset into a half-open `[start, end)` instant range,
|
||||
* anchored to the viewer's timezone.
|
||||
*
|
||||
* Falls back to UTC when the zone is missing or invalid so boundaries never
|
||||
* silently drift to the server's local zone.
|
||||
*/
|
||||
export const resolveAnalyticsPeriod = ({
|
||||
period,
|
||||
timezone,
|
||||
}: {
|
||||
period: AnalyticsPeriod;
|
||||
timezone?: string;
|
||||
}): AnalyticsPeriodRange => {
|
||||
const zoned = DateTime.now().setZone(timezone ?? 'utc');
|
||||
const now = zoned.isValid ? zoned : DateTime.now().setZone('utc');
|
||||
|
||||
const { start, end } = match(period)
|
||||
.with('week', () => ({
|
||||
start: now.startOf('week'),
|
||||
end: now.startOf('week').plus({ weeks: 1 }),
|
||||
}))
|
||||
.with('month', () => ({
|
||||
start: now.startOf('month'),
|
||||
end: now.startOf('month').plus({ months: 1 }),
|
||||
}))
|
||||
.with('quarter', () => ({
|
||||
start: now.startOf('quarter'),
|
||||
end: now.startOf('quarter').plus({ quarters: 1 }),
|
||||
}))
|
||||
.with('year', () => ({
|
||||
start: now.startOf('year'),
|
||||
end: now.startOf('year').plus({ years: 1 }),
|
||||
}))
|
||||
.with('lastMonth', () => ({
|
||||
start: now.startOf('month').minus({ months: 1 }),
|
||||
end: now.startOf('month'),
|
||||
}))
|
||||
.with('last7Days', () => ({
|
||||
start: now.startOf('day').minus({ days: 6 }),
|
||||
end: now.startOf('day').plus({ days: 1 }),
|
||||
}))
|
||||
.with('last30Days', () => ({
|
||||
start: now.startOf('day').minus({ days: 29 }),
|
||||
end: now.startOf('day').plus({ days: 1 }),
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
return {
|
||||
start: start.toJSDate(),
|
||||
end: end.toJSDate(),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user