mirror of
https://github.com/documenso/documenso.git
synced 2026-07-08 20:14:58 +10:00
feat(pdf): redesign audit log and signing certificate documents
This commit is contained in:
@@ -1,48 +1,20 @@
|
||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE, type TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
|
||||
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { DateTimeFormatOptions } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match, P } from 'ts-pattern';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
export type AuditLogDataTableProps = {
|
||||
logs: TDocumentAuditLog[];
|
||||
};
|
||||
|
||||
const dateFormat: DateTimeFormatOptions = {
|
||||
...DateTime.DATETIME_SHORT,
|
||||
hourCycle: 'h12',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the color indicator for the audit log type
|
||||
*/
|
||||
|
||||
const getAuditLogIndicatorColor = (type: string) =>
|
||||
match(type)
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => 'bg-green-500')
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => 'bg-red-500')
|
||||
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, () => 'bg-orange-500')
|
||||
.with(
|
||||
P.union(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED, DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED),
|
||||
() => 'bg-blue-500',
|
||||
)
|
||||
.otherwise(() => 'bg-muted');
|
||||
|
||||
/**
|
||||
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
|
||||
*/
|
||||
|
||||
const formatUserAgent = (userAgent: string | null | undefined, userAgentInfo: UAParser.IResult) => {
|
||||
if (!userAgent) {
|
||||
return msg`N/A`;
|
||||
}
|
||||
|
||||
const formatUserAgent = (userAgent: string, userAgentInfo: UAParser.IResult) => {
|
||||
const browser = userAgentInfo.browser.name;
|
||||
const version = userAgentInfo.browser.version;
|
||||
const os = userAgentInfo.os.name;
|
||||
@@ -63,74 +35,45 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
|
||||
const parser = new UAParser();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="divide-y divide-border">
|
||||
{logs.map((log, index) => {
|
||||
parser.setUA(log.userAgent || '');
|
||||
const formattedAction = formatDocumentAuditLogAction(i18n, log);
|
||||
const userAgentInfo = parser.getResult();
|
||||
|
||||
const createdAt = DateTime.fromJSDate(log.createdAt).setLocale(APP_I18N_OPTIONS.defaultLocale);
|
||||
|
||||
const metaSegments = [
|
||||
log.email,
|
||||
log.ipAddress,
|
||||
log.userAgent ? _(formatUserAgent(log.userAgent, userAgentInfo)) : null,
|
||||
].filter((segment): segment is string => Boolean(segment));
|
||||
|
||||
return (
|
||||
<Card
|
||||
<div
|
||||
key={index}
|
||||
// Add top margin for the first card to ensure it's not cut off from the 2nd page onwards
|
||||
className={`border shadow-sm ${index > 0 ? 'print:mt-8' : ''}`}
|
||||
className="flex gap-5 py-2.5"
|
||||
style={{
|
||||
pageBreakInside: 'avoid',
|
||||
breakInside: 'avoid',
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
{/* Header Section with indicator, event type, and timestamp */}
|
||||
<div className="mb-3 flex items-start justify-between">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<div className={cn(`h-2 w-2 rounded-full`, getAuditLogIndicatorColor(log.type))} />
|
||||
<div className="w-[5.5rem] shrink-0">
|
||||
<div className="text-foreground text-xs">{createdAt.toFormat('yyyy-MM-dd')}</div>
|
||||
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-sm uppercase tracking-wide print:text-[8pt]">
|
||||
{log.type.replace(/_/g, ' ')}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[0.6875rem] text-muted-foreground">{createdAt.toFormat('hh:mm:ss a')}</div>
|
||||
</div>
|
||||
|
||||
<div className="font-medium text-foreground text-sm print:text-[8pt]">
|
||||
{formattedAction.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-pretty text-foreground text-sm print:text-xs">{formattedAction.description}</div>
|
||||
|
||||
{metaSegments.length > 0 && (
|
||||
<div className="mt-1 break-words text-muted-foreground text-xs print:text-[0.6875rem]">
|
||||
{metaSegments.join(' · ')}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-sm print:text-[8pt]">
|
||||
{DateTime.fromJSDate(log.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toLocaleString(dateFormat)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
{/* Details Section - Two column layout */}
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">{_(msg`User`)}</div>
|
||||
|
||||
<div className="mt-1 font-mono text-foreground">{log.email || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
|
||||
{_(msg`IP Address`)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 font-mono text-foreground">{log.ipAddress || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
|
||||
{_(msg`User Agent`)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-foreground">{_(formatUserAgent(log.userAgent, userAgentInfo))}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
|
||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
@@ -106,82 +105,86 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
return (
|
||||
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
|
||||
<div className="mb-6 border-b pb-4">
|
||||
<h1 className="font-semibold text-xl">{_(msg`Audit Log`)}</h1>
|
||||
</div>
|
||||
<header>
|
||||
<h1 className="font-semibold text-lg tracking-tight">{_(msg`Audit Log`)}</h1>
|
||||
|
||||
<Card>
|
||||
<CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs">
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Envelope ID`)}</span>
|
||||
<p className="mt-1 text-pretty text-muted-foreground text-sm">{document.title}</p>
|
||||
</header>
|
||||
|
||||
<span className="mt-1 block break-words">{document.envelopeId}</span>
|
||||
</p>
|
||||
<dl className="mt-6 grid grid-cols-2 gap-x-8 gap-y-5">
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Envelope ID`)}</dt>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Enclosed Document`)}</span>
|
||||
<dd className="mt-1 break-all text-foreground text-sm print:text-xs">{document.envelopeId}</dd>
|
||||
</div>
|
||||
|
||||
<span className="mt-1 block break-words">{document.title}</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Owner`)}</dt>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Status`)}</span>
|
||||
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">
|
||||
{document.user.name} ({document.user.email})
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<span className="mt-1 block">
|
||||
{_(document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description).toUpperCase()}
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Status`)}</dt>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Owner`)}</span>
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
{_(document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<span className="mt-1 block break-words">
|
||||
{document.user.name} ({document.user.email})
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Time Zone`)}</dt>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Created At`)}</span>
|
||||
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">
|
||||
{document.documentMeta?.timezone ?? 'N/A'}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<span className="mt-1 block">
|
||||
{DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Created At`)}</dt>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Last Updated`)}</span>
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
{DateTime.fromJSDate(document.createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<span className="mt-1 block">
|
||||
{DateTime.fromJSDate(document.updatedAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Last Updated`)}</dt>
|
||||
|
||||
<p>
|
||||
<span className="font-medium">{_(msg`Time Zone`)}</span>
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
{DateTime.fromJSDate(document.updatedAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<span className="mt-1 block break-words">{document.documentMeta?.timezone ?? 'N/A'}</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Enclosed Document`)}</dt>
|
||||
|
||||
<div>
|
||||
<p className="font-medium">{_(msg`Recipients`)}</p>
|
||||
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">{document.title}</dd>
|
||||
</div>
|
||||
|
||||
<ul className="mt-1 list-inside list-disc">
|
||||
<div>
|
||||
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Recipients`)}</dt>
|
||||
|
||||
<dd className="mt-1 text-foreground text-sm print:text-xs">
|
||||
<ul className="space-y-0.5">
|
||||
{document.recipients.map((recipient) => (
|
||||
<li key={recipient.id}>
|
||||
<li key={recipient.id} className="break-words">
|
||||
{recipient.name} ({recipient.email}) ·{' '}
|
||||
<span className="text-muted-foreground">
|
||||
[{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]
|
||||
</span>{' '}
|
||||
{recipient.name} ({recipient.email})
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="mt-8">
|
||||
<InternalAuditLogTable logs={auditLogs} />
|
||||
|
||||
@@ -9,11 +9,8 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@documenso/ui/primitives/table';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType, FieldType, SigningStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { redirect } from 'react-router';
|
||||
@@ -203,165 +200,178 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
|
||||
return (
|
||||
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
|
||||
<div className="flex items-center">
|
||||
<h1 className="my-8 font-bold text-2xl">{_(msg`Signing Certificate`)}</h1>
|
||||
</div>
|
||||
<header>
|
||||
<h1 className="font-semibold text-lg tracking-tight">{_(msg`Signing Certificate`)}</h1>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table overflowHidden>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{_(msg`Signer Events`)}</TableHead>
|
||||
<TableHead>{_(msg`Signature`)}</TableHead>
|
||||
<TableHead>{_(msg`Details`)}</TableHead>
|
||||
{/* <TableHead>Security</TableHead> */}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<p className="mt-1 text-pretty text-muted-foreground text-sm">{document.title}</p>
|
||||
</header>
|
||||
|
||||
<TableBody className="print:text-xs">
|
||||
{document.recipients.map((recipient, i) => {
|
||||
const logs = getRecipientAuditLogs(recipient.id);
|
||||
const signature = getRecipientSignatureField(recipient.id);
|
||||
<table className="mt-6 w-full">
|
||||
<thead>
|
||||
<tr className="border-border border-b">
|
||||
<th className="w-[30%] pr-4 pb-2 text-left font-medium text-muted-foreground text-xs">
|
||||
{_(msg`Signer Events`)}
|
||||
</th>
|
||||
|
||||
return (
|
||||
<TableRow key={i} className="print:break-inside-avoid">
|
||||
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
|
||||
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
|
||||
<div className="break-all">{recipient.email}</div>
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</p>
|
||||
<th className="w-[30%] pr-4 pb-2 text-left font-medium text-muted-foreground text-xs">
|
||||
{_(msg`Signature`)}
|
||||
</th>
|
||||
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
|
||||
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
<th className="pb-2 text-left font-medium text-muted-foreground text-xs">{_(msg`Details`)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
||||
{signature ? (
|
||||
<>
|
||||
<div
|
||||
className="inline-block rounded-lg p-1"
|
||||
style={{
|
||||
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
|
||||
}}
|
||||
>
|
||||
{signature.signature?.signatureImageAsBase64 && (
|
||||
<img
|
||||
src={`${signature.signature?.signatureImageAsBase64}`}
|
||||
alt="Signature"
|
||||
className="max-h-12 max-w-full"
|
||||
/>
|
||||
)}
|
||||
<tbody className="divide-y divide-border">
|
||||
{document.recipients.map((recipient, i) => {
|
||||
const logs = getRecipientAuditLogs(recipient.id);
|
||||
const signature = getRecipientSignatureField(recipient.id);
|
||||
const isRejected = Boolean(logs.DOCUMENT_RECIPIENT_REJECTED[0]);
|
||||
|
||||
{signature.signature?.typedSignature && (
|
||||
<p className="text-center font-signature text-sm">
|
||||
{signature.signature?.typedSignature}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<tr key={i} className="align-top print:break-inside-avoid">
|
||||
<td className="py-3 pr-4">
|
||||
<div className="hyphens-auto break-words font-medium text-sm print:text-xs">{recipient.name}</div>
|
||||
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
|
||||
<span className="block font-mono uppercase">{signature.secondaryId}</span>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-muted-foreground">
|
||||
<Trans>N/A</Trans>
|
||||
</p>
|
||||
<div className="break-all text-muted-foreground text-xs">{recipient.email}</div>
|
||||
|
||||
<div className="mt-0.5 text-muted-foreground text-xs">
|
||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5">
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Authentication Level`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{getAuthenticationLevel(recipient.id)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-3 pr-4">
|
||||
{signature ? (
|
||||
<div className="space-y-2.5">
|
||||
{!isRejected && (
|
||||
<div
|
||||
className="inline-block rounded-lg p-1"
|
||||
style={{
|
||||
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
|
||||
}}
|
||||
>
|
||||
{signature.signature?.signatureImageAsBase64 && (
|
||||
<img
|
||||
src={`${signature.signature?.signatureImageAsBase64}`}
|
||||
alt="Signature"
|
||||
className="max-h-12 max-w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{signature.signature?.typedSignature && (
|
||||
<p className="text-center font-signature text-sm">{signature.signature?.typedSignature}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Signature ID`)}</div>
|
||||
|
||||
<p className="mt-1 text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||
</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell truncate={false} className="w-[min-content] align-top">
|
||||
<div className="space-y-1">
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Sent`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.EMAIL_SENT[0]
|
||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: logs.DOCUMENT_SENT[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_OPENED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_REJECTED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground text-sm print:text-xs">
|
||||
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
|
||||
<span className="inline-block">
|
||||
{recipient.signingStatus === SigningStatus.REJECTED
|
||||
? recipient.rejectionReason
|
||||
: _(
|
||||
isOwner(recipient.email)
|
||||
? FRIENDLY_SIGNING_REASONS['__OWNER__']
|
||||
: FRIENDLY_SIGNING_REASONS[recipient.role],
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
<div className="mt-0.5 break-all font-mono text-xs uppercase">{signature.secondaryId}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm print:text-xs">{_(msg`N/A`)}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-2.5">
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`IP Address`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5">
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Device`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-3">
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Sent`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.EMAIL_SENT[0]
|
||||
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: logs.DOCUMENT_SENT[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Viewed`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.DOCUMENT_OPENED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
|
||||
<div>
|
||||
<div className="font-medium text-red-600 text-xs">{_(msg`Rejected`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-red-600 text-sm print:text-xs">
|
||||
{DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_REJECTED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Signed`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
|
||||
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
|
||||
.setLocale(APP_I18N_OPTIONS.defaultLocale)
|
||||
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
|
||||
: _(msg`Unknown`)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="font-medium text-muted-foreground text-xs">{_(msg`Reason`)}</div>
|
||||
|
||||
<div className="mt-0.5 text-foreground text-sm print:text-xs">
|
||||
{recipient.signingStatus === SigningStatus.REJECTED
|
||||
? recipient.rejectionReason
|
||||
: _(
|
||||
isOwner(recipient.email)
|
||||
? FRIENDLY_SIGNING_REASONS['__OWNER__']
|
||||
: FRIENDLY_SIGNING_REASONS[recipient.role],
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="my-8 flex-row-reverse space-y-4">
|
||||
@@ -377,7 +387,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<p className="flex-shrink-0 font-medium text-sm print:text-xs">
|
||||
<p className="flex-shrink-0 font-medium text-muted-foreground text-xs">
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
|
||||
@@ -137,6 +137,7 @@ export const generateCertificatePdf = async (options: GenerateCertificatePdfOpti
|
||||
}),
|
||||
envelopeOwner,
|
||||
envelopeId: envelope.id,
|
||||
envelopeTitle: envelope.title,
|
||||
qrToken: envelope.qrToken,
|
||||
hidePoweredBy: organisationClaim.flags.hidePoweredBy ?? false,
|
||||
pageWidth,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user