feat(send-dialog): surface per-port attachment threshold in preview UI

Per PRE-DEPLOY-PLAN § 1.3.9. Adds an informational banner to the
SendDocumentDialog explaining the size cutoff at which the attachment
switches from inline to a 24h signed-link download. Threshold sourced
from the existing `email_attach_threshold_mb` setting, plumbed through
the previewBody return shape so rep-facing dialogs don't need to call
the admin-only sales-config endpoint.

Bounce monitoring deferred to land alongside the email_bounces table
in Step 3 (schema additions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 15:27:37 +02:00
parent d556bb88f7
commit fd2c7d6b12
2 changed files with 32 additions and 3 deletions

View File

@@ -56,7 +56,12 @@ interface SendDocumentDialogProps {
}
interface PreviewResponse {
data: { html: string; markdown: string; unresolved: string[] };
data: {
html: string;
markdown: string;
unresolved: string[];
attachmentThresholdMb: number;
};
}
export function SendDocumentDialog(props: SendDocumentDialogProps) {
@@ -97,6 +102,9 @@ function SendDocumentDialogInner({
// Live preview via /api/v1/document-sends/preview. Re-runs whenever the
// body text or recipient changes (debounce-by-react-query for free).
// The preview also surfaces the per-port attachment-size threshold so
// the rep can see up-front whether their attachment will go inline vs
// as a 24h download link.
const previewQuery = useQuery<PreviewResponse>({
queryKey: [
'document-sends-preview',
@@ -205,6 +213,13 @@ function SendDocumentDialogInner({
</p>
</div>
{previewQuery.data?.data.attachmentThresholdMb !== undefined && (
<p className="rounded border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-700">
Files over <strong>{previewQuery.data.data.attachmentThresholdMb} MB</strong> are
sent as a 24-hour download link instead of an inline attachment.
</p>
)}
<div className="space-y-1">
<Label htmlFor="ds-body">Message body</Label>
<Textarea

View File

@@ -263,7 +263,16 @@ export async function previewBody(
recipient: SendRecipientInput,
customBody: string | null,
ctx: { berthId?: string; brochureLabel?: string } = {},
): Promise<{ html: string; markdown: string; unresolved: string[] }> {
): Promise<{
html: string;
markdown: string;
unresolved: string[];
/** Per-port size cutoff at which the attachment is replaced with a
* 24-hour signed-link in the email body. Surfaced to the compose UI
* so the rep sees up-front whether their attachment will go inline
* vs as a link. */
attachmentThresholdMb: number;
}> {
const content = await getSalesContentConfig(portId);
const template = customBody?.trim()?.length
? customBody
@@ -274,7 +283,12 @@ export async function previewBody(
const expanded = expandMergeTokens(template, values);
const unresolved = findUnresolvedTokens(template, values);
const html = renderEmailBody(expanded);
return { html, markdown: expanded, unresolved };
return {
html,
markdown: expanded,
unresolved,
attachmentThresholdMb: content.emailAttachThresholdMb,
};
}
// ─── Internal helpers ────────────────────────────────────────────────────────