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.
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { resolveAnalyticsPeriod } from './analytics-period';
|
|
|
|
const iso = (date: Date) => date.toISOString();
|
|
|
|
describe('resolveAnalyticsPeriod', () => {
|
|
// Friday, 2026-05-15. May 2026 is EDT (UTC-4) in the US.
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-05-15T12:00:00.000Z'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('resolves "month" to the current calendar month, half-open, in UTC by default', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'month' });
|
|
|
|
expect(iso(start)).toBe('2026-05-01T00:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-06-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('anchors month boundaries to the viewer timezone', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'month', timezone: 'America/New_York' });
|
|
|
|
// Local midnight in EDT is 04:00 UTC.
|
|
expect(iso(start)).toBe('2026-05-01T04:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-06-01T04:00:00.000Z');
|
|
});
|
|
|
|
it('shifts the window for a zone ahead of UTC', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'month', timezone: 'Asia/Tokyo' });
|
|
|
|
// JST is UTC+9, so local midnight is the previous day at 15:00 UTC.
|
|
expect(iso(start)).toBe('2026-04-30T15:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-05-31T15:00:00.000Z');
|
|
});
|
|
|
|
it('falls back to UTC for an invalid timezone', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'month', timezone: 'Not/AZone' });
|
|
|
|
expect(iso(start)).toBe('2026-05-01T00:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-06-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('resolves "lastMonth" contiguous with the start of the current month', () => {
|
|
const lastMonth = resolveAnalyticsPeriod({ period: 'lastMonth' });
|
|
const thisMonth = resolveAnalyticsPeriod({ period: 'month' });
|
|
|
|
expect(iso(lastMonth.start)).toBe('2026-04-01T00:00:00.000Z');
|
|
expect(iso(lastMonth.end)).toBe('2026-05-01T00:00:00.000Z');
|
|
expect(iso(lastMonth.end)).toBe(iso(thisMonth.start));
|
|
});
|
|
|
|
it('resolves "quarter" to the current calendar quarter', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'quarter' });
|
|
|
|
expect(iso(start)).toBe('2026-04-01T00:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-07-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('resolves "year" to the current calendar year', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'year' });
|
|
|
|
expect(iso(start)).toBe('2026-01-01T00:00:00.000Z');
|
|
expect(iso(end)).toBe('2027-01-01T00:00:00.000Z');
|
|
});
|
|
|
|
it('resolves "last30Days" as a trailing 30-day window including today', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'last30Days' });
|
|
|
|
expect(iso(start)).toBe('2026-04-16T00:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-05-16T00:00:00.000Z');
|
|
expect(end.getTime() - start.getTime()).toBe(30 * 24 * 60 * 60 * 1000);
|
|
});
|
|
|
|
it('resolves "last7Days" as a trailing 7-day window including today', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'last7Days' });
|
|
|
|
expect(iso(start)).toBe('2026-05-09T00:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-05-16T00:00:00.000Z');
|
|
expect(end.getTime() - start.getTime()).toBe(7 * 24 * 60 * 60 * 1000);
|
|
});
|
|
|
|
it('resolves "week" to a 7-day ISO week starting Monday', () => {
|
|
const { start, end } = resolveAnalyticsPeriod({ period: 'week' });
|
|
|
|
// 2026-05-15 is a Friday; the ISO week starts Monday 2026-05-11.
|
|
expect(iso(start)).toBe('2026-05-11T00:00:00.000Z');
|
|
expect(iso(end)).toBe('2026-05-18T00:00:00.000Z');
|
|
expect(end.getTime() - start.getTime()).toBe(7 * 24 * 60 * 60 * 1000);
|
|
});
|
|
});
|