From 7615c9d2fa8e064e005c904c6a65e2020ec9f5af Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Fri, 5 Apr 2024 17:49:32 +0000 Subject: [PATCH 01/18] feat: add chat to admin dashboard --- .../src/app/(dashboard)/admin/stats/page.tsx | 59 +++++++++++++-- .../stats/user-with-document-cummulative.tsx | 69 +++++++++++++++++ .../admin/stats/user-with-document.tsx | 64 ++++++++++++++++ .../lib/server-only/admin/get-users-stats.ts | 74 ++++++++++++++++++- packages/ui/styles/theme.css | 4 + 5 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx create mode 100644 apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index 43fe4be01..204461f03 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -14,18 +14,35 @@ import { import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats'; import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats'; import { + getUserWithAtLeastOneDocumentPerMonth, + getUserWithAtLeastOneDocumentSignedPerMonth, + getUserWithSignedDocumentMonthlyGrowth, getUsersCount, getUsersWithSubscriptionsCount, } from '@documenso/lib/server-only/admin/get-users-stats'; import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; +import { UserWithDocumentChart } from './user-with-document'; +import { UserWithDocumentCummulativeChart } from './user-with-document-cummulative'; + export default async function AdminStatsPage() { - const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([ + const [ + usersCount, + usersWithSubscriptionsCount, + docStats, + recipientStats, + userWithAtLeastOneDocumentPerMonth, + userWithAtLeastOneDocumentSignedPerMonth, + MONTHLY_USERS_SIGNED, + ] = await Promise.all([ getUsersCount(), getUsersWithSubscriptionsCount(), getDocumentStats(), getRecipientsStats(), + getUserWithAtLeastOneDocumentPerMonth(), + getUserWithAtLeastOneDocumentSignedPerMonth(), + getUserWithSignedDocumentMonthlyGrowth(), ]); return ( @@ -43,12 +60,30 @@ export default async function AdminStatsPage() { -
+ {/* TODO: remove grid and see something */} +
+
+

User metrics

+ +
+ + +
+
+

Document metrics

-
- +
+ {/* */} @@ -58,7 +93,7 @@ export default async function AdminStatsPage() {

Recipients metrics

-
+
+ +
+

User Charts

+ + + + +
); } diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx new file mode 100644 index 000000000..a9c2c7038 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { DateTime } from 'luxon'; +import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; + +import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats'; + +export type UserWithDocumentCummulativeChartProps = { + className?: string; + data: GetUserWithDocumentMonthlyGrowth; +}; + +export const UserWithDocumentCummulativeChart = ({ + className, + data, +}: UserWithDocumentCummulativeChartProps) => { + const formattedData = [...data] + .reverse() + .map(({ month, cume_count: count, cume_signed_count: signed_count }) => { + return { + month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'), + count: Number(count), + signed_count: Number(signed_count), + }; + }); + + return ( +
+
+
+

Total Activity

+
+ + + + + + + [ + Number(value).toLocaleString('en-US'), + name === 'count' ? 'User with document' : 'Users with signed document', + ]} + cursor={{ fill: 'hsl(var(--primary) / 10%)' }} + /> + + + + + +
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx new file mode 100644 index 000000000..98858a386 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { DateTime } from 'luxon'; +import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; + +import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats'; + +export type UserWithDocumentChartProps = { + className?: string; + data: GetUserWithDocumentMonthlyGrowth; +}; + +export const UserWithDocumentChart = ({ className, data }: UserWithDocumentChartProps) => { + const formattedData = [...data].reverse().map(({ month, count, signed_count }) => { + return { + month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'), + count: Number(count), + signed_count: Number(signed_count), + }; + }); + + return ( +
+
+
+

Total Activity

+
+ + + + + + + [ + Number(value).toLocaleString('en-US'), + name === 'count' ? 'User with document' : 'Users with signed document', + ]} + cursor={{ fill: 'hsl(var(--primary) / 10%)' }} + /> + + + + + +
+
+ ); +}; diff --git a/packages/lib/server-only/admin/get-users-stats.ts b/packages/lib/server-only/admin/get-users-stats.ts index 09892171a..cf4d0b540 100644 --- a/packages/lib/server-only/admin/get-users-stats.ts +++ b/packages/lib/server-only/admin/get-users-stats.ts @@ -1,5 +1,7 @@ +import { DateTime } from 'luxon'; + import { prisma } from '@documenso/prisma'; -import { SubscriptionStatus } from '@documenso/prisma/client'; +import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client'; export const getUsersCount = async () => { return await prisma.user.count(); @@ -16,3 +18,73 @@ export const getUsersWithSubscriptionsCount = async () => { }, }); }; + +export const getUserWithAtLeastOneDocumentPerMonth = async () => { + return await prisma.user.count({ + where: { + Document: { + some: { + createdAt: { + gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), + }, + }, + }, + }, + }); +}; + +export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => { + return await prisma.user.count({ + where: { + Document: { + some: { + status: { + equals: DocumentStatus.COMPLETED, + }, + createdAt: { + gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), + }, + }, + }, + }, + }); +}; + +export type GetUserWithDocumentMonthlyGrowth = Array<{ + month: string; + count: number; + cume_count: number; + signed_count: number; + cume_signed_count: number; +}>; + +type GetUserWithDocumentMonthlyGrowthQueryResult = Array<{ + month: Date; + count: bigint; + cume_count: bigint; + signed_count: bigint; + cume_signed_count: bigint; +}>; + +export const getUserWithSignedDocumentMonthlyGrowth = async () => { + const result = await prisma.$queryRaw` + SELECT + DATE_TRUNC('month', "Document"."createdAt") AS "month", + COUNT(DISTINCT "Document"."userId") as "count", + SUM(COUNT(DISTINCT "Document"."userId")) OVER (ORDER BY DATE_TRUNC('month', "Document"."createdAt")) as "cume_count", + COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count", + SUM(COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END)) OVER (ORDER BY DATE_TRUNC('month', "Document"."createdAt")) as "cume_signed_count" + FROM "Document" + GROUP BY "month" + ORDER BY "month" DESC + LIMIT 12 +`; + + return result.map((row) => ({ + month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'), + count: Number(row.count), + cume_count: Number(row.cume_count), + signed_count: Number(row.signed_count), + cume_signed_count: Number(row.cume_signed_count), + })); +}; diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css index cb2d9d5c5..1271159d5 100644 --- a/packages/ui/styles/theme.css +++ b/packages/ui/styles/theme.css @@ -44,6 +44,8 @@ --radius: 0.5rem; --warning: 54 96% 45%; + + --gold: 47.9 95.8% 53.1%; } .dark { @@ -83,6 +85,8 @@ --radius: 0.5rem; --warning: 54 96% 45%; + + --gold: 47.9 95.8% 53.1%; } } From fdf4d03c1411ac3c9f3741a4dcb7204482c8085d Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Fri, 5 Apr 2024 17:54:36 +0000 Subject: [PATCH 02/18] fix: grid on mobile --- apps/web/src/app/(dashboard)/admin/stats/page.tsx | 6 +++--- .../admin/stats/user-with-document-cummulative.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index 204461f03..eb17a405f 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -65,7 +65,7 @@ export default async function AdminStatsPage() {

User metrics

-
+

Document metrics

-
+
{/* */} @@ -93,7 +93,7 @@ export default async function AdminStatsPage() {

Recipients metrics

-
+
-

Total Activity

+

Total Activity (Cummulative)

From 32348dd6f1dd753127388fef957065f3921edd69 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Mon, 8 Apr 2024 17:26:25 +0000 Subject: [PATCH 03/18] fix: pr review changes --- .../web/src/app/(dashboard)/admin/stats/page.tsx | 16 ++++------------ .../stats/user-with-document-cummulative.tsx | 2 +- .../admin/stats/user-with-document.tsx | 2 +- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index eb17a405f..cb2ead970 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -60,7 +60,6 @@ export default async function AdminStatsPage() {
- {/* TODO: remove grid and see something */}

User metrics

@@ -68,12 +67,12 @@ export default async function AdminStatsPage() {
@@ -83,7 +82,6 @@ export default async function AdminStatsPage() {

Document metrics

- {/* */} @@ -109,15 +107,9 @@ export default async function AdminStatsPage() {

User Charts

- + - +
); diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx index cfb94e1f7..96d2fa775 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx @@ -18,7 +18,7 @@ export const UserWithDocumentCummulativeChart = ({ .reverse() .map(({ month, cume_count: count, cume_signed_count: signed_count }) => { return { - month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'), + month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'), count: Number(count), signed_count: Number(signed_count), }; diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx index 98858a386..5ecb934f1 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -13,7 +13,7 @@ export type UserWithDocumentChartProps = { export const UserWithDocumentChart = ({ className, data }: UserWithDocumentChartProps) => { const formattedData = [...data].reverse().map(({ month, count, signed_count }) => { return { - month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLLL'), + month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'), count: Number(count), signed_count: Number(signed_count), }; From 50b57d5aa50eac0100d221bd03e1616c7d652b92 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 7 May 2024 09:35:59 +0000 Subject: [PATCH 04/18] fix: minor changes based on review --- apps/web/src/app/(dashboard)/admin/stats/page.tsx | 2 +- .../src/app/(dashboard)/admin/stats/user-with-document.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index cb2ead970..04fa9c0ca 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -72,7 +72,7 @@ export default async function AdminStatsPage() { />
diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx index 5ecb934f1..1afc55908 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -37,7 +37,10 @@ export const UserWithDocumentChart = ({ className, data }: UserWithDocumentChart }} formatter={(value, name) => [ Number(value).toLocaleString('en-US'), - name === 'count' ? 'User with document' : 'Users with signed document', + { + count: 'User with document', + signed_count: 'Users with signed document', + }[name], ]} cursor={{ fill: 'hsl(var(--primary) / 10%)' }} /> From 95a94d4fc1f0264ded805b6317d9b0dab328aa1d Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 21 May 2024 09:28:23 +0000 Subject: [PATCH 05/18] chore: use single chart graphs --- .../src/app/(dashboard)/admin/stats/page.tsx | 33 ++++++--- .../stats/user-with-document-cummulative.tsx | 69 ------------------- .../admin/stats/user-with-document.tsx | 63 +++++++++-------- .../lib/server-only/admin/get-users-stats.ts | 2 +- 4 files changed, 62 insertions(+), 105 deletions(-) delete mode 100644 apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index 04fa9c0ca..dd246d02e 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -24,7 +24,6 @@ import { import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; import { UserWithDocumentChart } from './user-with-document'; -import { UserWithDocumentCummulativeChart } from './user-with-document-cummulative'; export default async function AdminStatsPage() { const [ @@ -67,12 +66,12 @@ export default async function AdminStatsPage() {
@@ -105,11 +104,29 @@ export default async function AdminStatsPage() {
-

User Charts

- - - - +

Charts

+
+ + + + +
); diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx deleted file mode 100644 index 96d2fa775..000000000 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document-cummulative.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import { DateTime } from 'luxon'; -import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; - -import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats'; - -export type UserWithDocumentCummulativeChartProps = { - className?: string; - data: GetUserWithDocumentMonthlyGrowth; -}; - -export const UserWithDocumentCummulativeChart = ({ - className, - data, -}: UserWithDocumentCummulativeChartProps) => { - const formattedData = [...data] - .reverse() - .map(({ month, cume_count: count, cume_signed_count: signed_count }) => { - return { - month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'), - count: Number(count), - signed_count: Number(signed_count), - }; - }); - - return ( -
-
-
-

Total Activity (Cummulative)

-
- - - - - - - [ - Number(value).toLocaleString('en-US'), - name === 'count' ? 'User with document' : 'Users with signed document', - ]} - cursor={{ fill: 'hsl(var(--primary) / 10%)' }} - /> - - - - - -
-
- ); -}; diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx index 1afc55908..8ffc9db8c 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -7,27 +7,49 @@ import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-onl export type UserWithDocumentChartProps = { className?: string; + title: string; data: GetUserWithDocumentMonthlyGrowth; + cummulative?: boolean; + completed?: boolean; }; -export const UserWithDocumentChart = ({ className, data }: UserWithDocumentChartProps) => { - const formattedData = [...data].reverse().map(({ month, count, signed_count }) => { - return { - month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'), - count: Number(count), - signed_count: Number(signed_count), - }; - }); +export const UserWithDocumentChart = ({ + className, + data, + title, + cummulative = false, + completed = false, +}: UserWithDocumentChartProps) => { + const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => { + return [...data] + .reverse() + .map(({ month, count, cume_count, signed_count, cume_signed_count }) => { + const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'); + if (completed) { + return { + month: formattedMonth, + count: Number(signed_count), + cummulative: Number(cume_signed_count), + }; + } else { + return { + month: formattedMonth, + count: Number(count), + cummulative: Number(cume_count), + }; + } + }); + }; return (
-
-

Total Activity

+
+

{title}

- + @@ -35,29 +57,16 @@ export const UserWithDocumentChart = ({ className, data }: UserWithDocumentChart labelStyle={{ color: 'hsl(var(--primary-foreground))', }} - formatter={(value, name) => [ - Number(value).toLocaleString('en-US'), - { - count: 'User with document', - signed_count: 'Users with signed document', - }[name], - ]} + formatter={(value) => [Number(value).toLocaleString('en-US'), 'Documents']} cursor={{ fill: 'hsl(var(--primary) / 10%)' }} /> - diff --git a/packages/lib/server-only/admin/get-users-stats.ts b/packages/lib/server-only/admin/get-users-stats.ts index cf4d0b540..f79b4b66d 100644 --- a/packages/lib/server-only/admin/get-users-stats.ts +++ b/packages/lib/server-only/admin/get-users-stats.ts @@ -41,7 +41,7 @@ export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => { status: { equals: DocumentStatus.COMPLETED, }, - createdAt: { + completedAt: { gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), }, }, From c1449e01b188a0efafbeb1b4823244deda21540f Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Tue, 21 May 2024 14:07:27 +0200 Subject: [PATCH 06/18] chore: remove cummulative for clarity --- apps/web/src/app/(dashboard)/admin/stats/page.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index dd246d02e..ae39051f3 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -110,22 +110,11 @@ export default async function AdminStatsPage() { data={MONTHLY_USERS_SIGNED} title="Monthly users who created documents" /> - -
From 39e7eb0568eba5348c6c8c12147ee05b5e678b55 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 21 May 2024 22:44:44 +0000 Subject: [PATCH 07/18] fix: remove cummulative --- .../admin/stats/user-with-document.tsx | 36 ++++++++----------- .../lib/server-only/admin/get-users-stats.ts | 10 +----- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx index 8ffc9db8c..82dd93566 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -9,7 +9,6 @@ export type UserWithDocumentChartProps = { className?: string; title: string; data: GetUserWithDocumentMonthlyGrowth; - cummulative?: boolean; completed?: boolean; }; @@ -17,28 +16,23 @@ export const UserWithDocumentChart = ({ className, data, title, - cummulative = false, completed = false, }: UserWithDocumentChartProps) => { const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => { - return [...data] - .reverse() - .map(({ month, count, cume_count, signed_count, cume_signed_count }) => { - const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'); - if (completed) { - return { - month: formattedMonth, - count: Number(signed_count), - cummulative: Number(cume_signed_count), - }; - } else { - return { - month: formattedMonth, - count: Number(count), - cummulative: Number(cume_count), - }; - } - }); + return [...data].reverse().map(({ month, count, signed_count }) => { + const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'); + if (completed) { + return { + month: formattedMonth, + count: Number(signed_count), + }; + } else { + return { + month: formattedMonth, + count: Number(count), + }; + } + }); }; return ( @@ -62,7 +56,7 @@ export const UserWithDocumentChart = ({ /> { export type GetUserWithDocumentMonthlyGrowth = Array<{ month: string; count: number; - cume_count: number; signed_count: number; - cume_signed_count: number; }>; type GetUserWithDocumentMonthlyGrowthQueryResult = Array<{ month: Date; count: bigint; - cume_count: bigint; signed_count: bigint; - cume_signed_count: bigint; }>; export const getUserWithSignedDocumentMonthlyGrowth = async () => { @@ -71,9 +67,7 @@ export const getUserWithSignedDocumentMonthlyGrowth = async () => { SELECT DATE_TRUNC('month', "Document"."createdAt") AS "month", COUNT(DISTINCT "Document"."userId") as "count", - SUM(COUNT(DISTINCT "Document"."userId")) OVER (ORDER BY DATE_TRUNC('month', "Document"."createdAt")) as "cume_count", - COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count", - SUM(COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END)) OVER (ORDER BY DATE_TRUNC('month', "Document"."createdAt")) as "cume_signed_count" + COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count" FROM "Document" GROUP BY "month" ORDER BY "month" DESC @@ -83,8 +77,6 @@ export const getUserWithSignedDocumentMonthlyGrowth = async () => { return result.map((row) => ({ month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'), count: Number(row.count), - cume_count: Number(row.cume_count), signed_count: Number(row.signed_count), - cume_signed_count: Number(row.cume_signed_count), })); }; From 72d0a1b69ccedbeb110fce48b4f98318511232e7 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 21 May 2024 22:53:31 +0000 Subject: [PATCH 08/18] chore: custom tooltip --- apps/web/src/app/(dashboard)/admin/stats/page.tsx | 7 ++----- .../src/app/(dashboard)/admin/stats/user-with-document.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index ae39051f3..5037729f5 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -106,14 +106,11 @@ export default async function AdminStatsPage() {

Charts

- +
diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx index 82dd93566..1dfdbb269 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -10,6 +10,7 @@ export type UserWithDocumentChartProps = { title: string; data: GetUserWithDocumentMonthlyGrowth; completed?: boolean; + tooltip?: string; }; export const UserWithDocumentChart = ({ @@ -17,6 +18,7 @@ export const UserWithDocumentChart = ({ data, title, completed = false, + tooltip, }: UserWithDocumentChartProps) => { const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => { return [...data].reverse().map(({ month, count, signed_count }) => { @@ -51,7 +53,7 @@ export const UserWithDocumentChart = ({ labelStyle={{ color: 'hsl(var(--primary-foreground))', }} - formatter={(value) => [Number(value).toLocaleString('en-US'), 'Documents']} + formatter={(value) => [Number(value).toLocaleString('en-US'), tooltip]} cursor={{ fill: 'hsl(var(--primary) / 10%)' }} /> @@ -60,7 +62,7 @@ export const UserWithDocumentChart = ({ fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} maxBarSize={60} - label="Documents" + label={tooltip} /> From 3b2d184f05485f9dcc635c37a9ee63f05301a954 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 29 May 2024 08:26:58 +0000 Subject: [PATCH 09/18] chore: custom tooltip since it's hiding values under other charts --- .../src/app/(dashboard)/admin/stats/page.tsx | 7 ++++- .../admin/stats/user-with-document.tsx | 27 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index 5037729f5..a6749a94e 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -106,11 +106,16 @@ export default async function AdminStatsPage() {

Charts

- +
diff --git a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx index 1dfdbb269..cf9f11e23 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/user-with-document.tsx @@ -2,6 +2,8 @@ import { DateTime } from 'luxon'; import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import type { TooltipProps } from 'recharts'; +import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats'; @@ -13,6 +15,27 @@ export type UserWithDocumentChartProps = { tooltip?: string; }; +const CustomTooltip = ({ + active, + payload, + label, + tooltip, +}: TooltipProps & { tooltip?: string }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ {`${tooltip} : `} + {payload[0].value} +

+
+ ); + } + + return null; +}; + export const UserWithDocumentChart = ({ className, data, @@ -45,15 +68,15 @@ export const UserWithDocumentChart = ({
- + } labelStyle={{ color: 'hsl(var(--primary-foreground))', }} - formatter={(value) => [Number(value).toLocaleString('en-US'), tooltip]} cursor={{ fill: 'hsl(var(--primary) / 10%)' }} /> From 1bbfd9d0f3d8fcb8a882f4a6bd2755d27b6855bc Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 13 Jun 2024 05:46:34 +0000 Subject: [PATCH 10/18] fix: remove redundant cards --- .../src/app/(dashboard)/admin/stats/page.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index a6749a94e..bcce0b608 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -60,23 +60,6 @@ export default async function AdminStatsPage() {
-
-

User metrics

- -
- - -
-
-

Document metrics

From cc667233c62420c280396e2eb84c83ace388a43b Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 20 Jun 2024 10:59:07 +1000 Subject: [PATCH 11/18] fix: show correct authentication action for account required When using account required auth for a given document this change now shows the sign up or sign in button depending on if an account actually exists within Documenso. This change should reduce friction and confusion when a recipient has been invited to a document. --- apps/web/src/app/(signing)/sign/[token]/page.tsx | 9 ++++++++- .../src/app/(signing)/sign/[token]/signing-auth-page.tsx | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 3ae09f662..ef4b07c8a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -10,6 +10,7 @@ import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures'; +import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; @@ -70,8 +71,14 @@ export default async function SigningPage({ params: { token } }: SigningPageProp userId: user?.id, }); + let recipientHasAccount: boolean | null = null; + if (!isDocumentAccessValid) { - return ; + recipientHasAccount = await getUserByEmail({ email: recipient?.email }) + .then((user) => !!user) + .catch(() => false); + + return ; } await viewedDocument({ diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx index fb19384cd..2d77679df 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-auth-page.tsx @@ -11,9 +11,10 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; export type SigningAuthPageViewProps = { email: string; + emailHasAccount?: boolean; }; -export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => { +export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => { const { toast } = useToast(); const [isSigningOut, setIsSigningOut] = useState(false); @@ -30,7 +31,9 @@ export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => { }); await signOut({ - callbackUrl: `/signin?email=${encodeURIComponent(encryptedEmail)}`, + callbackUrl: emailHasAccount + ? `/signin?email=${encodeURIComponent(encryptedEmail)}` + : `/signup?email=${encodeURIComponent(encryptedEmail)}`, }); } catch { toast({ @@ -59,7 +62,7 @@ export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => { onClick={async () => handleChangeAccount(email)} loading={isSigningOut} > - Login + {emailHasAccount ? 'Login' : 'Sign up'}
From 16c6d4a8bd42f7c5ed58704cb938d4af1efa32a5 Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Mon, 24 Jun 2024 06:08:06 +0000 Subject: [PATCH 12/18] fix: show field on pending document (#1158) ## Description This pull request introduces the functionality to display pending fields on the document page view. This enhancement allows users to see which fields are pending and need to be completed. ![CleanShot 2024-05-14 at 23 31 29@2x](https://github.com/documenso/documenso/assets/55143799/ffea0b29-d251-4dd5-9742-5416ac8262ad) ## Changes Made - Added `getPendingFieldsForDocument` function in `packages/lib/server-only/field/get-pending-fields-for-document.ts` to fetch pending fields for a document. - Created a new component `DocumentPendingFields` in `document-pending-fields.tsx` to display the pending fields with options to hide individual fields. ## Testing Performed - Tested the new feature by creating documents with pending fields and verifying their display on the document page view. - Verified that the pending fields are correctly hidden when the "Hide field" button is clicked. - Ran unit tests for the new functionality and existing components to ensure no regressions. ## Checklist - [x] I have tested these changes locally and they work as expected. - [x] I have added/updated tests that prove the effectiveness of these changes. - [x] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## Additional Notes No additional notes. ## Summary by CodeRabbit - **New Features** - Introduced logic for handling pending and completed document fields based on signing status. - **Refactor** - Replaced `getCompletedFieldsForDocument` with `getFieldsForDocument`. - Updated `DocumentReadOnlyFields` component to `DocumentPendingFields`. - **Bug Fixes** - Improved field retrieval accuracy and display based on recipient signing status. - **Style** - Enhanced UI elements with new icons and button adjustments for better user interaction. --------- Co-authored-by: David Nguyen --- .../documents/[id]/document-page-view.tsx | 12 +- .../document/document-read-only-fields.tsx | 104 ++++++++++-------- .../field/get-completed-fields-for-token.ts | 1 + .../field/get-fields-for-document.ts | 12 ++ 4 files changed, 78 insertions(+), 51 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx index ba9b806e5..643534a5b 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx @@ -8,7 +8,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag'; -import { getCompletedFieldsForDocument } from '@documenso/lib/server-only/field/get-completed-fields-for-document'; +import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; @@ -86,14 +86,15 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) documentMeta.password = securePassword; } - const [recipients, completedFields] = await Promise.all([ + const [recipients, fields] = await Promise.all([ getRecipientsForDocument({ documentId, teamId: team?.id, userId: user.id, }), - getCompletedFieldsForDocument({ + getFieldsForDocument({ documentId, + userId: user.id, }), ]); @@ -163,10 +164,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) {document.status === DocumentStatus.PENDING && ( - + )}
diff --git a/apps/web/src/components/document/document-read-only-fields.tsx b/apps/web/src/components/document/document-read-only-fields.tsx index 95a907d8f..d62210b72 100644 --- a/apps/web/src/components/document/document-read-only-fields.tsx +++ b/apps/web/src/components/document/document-read-only-fields.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; +import { EyeOffIcon } from 'lucide-react'; import { P, match } from 'ts-pattern'; import { @@ -10,19 +11,19 @@ import { } from '@documenso/lib/constants/date-formats'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; -import type { CompletedField } from '@documenso/lib/types/fields'; +import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import type { DocumentMeta } from '@documenso/prisma/client'; -import { FieldType } from '@documenso/prisma/client'; +import { FieldType, SigningStatus } from '@documenso/prisma/client'; import { FieldRootContainer } from '@documenso/ui/components/field/field'; +import { cn } from '@documenso/ui/lib/utils'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; -import { Button } from '@documenso/ui/primitives/button'; import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { PopoverHover } from '@documenso/ui/primitives/popover'; export type DocumentReadOnlyFieldsProps = { - fields: CompletedField[]; + fields: DocumentField[]; documentMeta?: DocumentMeta; }; @@ -53,56 +54,71 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl } contentProps={{ - className: 'flex w-fit flex-col py-2.5 text-sm', + className: 'relative flex w-fit flex-col p-2.5 text-sm', }} > -

- - {field.Recipient.name - ? `${field.Recipient.name} (${field.Recipient.email})` - : field.Recipient.email}{' '} - - inserted a {FRIENDLY_FIELD_TYPE[field.type].toLowerCase()} +

+ {field.Recipient.signingStatus === SigningStatus.SIGNED ? 'Signed' : 'Pending'}{' '} + {FRIENDLY_FIELD_TYPE[field.type].toLowerCase()} field

- + +
- {match(field) - .with({ type: FieldType.SIGNATURE }, (field) => - field.Signature?.signatureImageAsBase64 ? ( - Signature - ) : ( -

- {field.Signature?.typedSignature} -

- ), - ) - .with( - { type: P.union(FieldType.NAME, FieldType.TEXT, FieldType.EMAIL) }, - () => field.customText, - ) - .with({ type: FieldType.DATE }, () => - convertToLocalSystemFormat( - field.customText, - documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, - documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, - ), - ) - .with({ type: FieldType.FREE_SIGNATURE }, () => null) - .exhaustive()} + {field.Recipient.signingStatus === SigningStatus.SIGNED && + match(field) + .with({ type: FieldType.SIGNATURE }, (field) => + field.Signature?.signatureImageAsBase64 ? ( + Signature + ) : ( +

+ {field.Signature?.typedSignature} +

+ ), + ) + .with( + { type: P.union(FieldType.NAME, FieldType.TEXT, FieldType.EMAIL) }, + () => field.customText, + ) + .with({ type: FieldType.DATE }, () => + convertToLocalSystemFormat( + field.customText, + documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, + documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + ), + ) + .with({ type: FieldType.FREE_SIGNATURE }, () => null) + .exhaustive()} + + {field.Recipient.signingStatus === SigningStatus.NOT_SIGNED && ( +

+ {FRIENDLY_FIELD_TYPE[field.type]} +

+ )}
), diff --git a/packages/lib/server-only/field/get-completed-fields-for-token.ts b/packages/lib/server-only/field/get-completed-fields-for-token.ts index d84fa1343..10cfb2672 100644 --- a/packages/lib/server-only/field/get-completed-fields-for-token.ts +++ b/packages/lib/server-only/field/get-completed-fields-for-token.ts @@ -26,6 +26,7 @@ export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsFo select: { name: true, email: true, + signingStatus: true, }, }, }, diff --git a/packages/lib/server-only/field/get-fields-for-document.ts b/packages/lib/server-only/field/get-fields-for-document.ts index 72a16c3f7..2cd8aa07c 100644 --- a/packages/lib/server-only/field/get-fields-for-document.ts +++ b/packages/lib/server-only/field/get-fields-for-document.ts @@ -5,6 +5,8 @@ export interface GetFieldsForDocumentOptions { userId: number; } +export type DocumentField = Awaited>[number]; + export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => { const fields = await prisma.field.findMany({ where: { @@ -26,6 +28,16 @@ export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForD ], }, }, + include: { + Signature: true, + Recipient: { + select: { + name: true, + email: true, + signingStatus: true, + }, + }, + }, orderBy: { id: 'asc', }, From 62cd4c019fd1a200d8f0156a37050baf54dac890 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:01:57 +0300 Subject: [PATCH 13/18] feat: force signature fields for document signers (#1139) ## Description Show a dialog when the document has signers with no signature fields placed. ## Changes Made Created a new dialog that'll be triggered when the document owner tries to send a document to the signers without placing signature fields. The document owners can't proceed to the next step unless they add signature fields. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. https://github.com/documenso/documenso/assets/25515812/f1b5c34e-2ce0-40e3-804c-f05d23045710 ## Summary by CodeRabbit - **New Features** - Introduced "Direct Links" for async signing, allowing users to create documents from templates using public links. - Added `MissingSignatureFieldDialog` component to ensure users don't miss adding signature fields. - **Enhancements** - Updated blog content to provide guidance on contract management and announce new pricing plans. - **Bug Fixes** - Improved async signing process for better efficiency and control. - **Refactor** - Improved internal code structure and import order for stripe-related functionality. - **Tests** - Enhanced e2e tests to verify signature presence before document creation and updated test flows for document approval. --------- Co-authored-by: David Nguyen --- .../content/blog/announcing-direct-links.mdx | 7 +-- ...ng-them-close-more-clients-efficiently.mdx | 10 ++-- ...lancers-close-more-clients-efficiently.mdx | 9 +++- .../blog/sunsetting-early-adopters.mdx | 9 ++-- .../templates/template-direct-link-dialog.tsx | 2 +- .../document-flow/stepper-component.spec.ts | 51 ++++--------------- .../stripe/get-document-related-prices.ts.ts | 6 ++- .../stripe/get-prices-by-interval.ts | 2 +- .../stripe/get-primary-account-plan-prices.ts | 6 ++- .../server-only/document/send-document.tsx | 38 ++++++++++---- packages/prisma/seed/documents.ts | 3 ++ .../primitives/document-flow/add-fields.tsx | 26 +++++++++- .../missing-signature-field-dialog.tsx | 46 +++++++++++++++++ 13 files changed, 148 insertions(+), 67 deletions(-) create mode 100644 packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx diff --git a/apps/marketing/content/blog/announcing-direct-links.mdx b/apps/marketing/content/blog/announcing-direct-links.mdx index 37e32ffcf..b52050ede 100644 --- a/apps/marketing/content/blog/announcing-direct-links.mdx +++ b/apps/marketing/content/blog/announcing-direct-links.mdx @@ -25,6 +25,7 @@ tags: > TLDR; We are launching direct links to templates. With direct links, a document is created from a template every time anyone signs the link. Links can be public. ## Sync or Async? + > Quick refresher on Sync vs. Async: Sync means everyone has to wait for me until they can continue their work. Async means everyone can and does their work at the time that fits best. Digital signing has become almost as normalized as email when doing business. While not 100% of companies are onboarded on digital signatures yet, hardly anyone is surprised when receiving a link to sign something digitally. As we got used to the user experience of sending emails, we also got used to the experience of sending document signature requests, with all the downsides: @@ -34,6 +35,7 @@ Digital signing has become almost as normalized as email when doing business. Wh - I need to monitor the requests I started for completion: "I sent you a link yesterday; please check it out." ## Introducing Direct Links + Today, we are introducing a new paradigm to signing: Async Direct Signing Links. Direct links are attached to a template and can be used anytime by anyone using the link. You set up the signature experience and flow once using all existing template mechanisms and you are done. You can provide anyone with the link so they can sign whenever they need to. You can even post the link publicly if you want to maximize its reach, i.e. for sales contracts. - ## Embrace Async + So, how does this help anyone? You may still need to send a signature request to people, but in the cases you don't, you are not forced to anymore. Need an NDA? Check out our standing NDA link. A customer needs an updated Form W-9? Just use the company W-9 Link; it always has the most up-to-date form. You can even go as far as publicly posting a link to a software development or design contract any potential customer can sign anytime. Can they talk to you first? Sure, but if they don't need to or already have to, they go straight to the link. The process of actively sending has gotten us used to using a sync paradigm (I send, you receive and sign, and I get the result), whereas an async one (you sign whenever it suits you, and I become active only then, if at all) is way better suited. Adding more approval and signature steps makes sure you still control the outcome, but the process becomes a lot more efficient. For example, you can grab your own copy of the early adopter's pledge here if you missed it: [documen.so/pledge](https://documen.so/pledge). > Take a minute to think about every signing request you send and whether they really require you to be part of the transaction. Could they be outsourced to the recipient and only reviewed once their part is done? - - ## Coming Soon: Profiles + The best place to put your public links will be your **Documenso profile**, which is also close to launching. We want to get a feel for how links are used and move on to profiles shortly after. Want to try out direct links? Grab a free account here to get started: [documen.so/free](https://documen.so/free). As always, we want to hear from you on [Twitter / X](https://twitter.com/eltimuro) (DMs are open) or [Discord](https://documen.so/discord) if you have any questions, ideas, or comments. diff --git a/apps/marketing/content/blog/how-documenso-enhances-contract-management-for-freelancers-helping-them-close-more-clients-efficiently.mdx b/apps/marketing/content/blog/how-documenso-enhances-contract-management-for-freelancers-helping-them-close-more-clients-efficiently.mdx index c18e27579..fc837932a 100644 --- a/apps/marketing/content/blog/how-documenso-enhances-contract-management-for-freelancers-helping-them-close-more-clients-efficiently.mdx +++ b/apps/marketing/content/blog/how-documenso-enhances-contract-management-for-freelancers-helping-them-close-more-clients-efficiently.mdx @@ -11,23 +11,26 @@ tags: - Productivity --- - ## Yes to Yes + > [Check out Part 1](https://documen.so/freelance-proposal) to learn about signing freelance proposals with Documenso and getting your first yes A basic rule of sales is going from "yes to yes”. Outlining the main points of working together in a proposal is a good way to get to your first yes since it reduces details and focuses on the main points of the work at hand. After being on the same page about the work and getting the first yes, it's time to draw up a formal contract. While agreeing to the proposal has some weight as well, the legal contract formalizes the commitments of both sides in an enforceable way. Having clear legal terms on payments, unexpected cases, and even dissolving the partnership helps both parties to feel assured about what to expect. ### **Digital Signatures for the Win** + Digitally signing documents accelerates contract closure, enhancing both speed and security. Parties can review and sign documents within minutes, eliminating the days required for manual signatures or even weeks with traditional mail. Beyond these efficiency gains, digital signatures boost trust by making the process secure and auditable. Once signed, digital documents are immutable, and every step is logged. Documenso simplifies this process, allowing you to send contracts effortlessly. As an open-source solution, our product's integrity and security are verifiable by anyone, which is why thousands of users rely on Documenso for their signing needs. Discover more at [https://documen.so/open](https://documen.so/open). ## Preparing the Contract + As a freelancer, obtaining a contract template ensures you have a standardized and professional agreement ready for new clients, helping to protect your interests and clarify project terms. While there are many good templates out there, be sure to verify that they fit your case since contracts are often very specific to a certain case. Always consider having your contract checked by a legal professional if it's a high-value transaction. Here is a quick checklist of what your contract should include: ### Checklist + - Names and Addresses of you and your client - Scope of Work to be performed, deadlines and deliverables - Payment Terms, Payment Schedules, and Pricing @@ -43,9 +46,9 @@ Here is a quick checklist of what your contract should include: - Severability Clause ensuring minor errors will not endanger the whole contract - The signees with name, role, and date - ## Getting the Signature -Once you have your contract ready, you can upload it and add recipients and signature fields. To add a more personal touch, consider adding a personal message to the signature request. + +Once you have your contract ready, you can upload it and add recipients and signature fields. To add a more personal touch, consider adding a personal message to the signature request.
### Conclusion + Sending a contract to clients using Documenso makes the process fast and easy. Seeing if your contract was signed or even read helps you understand where you are in the process. You can use the [Documenso Free Plan](https://app.documenso.com/signup?utm_source=blog-freelancer-contract) to send 5 contracts per month. Digital signing in 2024 is the best practice for professionals seeking the most efficient way to get business done. Let us know what you think and what we can improve. Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :) diff --git a/apps/marketing/content/blog/how-documenso-help-freelancers-close-more-clients-efficiently.mdx b/apps/marketing/content/blog/how-documenso-help-freelancers-close-more-clients-efficiently.mdx index 007fe7fcd..7d7265842 100644 --- a/apps/marketing/content/blog/how-documenso-help-freelancers-close-more-clients-efficiently.mdx +++ b/apps/marketing/content/blog/how-documenso-help-freelancers-close-more-clients-efficiently.mdx @@ -16,11 +16,13 @@ Getting new clients, or maybe even your first client, to sign with you is at the ## Understanding Proposal and Contracts ### 1. Initial Proposal + > Agreeing on what needs to be done and terms for payment A proposal will include the scope of the work (what does the customer want done?), desired deliverables (documents, code, features, videos, etc.), timelines, payment terms (one-time, monthly, per hour), and prices (e.g. $60/ hour, $5k one-time). A proposal is important for both sides to be clear about the goal and the terms that apply. Customers usually decide based on the proposal if your offer is what they want. ### 2. Formal Contract + > After the client signs the proposal, a contract can be signed, formalizing the agreement and adding detailed legal terms. Once the terms are agreed upon, a more formal document should specify the terms of working together, especially legal details such as confidentiality, indemnification, governing law, etc. The contract provides clarity on legal details for both sides and formalizes their claims. @@ -28,11 +30,13 @@ Once the terms are agreed upon, a more formal document should specify the terms Sending one or even multiple documents to a potential client and having them send them back signed (in the worst case, they send it via actual mail) is time-consuming for both sides. It also introduces friction at a time when making it as easy as possible for your potential client to say yes should be your number one goal. ### **Digital Signatures for the Win** + Signing documents digitally makes closing proposals and contracts faster and more secure. Each party can review and sign the documents in minutes instead of days (inserting the signatures manually via PDF editor) or even weeks (using conventional mail). Apart from the efficiency gains, signing digitally also increases trust by making the process more secure and auditable. Digitally signed documents can’t be changed after the fact, and every step of the process is logged. Documenso lets you reap these benefits by sending proposals and contracts with minimal effort. Being open source, the whole world can verify our product and how we deliver on these promises, which is why thousands of users already trust Documenso for their signing needs: [https://documen.so/open](https://documen.so/open). ## Preparing the Proposal + If you already have a proposal template, create a new version for your client and export it to PDF. If your tool doesn’t support that, your system's “PDF printer” lets you create a PDF from almost any tool by using the print function. If you do not have a template yet, you can find a lot of content and guides on the matter through a quick Google search. Here is a quick checklist of what your proposal should cover: - A clear and concise title @@ -48,6 +52,7 @@ If you already have a proposal template, create a new version for your client an - Summary of major terms for the coming contract ## Sending the Proposal + If you don’t have a Documenso Account yet, you can [create one for free](https://documen.so/signup?utm_source=blog-freelancer-proposal). Once you sign up, you can upload your proposal PDF by simply dragging it into the upload area. Add your potential client as a recipient, add a signature field, and you are done! You can track the status of your proposal simply by clicking the Document in the overview. Documenso will also notify you once the proposal is signed. ### Conclusion -Sending a proposal to potential clients using Documenso makes getting to the first “yes” fast and easy. Seeing if your proposal was signed or even read helps you to get a feel for where you are in the process. You can use the [Documenso Free Plan](https://app.documenso.com/signup?utm_source=blog-freelancer-proposal) to send 5 proposals per month. Digital Signing in 2024 is the best practice for all professionals looking for the most efficient way to get business done. + +Sending a proposal to potential clients using Documenso makes getting to the first “yes” fast and easy. Seeing if your proposal was signed or even read helps you to get a feel for where you are in the process. You can use the [Documenso Free Plan](https://app.documenso.com/signup?utm_source=blog-freelancer-proposal) to send 5 proposals per month. Digital Signing in 2024 is the best practice for all professionals looking for the most efficient way to get business done. > [Check out Part 2](https://documen.so/freelance-contract) to learn about signing freelance contracts with Documenso. @@ -68,4 +74,3 @@ Let us know what you think and what we can improve. Which field types are you mi Best from Hamburg\ Timur - diff --git a/apps/marketing/content/blog/sunsetting-early-adopters.mdx b/apps/marketing/content/blog/sunsetting-early-adopters.mdx index 47958de99..8f4d24111 100644 --- a/apps/marketing/content/blog/sunsetting-early-adopters.mdx +++ b/apps/marketing/content/blog/sunsetting-early-adopters.mdx @@ -7,7 +7,7 @@ authorRole: 'Co-Founder' date: 2024-06-12 tags: - Early Adopters - - Pricing + - Pricing - Open Startup --- @@ -27,9 +27,11 @@ tags: > TLDR; The Early Adopters Plan ended, and we have a new pricing. If you are an Early Adopter, reach out for a Discord community badge 🏅 # The End of the Beginning -12 months, 13 releases, and 344 merged pull requests after announcing the Early Adopter plan in our first-ever launch week, and we hit the cap of 100. Documenso has changed and grown a lot since then. For us, this is a great milestone towards our broader mission of bringing open signing to the world since now we are joined by a community consisting of contributors and early customers all around the world. + +12 months, 13 releases, and 344 merged pull requests after announcing the Early Adopter plan in our first-ever launch week, and we hit the cap of 100. Documenso has changed and grown a lot since then. For us, this is a great milestone towards our broader mission of bringing open signing to the world since now we are joined by a community consisting of contributors and early customers all around the world. # The New Plans + Starting today, we are sunsetting the Early Adopter Plan in favor of our new, more nuanced pricing model. The Early Adopter plan will succeeded by the **Individual plan**, which is still priced at $30/mo. The Individual plans will still include unlimited signatures and recipients since this aligns with our core belief of empowering our users wherever possible. If you managed to grab an Early Adopter plan, reach out on X or Discord to receive a special community badge. Early Adopters are meant to get preferential treatment where possible. Previously soft-launched as part of Early Adopters, we are officially introducing the **Team Plan** to our pricing for customers requiring multi-user accounts. Priced at $50/ mo. for 5 users, this plan offers unlimited signature volume as well. Additional users can be added for $10/mo. as needed. We have carefully crafted the billing of teams to ensure that dynamic changes are accurately reflected at the end of each billing cycle, providing you with a fair-value pricing structure. @@ -39,6 +41,7 @@ Our **Free Plan** stays unchanged, offering coverage to casual users and an easy Check out our [new pricing page here](https://documen.so/pricing). We also updated our [open page](https://documen.so/open) to reflect the end of Early Adopters. The metric now counts active subscriptions from Individuals and Teams. # API Access + All plans include access to the API as per our philosophy, making Documenso an open platform, and allowing everyone to build on it, no matter how big or small. Besides the Free Plan's 5 signatures per month limit, the API does not have access restrictions. Even the free plan can keep using the API after using its signature volume for non-signing operations like reading, editing, and even creating documents. Since the individual plan technically allows for running a Fortune 500 company for $30/ mo., plan we are adding a fair use clause here: You are free to use the API "a lot" if you are a big organization trying to stay on the Individual Plan we will ask to have a word about upgrading (which might make sense anyway considering your requirements). Fair use excludes Early Adopters, which we consider limitless by any measure. If you need clarification on whether your case is covered under fair use, you can contact us on Discord or support@documenso.com. It's probably fine, though. We also have a lot in the pipeline, and we are excited to share everything with you soon. A Big Shoutout to all Early Adopters. We salute you, and you will receive the preferred treatment where possible. @@ -46,4 +49,4 @@ We also have a lot in the pipeline, and we are excited to share everything with If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord). Best from Hamburg\ -Timur \ No newline at end of file +Timur diff --git a/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx b/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx index 6874fef90..c4fe8d714 100644 --- a/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/template-direct-link-dialog.tsx @@ -383,7 +383,7 @@ export const TemplateDirectLinkDialog = ({
- + + + + + + ); +}; From b6a2fe88cb081216e0947cca4ec92e07a572bab2 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Mon, 24 Jun 2024 17:34:52 +0200 Subject: [PATCH 14/18] chore: add direct link video --- apps/marketing/src/components/(marketing)/carousel.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/marketing/src/components/(marketing)/carousel.tsx b/apps/marketing/src/components/(marketing)/carousel.tsx index 307d4a4f0..f3d903809 100644 --- a/apps/marketing/src/components/(marketing)/carousel.tsx +++ b/apps/marketing/src/components/(marketing)/carousel.tsx @@ -30,6 +30,12 @@ const SLIDES = [ srcLight: 'https://github.com/documenso/design/raw/main/marketing/zapier.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm', }, + { + label: 'Direct Link', + type: 'video', + srcLight: 'https://github.com/documenso/design/raw/main/marketing/direct-links.webm', + srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/direct-links.webm', + }, { label: 'Webhooks', type: 'video', From 817103ebba3a32c8423f185f1e067480b2af28fe Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Tue, 25 Jun 2024 15:42:25 +0200 Subject: [PATCH 15/18] Update README.md chore: add shiny badges --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d6a5053f4..1ae2362bf 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,22 @@ Contact us if you are interested in our Enterprise plan for large organizations Book us with Cal.com ## Tech Stack +

+ TypeScript + NextJS + Made with Prisma + + + + + + +

+ - [Typescript](https://www.typescriptlang.org/) - Language - [Next.js](https://nextjs.org/) - Framework -- [Prisma](https://www.prisma.io/) - ORM +- [Prisma](https://www.prisma.io/) - ORM - [Tailwind](https://tailwindcss.com/) - CSS - [shadcn/ui](https://ui.shadcn.com/) - Component Library - [NextAuth.js](https://next-auth.js.org/) - Authentication From bbd68f37c241a3720059534a9b6c4fcff36afb61 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Tue, 25 Jun 2024 15:43:28 +0200 Subject: [PATCH 16/18] Update README.md chore alt --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ae2362bf..16738923c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Contact us if you are interested in our Enterprise plan for large organizations TypeScript NextJS Made with Prisma - + Tailwind CSS From 7e065764ec130ba51726d81d9df40f1baa6ab983 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 25 Jun 2024 15:10:58 +0000 Subject: [PATCH 17/18] chore: use luxon for dates --- packages/lib/server-only/admin/get-users-stats.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/server-only/admin/get-users-stats.ts b/packages/lib/server-only/admin/get-users-stats.ts index 089591954..0f4a2f0b4 100644 --- a/packages/lib/server-only/admin/get-users-stats.ts +++ b/packages/lib/server-only/admin/get-users-stats.ts @@ -25,7 +25,7 @@ export const getUserWithAtLeastOneDocumentPerMonth = async () => { Document: { some: { createdAt: { - gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), + gte: DateTime.now().minus({ months: 1 }).toJSDate(), }, }, }, @@ -42,7 +42,7 @@ export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => { equals: DocumentStatus.COMPLETED, }, completedAt: { - gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), + gte: DateTime.now().minus({ months: 1 }).toJSDate(), }, }, }, From 51ad6a6ff80ec4db89b9ae0e55af7a853c3f1c9c Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 25 Jun 2024 16:09:19 +0000 Subject: [PATCH 18/18] fix: direct links for teams are generated wrong --- .../template/create-document-from-direct-template.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts index 229851729..ce891e6ca 100644 --- a/packages/lib/server-only/template/create-document-from-direct-template.ts +++ b/packages/lib/server-only/template/create-document-from-direct-template.ts @@ -481,7 +481,9 @@ export const createDocumentFromDirectTemplate = async ({ const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, { recipientName: directRecipientEmail, recipientRole: directTemplateRecipient.role, - documentLink: `${formatDocumentsPath(document.team?.url)}/${document.id}`, + documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${ + document.id + }`, documentName: document.title, assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000', });