diff --git a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx index e216aa26d..059ad2db4 100644 --- a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx @@ -6,8 +6,8 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents'; import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; -import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats'; -import { getStats } from '@documenso/lib/server-only/document/get-stats'; +import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats-new'; +import { getStats } from '@documenso/lib/server-only/document/get-stats-new'; import { parseToIntegerArray } from '@documenso/lib/utils/params'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import type { Team, TeamEmail, TeamMemberRole } from '@documenso/prisma/client'; @@ -35,7 +35,7 @@ export interface DocumentsPageViewProps { senderIds?: string; search?: string; }; - team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } }; + team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } }; } export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => { @@ -50,25 +50,14 @@ export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPa const currentTeam = team ? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email } : undefined; - const currentTeamMemberRole = team?.currentTeamMember?.role; const getStatOptions: GetStatsInput = { user, period, + team, search, }; - if (team) { - getStatOptions.team = { - teamId: team.id, - teamEmail: team.teamEmail?.email, - senderIds, - currentTeamMemberRole, - currentUserEmail: user.email, - userId: user.id, - }; - } - const stats = await getStats(getStatOptions); const results = await findDocuments({ diff --git a/packages/app-tests/playwright.config.ts b/packages/app-tests/playwright.config.ts index effbfa236..725f4bb04 100644 --- a/packages/app-tests/playwright.config.ts +++ b/packages/app-tests/playwright.config.ts @@ -16,8 +16,8 @@ ENV_FILES.forEach((file) => { export default defineConfig({ testDir: './e2e', /* Run tests in files in parallel */ - fullyParallel: false, - workers: 1, + fullyParallel: true, + workers: '50%', /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 0d0873173..b683c7579 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -2,9 +2,8 @@ import { DateTime } from 'luxon'; import { P, match } from 'ts-pattern'; import { prisma } from '@documenso/prisma'; -import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client'; import type { Document, DocumentSource, Team, TeamEmail, User } from '@documenso/prisma/client'; -import { Prisma } from '@documenso/prisma/client'; +import { Prisma, RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { DocumentVisibility } from '../../types/document-visibility'; @@ -133,6 +132,8 @@ export const findDocuments = async ({ let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user); + console.log('find documets team', team); + if (team) { filters = findTeamDocumentsFilter(status, team, visibilityFilters); } @@ -285,7 +286,7 @@ export const findDocuments = async ({ } satisfies FindResultSet; }; -const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { +export const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { return match(status) .with(ExtendedDocumentStatus.ALL, () => ({ OR: [ @@ -443,7 +444,7 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { * @param team The team to find the documents for. * @returns A filter which can be applied to the Prisma Document schema. */ -const findTeamDocumentsFilter = ( +export const findTeamDocumentsFilter = ( status: ExtendedDocumentStatus, team: Team & { teamEmail: TeamEmail | null }, visibilityFilters: Prisma.DocumentWhereInput[], diff --git a/packages/lib/server-only/document/get-stats-new.tsx b/packages/lib/server-only/document/get-stats-new.tsx new file mode 100644 index 000000000..6b7816ba6 --- /dev/null +++ b/packages/lib/server-only/document/get-stats-new.tsx @@ -0,0 +1,118 @@ +import { DateTime } from 'luxon'; +import { match } from 'ts-pattern'; + +import { + type PeriodSelectorValue, + findDocumentsFilter, + findTeamDocumentsFilter, +} from '@documenso/lib/server-only/document/find-documents'; +import { prisma } from '@documenso/prisma'; +import type { Prisma, Team, TeamEmail, User } from '@documenso/prisma/client'; +import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; + +export type GetStatsInput = { + user: User; + team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } }; + period?: PeriodSelectorValue; + search?: string; +}; + +export const getStats = async ({ user, period, search, ...options }: GetStatsInput) => { + let createdAt: Prisma.DocumentWhereInput['createdAt']; + + if (period) { + const daysAgo = parseInt(period.replace(/d$/, ''), 10); + + const startOfPeriod = DateTime.now().minus({ days: daysAgo }).startOf('day'); + + createdAt = { + gte: startOfPeriod.toJSDate(), + }; + } + + const stats: Record = { + [ExtendedDocumentStatus.DRAFT]: 0, + [ExtendedDocumentStatus.PENDING]: 0, + [ExtendedDocumentStatus.COMPLETED]: 0, + [ExtendedDocumentStatus.INBOX]: 0, + [ExtendedDocumentStatus.ALL]: 0, + [ExtendedDocumentStatus.BIN]: 0, + }; + + const searchFilter: Prisma.DocumentWhereInput = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { Recipient: { some: { name: { contains: search, mode: 'insensitive' } } } }, + { Recipient: { some: { email: { contains: search, mode: 'insensitive' } } } }, + ], + } + : {}; + + const visibilityFilters = [ + match(options.team?.currentTeamMember?.role) + .with(TeamMemberRole.ADMIN, () => ({ + visibility: { + in: [ + DocumentVisibility.EVERYONE, + DocumentVisibility.MANAGER_AND_ABOVE, + DocumentVisibility.ADMIN, + ], + }, + })) + .with(TeamMemberRole.MANAGER, () => ({ + visibility: { + in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], + }, + })) + .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })), + ]; + + const statusCounts = await Promise.all( + Object.values(ExtendedDocumentStatus).map(async (status) => { + if (status === ExtendedDocumentStatus.ALL) { + return; + } + + const filter = options.team + ? findTeamDocumentsFilter(status, options.team, visibilityFilters) + : findDocumentsFilter(status, user); + + if (filter === null) { + return { status, count: 0 }; + } + + const whereClause = { + ...filter, + ...(createdAt && { createdAt }), + ...searchFilter, + }; + + const count = await prisma.document.count({ + where: whereClause, + }); + + return { status, count }; + }), + ); + + statusCounts.forEach((result) => { + if (result) { + stats[result.status] = result.count; + if ( + result.status !== ExtendedDocumentStatus.BIN && + [ + ExtendedDocumentStatus.DRAFT, + ExtendedDocumentStatus.PENDING, + ExtendedDocumentStatus.COMPLETED, + ExtendedDocumentStatus.INBOX, + ].includes(result.status) + ) { + stats[ExtendedDocumentStatus.ALL] += result.count; + } + } + }); + + return stats; +}; diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts index 3dec3f245..b49c2949f 100644 --- a/packages/lib/server-only/document/get-stats.ts +++ b/packages/lib/server-only/document/get-stats.ts @@ -114,16 +114,9 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { deletedAt: null, }, { - status: ExtendedDocumentStatus.COMPLETED, - Recipient: { - some: { - email: user.email, - documentDeletedAt: null, - }, + status: { + not: ExtendedDocumentStatus.DRAFT, }, - }, - { - status: ExtendedDocumentStatus.PENDING, Recipient: { some: { email: user.email, @@ -151,7 +144,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { email: user.email, signingStatus: SigningStatus.NOT_SIGNED, role: { - not: 'CC', + not: RecipientRole.CC, }, documentDeletedAt: null, }, @@ -181,7 +174,7 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { email: user.email, signingStatus: SigningStatus.SIGNED, role: { - not: 'CC', + not: RecipientRole.CC, }, documentDeletedAt: null, }, @@ -302,190 +295,197 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })), ]; - // Owner counts (ALL) - const ownerCountsWhereInput: Prisma.DocumentWhereInput = { - userId: userIdWhereClause, - createdAt, - OR: [ - { - teamId, - deletedAt: null, - OR: visibilityFilters, - }, - ...(teamEmail - ? [ - { - status: { - not: ExtendedDocumentStatus.DRAFT, - }, - Recipient: { - some: { - email: teamEmail, - }, - }, - deletedAt: null, - OR: visibilityFilters, - }, - { - User: { - email: teamEmail, - }, - deletedAt: null, - OR: visibilityFilters, - }, - ] - : []), - ], - ...searchFilter, - }; - - // Not signed counts (INBOX) - const notSignedCountsWhereInput: Prisma.DocumentWhereInput = teamEmail - ? { - userId: userIdWhereClause, - createdAt, - status: { - not: ExtendedDocumentStatus.DRAFT, - }, - Recipient: { - some: { - email: teamEmail, - signingStatus: SigningStatus.NOT_SIGNED, - role: { - not: RecipientRole.CC, - }, - }, - }, - deletedAt: null, - OR: visibilityFilters, - ...searchFilter, - } - : { - userId: userIdWhereClause, - createdAt, - AND: [ + return Promise.all([ + // Owner counts (ALL) + prisma.document.groupBy({ + by: ['status'], + _count: { _all: true }, + where: { + OR: [ { - OR: [{ id: -1 }], // Empty set if no team email + teamId, + deletedAt: null, + OR: visibilityFilters, }, - searchFilter, + ...(teamEmail + ? [ + { + status: { + not: ExtendedDocumentStatus.DRAFT, + }, + Recipient: { + some: { + email: teamEmail, + documentDeletedAt: null, + }, + }, + deletedAt: null, + OR: visibilityFilters, + }, + { + User: { + email: teamEmail, + }, + deletedAt: null, + OR: visibilityFilters, + }, + ] + : []), ], - }; + userId: userIdWhereClause, + createdAt, + ...searchFilter, + }, + }), - // Has signed counts (PENDING + COMPLETED) - const hasSignedCountsWhereInput: Prisma.DocumentWhereInput = { - userId: userIdWhereClause, - createdAt, - OR: [ - { - teamId, - status: ExtendedDocumentStatus.PENDING, - deletedAt: null, - OR: visibilityFilters, - }, - { - teamId, - status: ExtendedDocumentStatus.COMPLETED, - deletedAt: null, - OR: visibilityFilters, - }, - ...(teamEmail - ? [ - { - status: ExtendedDocumentStatus.PENDING, - OR: [ + // Not signed counts (INBOX) + prisma.document.groupBy({ + by: ['status'], + _count: { _all: true }, + where: teamEmail + ? { + userId: userIdWhereClause, + createdAt, + status: { + not: ExtendedDocumentStatus.DRAFT, + }, + Recipient: { + some: { + email: teamEmail, + signingStatus: SigningStatus.NOT_SIGNED, + role: { + not: RecipientRole.CC, + }, + }, + }, + deletedAt: null, + OR: visibilityFilters, + ...searchFilter, + } + : { + userId: userIdWhereClause, + createdAt, + AND: [ + { + OR: [{ id: -1 }], // Empty set if no team email + }, + searchFilter, + ], + }, + }), + + // Has signed counts (PENDING + COMPLETED) + prisma.document.groupBy({ + by: ['status'], + _count: { _all: true }, + where: { + userId: userIdWhereClause, + createdAt, + OR: [ + { + teamId, + status: ExtendedDocumentStatus.PENDING, + deletedAt: null, + OR: visibilityFilters, + }, + { + teamId, + status: ExtendedDocumentStatus.COMPLETED, + deletedAt: null, + OR: visibilityFilters, + }, + ...(teamEmail + ? [ { - Recipient: { - some: { - email: teamEmail, - signingStatus: SigningStatus.SIGNED, - role: { - not: RecipientRole.CC, + status: ExtendedDocumentStatus.PENDING, + OR: [ + { + Recipient: { + some: { + email: teamEmail, + signingStatus: SigningStatus.SIGNED, + role: { + not: RecipientRole.CC, + }, + documentDeletedAt: null, + }, }, + OR: visibilityFilters, }, - }, - OR: visibilityFilters, - }, - { - User: { - email: teamEmail, - }, - OR: visibilityFilters, - }, - ], - deletedAt: null, - }, - { - status: ExtendedDocumentStatus.COMPLETED, - OR: [ - { - Recipient: { - some: { - email: teamEmail, + { + User: { + email: teamEmail, + }, + OR: visibilityFilters, }, - }, - OR: visibilityFilters, + ], + deletedAt: null, }, { - User: { - email: teamEmail, - }, - OR: visibilityFilters, + status: ExtendedDocumentStatus.COMPLETED, + OR: [ + { + Recipient: { + some: { + email: teamEmail, + documentDeletedAt: null, + }, + }, + OR: visibilityFilters, + }, + { + User: { + email: teamEmail, + }, + OR: visibilityFilters, + }, + ], + deletedAt: null, }, - ], - deletedAt: null, - }, - ] - : []), - ], - ...searchFilter, - }; - - const deletedCountsWhereInput: Prisma.DocumentWhereInput = { - OR: [ - { - teamId, - deletedAt: { - gte: DateTime.now().minus({ days: 30 }).startOf('day').toJSDate(), - }, + ] + : []), + ], + ...searchFilter, }, - ...(teamEmail - ? [ - { - Recipient: { - some: { - email: teamEmail, - documentDeletedAt: { + }), + + // Deleted counts (BIN) + prisma.document.groupBy({ + by: ['status'], + _count: { _all: true }, + where: { + OR: [ + { + teamId, + deletedAt: { + gte: DateTime.now().minus({ days: 30 }).startOf('day').toJSDate(), + }, + }, + ...(teamEmail + ? [ + { + User: { + email: teamEmail, + }, + deletedAt: { gte: DateTime.now().minus({ days: 30 }).startOf('day').toJSDate(), }, }, - }, - }, - ] - : []), - ], - ...searchFilter, - }; - - return Promise.all([ - prisma.document.groupBy({ - by: ['status'], - _count: { _all: true }, - where: ownerCountsWhereInput, - }), - prisma.document.groupBy({ - by: ['status'], - _count: { _all: true }, - where: notSignedCountsWhereInput, - }), - prisma.document.groupBy({ - by: ['status'], - _count: { _all: true }, - where: hasSignedCountsWhereInput, - }), - prisma.document.groupBy({ - by: ['status'], - _count: { _all: true }, - where: deletedCountsWhereInput, + { + Recipient: { + some: { + email: teamEmail, + documentDeletedAt: { + gte: DateTime.now().minus({ days: 30 }).startOf('day').toJSDate(), + }, + }, + }, + }, + ] + : []), + ], + ...searchFilter, + }, }), ]); }; diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po index 07b10dc5f..672b97101 100644 --- a/packages/lib/translations/de/web.po +++ b/packages/lib/translations/de/web.po @@ -1619,7 +1619,7 @@ msgstr "Dokument wird dauerhaft gelöscht" #: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109 #: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16 #: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15 -#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119 +#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:108 #: apps/web/src/app/(profile)/p/[url]/page.tsx:166 #: apps/web/src/app/not-found.tsx:21 #: apps/web/src/components/(dashboard)/common/command-menu.tsx:205 diff --git a/packages/lib/translations/en/web.po b/packages/lib/translations/en/web.po index 080d15dfd..e8460c097 100644 --- a/packages/lib/translations/en/web.po +++ b/packages/lib/translations/en/web.po @@ -1614,7 +1614,7 @@ msgstr "Document will be permanently deleted" #: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109 #: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16 #: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15 -#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119 +#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:108 #: apps/web/src/app/(profile)/p/[url]/page.tsx:166 #: apps/web/src/app/not-found.tsx:21 #: apps/web/src/components/(dashboard)/common/command-menu.tsx:205 diff --git a/packages/lib/translations/es/web.po b/packages/lib/translations/es/web.po index 37008eee8..66d42350a 100644 --- a/packages/lib/translations/es/web.po +++ b/packages/lib/translations/es/web.po @@ -1619,7 +1619,7 @@ msgstr "El documento será eliminado permanentemente" #: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109 #: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16 #: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15 -#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119 +#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:108 #: apps/web/src/app/(profile)/p/[url]/page.tsx:166 #: apps/web/src/app/not-found.tsx:21 #: apps/web/src/components/(dashboard)/common/command-menu.tsx:205 diff --git a/packages/lib/translations/fr/web.po b/packages/lib/translations/fr/web.po index 2de78cb35..e0700d6d0 100644 --- a/packages/lib/translations/fr/web.po +++ b/packages/lib/translations/fr/web.po @@ -1619,7 +1619,7 @@ msgstr "Le document sera supprimé de manière permanente" #: apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx:109 #: apps/web/src/app/(dashboard)/documents/[id]/loading.tsx:16 #: apps/web/src/app/(dashboard)/documents/[id]/sent/page.tsx:15 -#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:119 +#: apps/web/src/app/(dashboard)/documents/documents-page-view.tsx:108 #: apps/web/src/app/(profile)/p/[url]/page.tsx:166 #: apps/web/src/app/not-found.tsx:21 #: apps/web/src/components/(dashboard)/common/command-menu.tsx:205