From 7f04c765f4e7212c80cc19a31ffaab43d661b450 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 18 Jun 2026 17:36:35 +0200 Subject: [PATCH] fix(crm): inquiry detail polish, EOI preview mime, EOI next-step, documenso v1 banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - inquiries: format triage badges with labels (Open/Assigned/Converted/Dismissed), surface the lead's free-text message for every kind, and gate the raw-payload tab to super admins (was exposing raw JSON to all users) - file preview: fall back to the server-resolved mime (getPreviewUrl already returns it) so files whose stored name lacks a .pdf extension — e.g. migration-backfilled signed EOIs — render instead of "preview not supported" - interest overview: a signed EOI left at stage=eoi no longer shows as "NEXT STEP"; completion ordering rolls the next step to Reservation (display only, no pipeline_stage change) - documenso admin: warning banner discouraging the deprecated v1 API + what breaks on it Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[portSlug]/admin/documenso/page.tsx | 30 +++++++++++++++++++ src/components/files/file-preview-dialog.tsx | 10 +++++-- src/components/inquiries/inquiry-columns.tsx | 9 +++++- src/components/inquiries/inquiry-detail.tsx | 21 ++++++++++--- src/components/interests/interest-tabs.tsx | 13 +++++++- 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx b/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx index c52859ac..b9f327a4 100644 --- a/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx +++ b/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx @@ -7,6 +7,7 @@ import { TemplateSyncButton } from '@/components/admin/documenso/template-sync-b import { WebhookHealthCard } from '@/components/admin/documenso/webhook-health-card'; import { PageHeader } from '@/components/shared/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { WarningCallout } from '@/components/ui/warning-callout'; // All field arrays removed - every Documenso setting now flows through // `RegistryDrivenForm`, which surfaces the env-fallback / port / global @@ -22,6 +23,35 @@ export default function DocumensoSettingsPage() { description="API credentials, signer identities, templates, and signing behaviour for every document the CRM puts out for signature (EOI, reservation, contract, custom uploads). Use the test-connection button to verify a saved configuration before relying on it." /> + +

+ The CRM's signing features are built for Documenso 2.x (v2). Set the API version + below to v1 only if this port still points at a Documenso 1.13.x server. + Be aware these CRM functions do not work (or run degraded) on v1: +

+
    +
  • + Editing an envelope after it is created (title, subject, redirect URL): + hard-fails, because v1 has no /envelope/update endpoint. +
  • +
  • + Upload-and-send contracts / reservations fall back to v1's + per-field placement: page size is assumed to be A4, and rich field metadata (required + flags, NUMBER min/max, CHECKBOX / DROPDOWN / RADIO option lists) is dropped. +
  • +
  • + One-call send with per-recipient signing links,{' '} + sequential signing enforcement, and the{' '} + v2 webhook events (recipient viewed / signed, declined, reminder sent) + are unavailable or ignored on v1. +
  • +
+

+ Recommended: upgrade the Documenso server to 2.x, then set the API version to v2 and run + the test-connection button to confirm. +

+
+ diff --git a/src/components/files/file-preview-dialog.tsx b/src/components/files/file-preview-dialog.tsx index 007b2ba1..d2d335ac 100644 --- a/src/components/files/file-preview-dialog.tsx +++ b/src/components/files/file-preview-dialog.tsx @@ -104,7 +104,7 @@ export function FilePreviewDialog({ // useQuery replaces the prior useEffect(fetch+setState) pattern. The // request is gated on the dialog being open and a fileId being set. - const previewQuery = useQuery<{ data: { url: string } }>({ + const previewQuery = useQuery<{ data: { url: string; mimeType?: string } }>({ queryKey: ['file-preview', fileId], queryFn: () => apiFetch(`/api/v1/files/${fileId}/preview`), enabled: open && !!fileId, @@ -113,7 +113,13 @@ export function FilePreviewDialog({ const loading = previewQuery.isLoading; const error = previewQuery.error ? 'Failed to load preview' : null; - const kind = previewKindFor(mimeType, fileName); + // Prefer the caller-supplied mime, but fall back to the server's resolved + // mime (getPreviewUrl returns it). Without this, callers that pass only a + // display name (e.g. the EOI tab passing "EOI - ") or files whose + // stored name lacks a `.pdf` extension (migration-backfilled EOIs) fall + // through to the "unknown" surface even though the server knows it's a PDF. + const resolvedMime = mimeType ?? previewQuery.data?.data.mimeType; + const kind = previewKindFor(resolvedMime, fileName); return ( diff --git a/src/components/inquiries/inquiry-columns.tsx b/src/components/inquiries/inquiry-columns.tsx index 735534c6..4ecd16c8 100644 --- a/src/components/inquiries/inquiry-columns.tsx +++ b/src/components/inquiries/inquiry-columns.tsx @@ -49,6 +49,13 @@ export const TRIAGE_TONE: Record = { dismissed: 'bg-slate-100 text-slate-600', }; +export const TRIAGE_LABELS: Record = { + open: 'Open', + assigned: 'Assigned', + converted: 'Converted', + dismissed: 'Dismissed', +}; + export const INQUIRY_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [ { id: 'contactEmail', label: 'Email' }, { id: 'kind', label: 'Type' }, @@ -114,7 +121,7 @@ export function getInquiryColumns({ const state = row.original.triageState; return (
- {state} + {TRIAGE_LABELS[state]} {row.original.convertedInterestId ? ( (); const portSlug = params?.portSlug ?? ''; + const { isSuperAdmin } = usePermissions(); const { data, isLoading, error } = useQuery({ queryKey: ['inquiries', id], @@ -74,6 +77,10 @@ export function InquiryDetail({ id }: { id: string }) { const p = (data?.payload ?? {}) as Record; const str = (k: string) => (typeof p[k] === 'string' ? (p[k] as string) : ''); + // The free-text message a lead left. Website forms use different keys + // (contact form -> `comments`; others -> `message`/`comment`), so probe the + // common ones and surface it for every inquiry kind. + const comment = str('comments') || str('message') || str('comment') || str('notes'); const tabs: DetailTab[] = [ { @@ -88,7 +95,9 @@ export function InquiryDetail({ id }: { id: string }) { ) : null} {data?.kind === 'berth_inquiry' ? : null} - {data?.kind === 'contact_form' ? : null} + {comment ? ( + {comment}} /> + ) : null} @@ -107,7 +116,9 @@ export function InquiryDetail({ id }: { id: string }) { label="Status" value={ data ? ( - {data.triageState} + + {TRIAGE_LABELS[data.triageState]} + ) : ( '' ) @@ -155,7 +166,7 @@ export function InquiryDetail({ id }: { id: string }) { ), }, - ]; + ].filter((tab) => tab.id !== 'payload' || isSuperAdmin); return (

{data?.contactName || '(no name)'}

{data ? ( - {data.triageState} + + {TRIAGE_LABELS[data.triageState]} + ) : null}

diff --git a/src/components/interests/interest-tabs.tsx b/src/components/interests/interest-tabs.tsx index e33321c9..96218a16 100644 --- a/src/components/interests/interest-tabs.tsx +++ b/src/components/interests/interest-tabs.tsx @@ -848,7 +848,18 @@ function OverviewTab({ deposit_paid: 'deposit', contract: 'contract', }; - const stageOwnedMilestone = STAGE_TO_MILESTONE[interest.pipelineStage as PipelineStage] ?? null; + const stageOwnedMilestoneRaw = + STAGE_TO_MILESTONE[interest.pipelineStage as PipelineStage] ?? null; + // B2 (2026-06-18): if the stage-owned milestone is already COMPLETE — e.g. a + // migrated deal left at stage=eoi with a signed EOI that never auto-advanced — + // don't pin it as the current "NEXT STEP". Falling back to null makes phaseFor + // use completion ordering, so the signed milestone shows as done/past and the + // next incomplete one (Reservation) becomes current. Display-only; the + // pipeline_stage column is unchanged. + const stageOwnedMilestoneComplete = stageOwnedMilestoneRaw + ? milestoneCompletion[stageOwnedMilestoneRaw] + : false; + const stageOwnedMilestone = stageOwnedMilestoneComplete ? null : stageOwnedMilestoneRaw; const stageOwnedIdx = stageOwnedMilestone ? order.indexOf(stageOwnedMilestone) : -1; const phaseFor = (k: (typeof order)[number]): Phase => { // Stage owns this milestone → always current, never collapsed.