mirror of
https://github.com/documenso/documenso.git
synced 2026-08-23 23:02:22 +10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46d712f4cc | ||
|
|
62c609f105 | ||
|
|
4babe9b192 | ||
|
|
7e422bc3fd |
@@ -169,12 +169,28 @@ export const run = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const envelopeCompletedAuditLog = createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
requestMetadata,
|
||||||
|
user: null,
|
||||||
|
data: {
|
||||||
|
transactionId: nanoid(),
|
||||||
|
...(isRejected ? { isRejected: true, rejectionReason: rejectionReason } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalEnvelopeStatus = isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED;
|
||||||
|
|
||||||
let certificateDoc: PDF | null = null;
|
let certificateDoc: PDF | null = null;
|
||||||
let auditLogDoc: PDF | null = null;
|
let auditLogDoc: PDF | null = null;
|
||||||
|
|
||||||
if (settings.includeSigningCertificate || settings.includeAuditLog) {
|
if (settings.includeSigningCertificate || settings.includeAuditLog) {
|
||||||
const certificatePayload = {
|
const certificatePayload = {
|
||||||
envelope,
|
envelope: {
|
||||||
|
...envelope,
|
||||||
|
status: finalEnvelopeStatus,
|
||||||
|
},
|
||||||
recipients: envelope.recipients, // Need to use the recipients from envelope which contains ALL recipients.
|
recipients: envelope.recipients, // Need to use the recipients from envelope which contains ALL recipients.
|
||||||
fields,
|
fields,
|
||||||
language: envelope.documentMeta.language,
|
language: envelope.documentMeta.language,
|
||||||
@@ -185,6 +201,7 @@ export const run = async ({
|
|||||||
envelopeItems: envelopeItems.map((item) => item.title),
|
envelopeItems: envelopeItems.map((item) => item.title),
|
||||||
pageWidth: PDF_SIZE_A4_72PPI.width,
|
pageWidth: PDF_SIZE_A4_72PPI.width,
|
||||||
pageHeight: PDF_SIZE_A4_72PPI.height,
|
pageHeight: PDF_SIZE_A4_72PPI.height,
|
||||||
|
envelopeCompletedAuditLog,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use Playwright-based PDF generation if enabled, otherwise use Konva-based generation.
|
// Use Playwright-based PDF generation if enabled, otherwise use Konva-based generation.
|
||||||
@@ -263,22 +280,13 @@ export const run = async ({
|
|||||||
id: envelope.id,
|
id: envelope.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED,
|
status: finalEnvelopeStatus,
|
||||||
completedAt: new Date(),
|
completedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: envelopeCompletedAuditLog,
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
requestMetadata,
|
|
||||||
user: null,
|
|
||||||
data: {
|
|
||||||
transactionId: nanoid(),
|
|
||||||
...(isRejected ? { isRejected: true, rejectionReason: rejectionReason } : {}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PDF } from '@libpdf/core';
|
import { PDF } from '@libpdf/core';
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
|
|
||||||
|
import { type TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
||||||
@@ -15,12 +16,20 @@ type GenerateAuditLogPdfOptions = GenerateCertificatePdfOptions & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) => {
|
export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) => {
|
||||||
const { envelope, envelopeOwner, envelopeItems, recipients, language, pageWidth, pageHeight } =
|
const {
|
||||||
options;
|
envelope,
|
||||||
|
envelopeOwner,
|
||||||
|
envelopeItems,
|
||||||
|
recipients,
|
||||||
|
language,
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
envelopeCompletedAuditLog,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(language);
|
const documentLanguage = ZSupportedLanguageCodeSchema.parse(language);
|
||||||
|
|
||||||
const [organisationClaim, auditLogs, messages] = await Promise.all([
|
const [organisationClaim, partialAuditLogs, messages] = await Promise.all([
|
||||||
getOrganisationClaimByTeamId({ teamId: envelope.teamId }),
|
getOrganisationClaimByTeamId({ teamId: envelope.teamId }),
|
||||||
getAuditLogs(envelope.id),
|
getAuditLogs(envelope.id),
|
||||||
getTranslations(documentLanguage),
|
getTranslations(documentLanguage),
|
||||||
@@ -31,6 +40,17 @@ export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) =
|
|||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const auditLogs: TDocumentAuditLog[] = [...partialAuditLogs];
|
||||||
|
|
||||||
|
if (envelopeCompletedAuditLog) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
auditLogs.unshift({
|
||||||
|
...envelopeCompletedAuditLog,
|
||||||
|
id: '',
|
||||||
|
createdAt: new Date(),
|
||||||
|
} satisfies Omit<TDocumentAuditLog, 'type'> as TDocumentAuditLog);
|
||||||
|
}
|
||||||
|
|
||||||
const auditLogPages = await renderAuditLogs({
|
const auditLogPages = await renderAuditLogs({
|
||||||
envelope,
|
envelope,
|
||||||
envelopeOwner,
|
envelopeOwner,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { FieldType } from '@prisma/client';
|
|||||||
import { prop, sortBy } from 'remeda';
|
import { prop, sortBy } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
|
||||||
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
|
||||||
import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs';
|
import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs';
|
||||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
@@ -16,7 +18,14 @@ import { getOrganisationClaimByTeamId } from '../organisation/get-organisation-c
|
|||||||
import { renderCertificate } from './render-certificate';
|
import { renderCertificate } from './render-certificate';
|
||||||
|
|
||||||
export type GenerateCertificatePdfOptions = {
|
export type GenerateCertificatePdfOptions = {
|
||||||
envelope: Envelope & {
|
/**
|
||||||
|
* Note: completedAt is not included since it's not real at this point in time.
|
||||||
|
*
|
||||||
|
* If we actually need it here in the future, we will need to preserve the
|
||||||
|
* completedAt value and pass it to the final `envelope.update` function when
|
||||||
|
* the document is initially sealed.
|
||||||
|
*/
|
||||||
|
envelope: Omit<Envelope, 'completedAt'> & {
|
||||||
documentMeta: DocumentMeta;
|
documentMeta: DocumentMeta;
|
||||||
};
|
};
|
||||||
envelopeOwner: {
|
envelopeOwner: {
|
||||||
@@ -30,6 +39,7 @@ export type GenerateCertificatePdfOptions = {
|
|||||||
language?: string;
|
language?: string;
|
||||||
pageWidth: number;
|
pageWidth: number;
|
||||||
pageHeight: number;
|
pageHeight: number;
|
||||||
|
envelopeCompletedAuditLog?: CreateDocumentAuditLogDataResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateCertificatePdf = async (options: GenerateCertificatePdfOptions) => {
|
export const generateCertificatePdf = async (options: GenerateCertificatePdfOptions) => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export type AuditLogRecipient = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type GenerateAuditLogsOptions = {
|
type GenerateAuditLogsOptions = {
|
||||||
envelope: Envelope & {
|
envelope: Omit<Envelope, 'completedAt'> & {
|
||||||
documentMeta: DocumentMeta;
|
documentMeta: DocumentMeta;
|
||||||
};
|
};
|
||||||
envelopeItems: string[];
|
envelopeItems: string[];
|
||||||
@@ -168,7 +168,7 @@ const renderVerticalLabelAndText = (options: RenderVerticalLabelAndTextOptions)
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RenderOverviewCardOptions = {
|
type RenderOverviewCardOptions = {
|
||||||
envelope: Envelope & {
|
envelope: Omit<Envelope, 'completedAt'> & {
|
||||||
documentMeta: DocumentMeta;
|
documentMeta: DocumentMeta;
|
||||||
};
|
};
|
||||||
envelopeItems: string[];
|
envelopeItems: string[];
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const getDevice = (userAgent?: string | null): string => {
|
|||||||
const textMutedForegroundLight = '#929DAE';
|
const textMutedForegroundLight = '#929DAE';
|
||||||
const textForeground = '#000';
|
const textForeground = '#000';
|
||||||
const textMutedForeground = '#64748B';
|
const textMutedForeground = '#64748B';
|
||||||
|
const textRejectedRed = '#dc2626';
|
||||||
const textBase = 10;
|
const textBase = 10;
|
||||||
const textSm = 9;
|
const textSm = 9;
|
||||||
const textXs = 8;
|
const textXs = 8;
|
||||||
@@ -97,6 +98,8 @@ type RenderLabelAndTextOptions = {
|
|||||||
text: string;
|
text: string;
|
||||||
width: number;
|
width: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
|
labelFill?: string;
|
||||||
|
valueFill?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLabelAndText = (options: RenderLabelAndTextOptions) => {
|
const renderLabelAndText = (options: RenderLabelAndTextOptions) => {
|
||||||
@@ -106,13 +109,16 @@ const renderLabelAndText = (options: RenderLabelAndTextOptions) => {
|
|||||||
y,
|
y,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const labelFill = options.labelFill ?? textMutedForeground;
|
||||||
|
const valueFill = options.valueFill ?? textMutedForeground;
|
||||||
|
|
||||||
const label = new Konva.Text({
|
const label = new Konva.Text({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
text: `${options.label}: `,
|
text: `${options.label}: `,
|
||||||
fontStyle: fontMedium,
|
fontStyle: fontMedium,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
fill: textMutedForeground,
|
fill: labelFill,
|
||||||
fontSize: textSm,
|
fontSize: textSm,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,7 +130,7 @@ const renderLabelAndText = (options: RenderLabelAndTextOptions) => {
|
|||||||
width: width - label.width(),
|
width: width - label.width(),
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
text: options.text,
|
text: options.text,
|
||||||
fill: textMutedForeground,
|
fill: valueFill,
|
||||||
wrap: 'char',
|
wrap: 'char',
|
||||||
fontSize: textSm,
|
fontSize: textSm,
|
||||||
});
|
});
|
||||||
@@ -269,6 +275,8 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
|
|||||||
|
|
||||||
const columnWidth = width - columnPadding;
|
const columnWidth = width - columnPadding;
|
||||||
|
|
||||||
|
const isRejected = Boolean(recipient.logs.rejected);
|
||||||
|
|
||||||
if (recipient.signatureField?.secondaryId) {
|
if (recipient.signatureField?.secondaryId) {
|
||||||
// Signature container with green border
|
// Signature container with green border
|
||||||
const signatureContainer = new Konva.Group({ x: 0, y: 0 });
|
const signatureContainer = new Konva.Group({ x: 0, y: 0 });
|
||||||
@@ -313,7 +321,10 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
|
|||||||
signatureContainer.add(typedSig);
|
signatureContainer.add(typedSig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not add the signature container for rejected recipients.
|
||||||
|
if (!isRejected) {
|
||||||
column.add(signatureContainer);
|
column.add(signatureContainer);
|
||||||
|
}
|
||||||
|
|
||||||
const signatureHeight = Math.max(signatureContainer.getClientRect().height, minSignatureHeight);
|
const signatureHeight = Math.max(signatureContainer.getClientRect().height, minSignatureHeight);
|
||||||
|
|
||||||
@@ -342,7 +353,7 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
|
|||||||
// Signature ID
|
// Signature ID
|
||||||
const sigIdLabel = new Konva.Text({
|
const sigIdLabel = new Konva.Text({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: signatureHeight + 10,
|
y: isRejected ? 0 : signatureHeight + 10,
|
||||||
text: `${i18n._(msg`Signature ID`)}:`,
|
text: `${i18n._(msg`Signature ID`)}:`,
|
||||||
fill: textMutedForeground,
|
fill: textMutedForeground,
|
||||||
width: columnWidth,
|
width: columnWidth,
|
||||||
@@ -376,9 +387,11 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
|
|||||||
column.add(naText);
|
column.add(naText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relevantLog = isRejected ? recipient.logs.rejected : recipient.logs.completed;
|
||||||
|
|
||||||
const ipLabelAndText = renderLabelAndText({
|
const ipLabelAndText = renderLabelAndText({
|
||||||
label: i18n._(msg`IP Address`),
|
label: i18n._(msg`IP Address`),
|
||||||
text: recipient.logs.completed?.ipAddress ?? i18n._(msg`Unknown`),
|
text: relevantLog?.ipAddress ?? i18n._(msg`Unknown`),
|
||||||
width,
|
width,
|
||||||
y: column.getClientRect().height + 6,
|
y: column.getClientRect().height + 6,
|
||||||
});
|
});
|
||||||
@@ -386,7 +399,7 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
|
|||||||
|
|
||||||
const deviceLabelAndText = renderLabelAndText({
|
const deviceLabelAndText = renderLabelAndText({
|
||||||
label: i18n._(msg`Device`),
|
label: i18n._(msg`Device`),
|
||||||
text: getDevice(recipient.logs.completed?.userAgent),
|
text: getDevice(relevantLog?.userAgent),
|
||||||
width,
|
width,
|
||||||
y: column.getClientRect().height + 6,
|
y: column.getClientRect().height + 6,
|
||||||
});
|
});
|
||||||
@@ -400,7 +413,14 @@ const renderColumnThree = (options: RenderColumnOptions) => {
|
|||||||
|
|
||||||
const column = new Konva.Group();
|
const column = new Konva.Group();
|
||||||
|
|
||||||
const itemsToRender = [
|
type DetailItem = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
labelFill?: string;
|
||||||
|
valueFill?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemsToRender: DetailItem[] = [
|
||||||
{
|
{
|
||||||
label: i18n._(msg`Sent`),
|
label: i18n._(msg`Sent`),
|
||||||
value: recipient.logs.emailed
|
value: recipient.logs.emailed
|
||||||
@@ -429,6 +449,8 @@ const renderColumnThree = (options: RenderColumnOptions) => {
|
|||||||
value: DateTime.fromJSDate(recipient.logs.rejected.createdAt)
|
value: DateTime.fromJSDate(recipient.logs.rejected.createdAt)
|
||||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)'),
|
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)'),
|
||||||
|
labelFill: textRejectedRed,
|
||||||
|
valueFill: textRejectedRed,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
itemsToRender.push({
|
itemsToRender.push({
|
||||||
@@ -459,6 +481,8 @@ const renderColumnThree = (options: RenderColumnOptions) => {
|
|||||||
text: item.value,
|
text: item.value,
|
||||||
width,
|
width,
|
||||||
y: column.getClientRect().height + (index === 0 ? 0 : 8),
|
y: column.getClientRect().height + (index === 0 ? 0 : 8),
|
||||||
|
labelFill: item.labelFill,
|
||||||
|
valueFill: item.valueFill,
|
||||||
});
|
});
|
||||||
column.add(labelAndText);
|
column.add(labelAndText);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user