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: +

+ +

+ 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.