From 689a114abad5ff9df0a8e9e319b5aae13751717b Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 May 2026 12:12:40 +0200 Subject: [PATCH] fix(audit-wave-9): copy/terminology sweep (copy-auditor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the highest-impact items from the copy-auditor's CRITICAL + HIGH + MEDIUM bands: **C2 portal raw-status leak** - Drop the staff-only `leadCategory` chip from the portal interests page entirely. Privacy + optics: clients should never see "hot lead" in their own portal. `eoiStatus` was already wrapped in `portalSigningLabel`; only the categorical chip remained. **C3 signing-status label drift** - Add `src/lib/labels/document-status.ts` as the single source of truth for the {draft, sent, partially_signed, completed, expired, cancelled} lifecycle: labels (CRM + portal variants), StatusPill variant, and the "active / in-flight" set. - Wire it into interest-eoi-tab, interest-contract-tab, interest-reservation-tab — they previously redefined identical STATUS_LABELS / ACTIVE_STATUSES blocks per-file. **H1 + M3 verbiage codemod** - `Save Changes` → `Save changes` (sentence case, matches the surrounding admin/CRM pattern). - `Saving...` (ASCII three dots) → `Saving…` (Unicode ellipsis). Matches the project's UTF-8-elsewhere convention and reads correctly via screen-readers. **M1 envelope jargon → signing request** - smart-archive-dialog: "Leave envelope pending" → "Leave signing request pending"; "Void the signing envelope" → "Cancel the signing request"; section header updated to match. - document-detail: "voids the signing envelope" → "cancels the signing request". - bulk-archive-wizard: "leave invoices/signing envelopes alone" → "leave invoices/signing requests alone". - Documenso admin page intentionally keeps `envelope` (dev/integration vocabulary). **M5 Hot Lead casing** - Normalize `Hot Lead` / `General Interest` / `Specific Qualified` to sentence case in `constants.ts` LABEL_OVERRIDES and all per-file lead-category maps so the CRM trend (sentence case) is consistent. **C1 surface-level rename** - "Linked prospect (optional)" → "Linked interest (optional)" on the berth status-change dialog. - "Deal Documents" tab → "Interest Documents" (URL/route kept as `/deal-documents` to avoid breaking deep links; rename deferred). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(portal)/portal/interests/page.tsx | 9 +- .../admin/custom-fields/custom-field-form.tsx | 2 +- .../document-templates/template-form.tsx | 2 +- src/components/admin/ports/port-form.tsx | 2 +- src/components/admin/roles/role-form.tsx | 2 +- .../admin/settings/settings-manager.tsx | 2 +- src/components/admin/tags/tag-form.tsx | 2 +- src/components/admin/users/user-form.tsx | 2 +- .../admin/webhooks/webhook-form.tsx | 2 +- src/components/berths/berth-detail-header.tsx | 10 +-- src/components/berths/berth-form.tsx | 2 +- src/components/berths/berth-interests-tab.tsx | 6 +- src/components/berths/berth-tabs.tsx | 2 +- .../clients/bulk-archive-wizard.tsx | 2 +- src/components/clients/client-form.tsx | 2 +- .../clients/smart-archive-dialog.tsx | 8 +- src/components/companies/company-form.tsx | 2 +- src/components/documents/document-detail.tsx | 2 +- .../expenses/expense-form-dialog.tsx | 2 +- .../interests/interest-contract-tab.tsx | 20 ++--- .../interests/interest-detail-header.tsx | 4 +- src/components/interests/interest-eoi-tab.tsx | 20 ++--- src/components/interests/interest-filters.tsx | 6 +- src/components/interests/interest-form.tsx | 8 +- .../interests/interest-reservation-tab.tsx | 20 ++--- src/components/reminders/reminder-form.tsx | 2 +- src/components/yachts/yacht-form.tsx | 2 +- src/lib/constants.ts | 6 +- src/lib/labels/document-status.ts | 87 +++++++++++++++++++ 29 files changed, 159 insertions(+), 79 deletions(-) create mode 100644 src/lib/labels/document-status.ts diff --git a/src/app/(portal)/portal/interests/page.tsx b/src/app/(portal)/portal/interests/page.tsx index 4a7b0b3d..491d0b4b 100644 --- a/src/app/(portal)/portal/interests/page.tsx +++ b/src/app/(portal)/portal/interests/page.tsx @@ -86,11 +86,10 @@ export default async function PortalInterestsPage() { - {interest.berthArea} )} - {interest.leadCategory && ( -

- {interest.leadCategory.replace(/_/g, ' ')} -

- )} + {/* leadCategory ("hot_lead" / "qualified_lead" / etc.) + is a staff classification — never render to clients. + Privacy + optics: we shouldn't be telling the + prospect they're a "hot lead". */}
{interest.dateFirstContact && ( diff --git a/src/components/admin/custom-fields/custom-field-form.tsx b/src/components/admin/custom-fields/custom-field-form.tsx index 3735943c..17b520a3 100644 --- a/src/components/admin/custom-fields/custom-field-form.tsx +++ b/src/components/admin/custom-fields/custom-field-form.tsx @@ -319,7 +319,7 @@ function CustomFieldFormBody({ open, onOpenChange, field, onSuccess }: CustomFie Cancel diff --git a/src/components/admin/document-templates/template-form.tsx b/src/components/admin/document-templates/template-form.tsx index a98f448c..7cdde4fd 100644 --- a/src/components/admin/document-templates/template-form.tsx +++ b/src/components/admin/document-templates/template-form.tsx @@ -197,7 +197,7 @@ export function TemplateForm({ open, onOpenChange, template, onSuccess }: Templa Cancel diff --git a/src/components/admin/ports/port-form.tsx b/src/components/admin/ports/port-form.tsx index 162ed9a5..d5a34ddc 100644 --- a/src/components/admin/ports/port-form.tsx +++ b/src/components/admin/ports/port-form.tsx @@ -208,7 +208,7 @@ function PortFormBody({ open, onOpenChange, port, onSuccess }: PortFormProps) { Cancel diff --git a/src/components/admin/roles/role-form.tsx b/src/components/admin/roles/role-form.tsx index f2d2a120..b6560511 100644 --- a/src/components/admin/roles/role-form.tsx +++ b/src/components/admin/roles/role-form.tsx @@ -307,7 +307,7 @@ function RoleFormBody({ open, onOpenChange, role, onSuccess }: RoleFormProps) { Cancel diff --git a/src/components/admin/settings/settings-manager.tsx b/src/components/admin/settings/settings-manager.tsx index 7a90047c..58156eb2 100644 --- a/src/components/admin/settings/settings-manager.tsx +++ b/src/components/admin/settings/settings-manager.tsx @@ -503,7 +503,7 @@ export function SettingsManager() { } }} > - {saving === setting.key ? 'Saving...' : 'Save'} + {saving === setting.key ? 'Saving…' : 'Save'}
); diff --git a/src/components/admin/tags/tag-form.tsx b/src/components/admin/tags/tag-form.tsx index 4a80cccb..07452b9d 100644 --- a/src/components/admin/tags/tag-form.tsx +++ b/src/components/admin/tags/tag-form.tsx @@ -144,7 +144,7 @@ export function TagForm({ open, onOpenChange, tag, onSuccess }: TagFormProps) { Cancel diff --git a/src/components/admin/users/user-form.tsx b/src/components/admin/users/user-form.tsx index e54bc323..4dc5f658 100644 --- a/src/components/admin/users/user-form.tsx +++ b/src/components/admin/users/user-form.tsx @@ -327,7 +327,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) { Cancel diff --git a/src/components/admin/webhooks/webhook-form.tsx b/src/components/admin/webhooks/webhook-form.tsx index 520cceb6..3eebcecd 100644 --- a/src/components/admin/webhooks/webhook-form.tsx +++ b/src/components/admin/webhooks/webhook-form.tsx @@ -142,7 +142,7 @@ export function WebhookForm({ open, onOpenChange, webhook, onSuccess }: WebhookF type="submit" disabled={loading || !name.trim() || !url.trim() || events.length === 0} > - {loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Webhook'} + {loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Webhook'} diff --git a/src/components/berths/berth-detail-header.tsx b/src/components/berths/berth-detail-header.tsx index 7b5368a0..976e6952 100644 --- a/src/components/berths/berth-detail-header.tsx +++ b/src/components/berths/berth-detail-header.tsx @@ -222,16 +222,16 @@ function StatusChangeDialog({ {showInterestPicker && (
- + setValue('interestId', id ?? undefined)} />

- Link this status change to the prospect (interest) it relates to. The change will - appear on that interest's timeline, and the berth gets attached to the prospect - automatically if it wasn't already. + Link this status change to the interest it relates to. The change will appear on + that interest's timeline, and the berth gets attached to it automatically if it + wasn't already.

)} @@ -240,7 +240,7 @@ function StatusChangeDialog({ Cancel diff --git a/src/components/berths/berth-form.tsx b/src/components/berths/berth-form.tsx index 89a750e5..5a240f33 100644 --- a/src/components/berths/berth-form.tsx +++ b/src/components/berths/berth-form.tsx @@ -474,7 +474,7 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) { Cancel diff --git a/src/components/berths/berth-interests-tab.tsx b/src/components/berths/berth-interests-tab.tsx index 06fcde97..248eb93e 100644 --- a/src/components/berths/berth-interests-tab.tsx +++ b/src/components/berths/berth-interests-tab.tsx @@ -41,9 +41,9 @@ const CATEGORY_RANK: Record = { }; const CATEGORY_LABELS: Record = { - hot_lead: 'Hot Lead', - specific_qualified: 'Specific Qualified', - general_interest: 'General Interest', + hot_lead: 'Hot lead', + specific_qualified: 'Specific qualified', + general_interest: 'General interest', }; interface ListResponse { diff --git a/src/components/berths/berth-tabs.tsx b/src/components/berths/berth-tabs.tsx index 51dceedd..9452c50a 100644 --- a/src/components/berths/berth-tabs.tsx +++ b/src/components/berths/berth-tabs.tsx @@ -461,7 +461,7 @@ export function buildBerthTabs(berth: BerthData): DetailTab[] { }, { id: 'deal-documents', - label: 'Deal Documents', + label: 'Interest Documents', content: , }, // Waiting List + Maintenance Log tabs were stubs ("coming soon") diff --git a/src/components/clients/bulk-archive-wizard.tsx b/src/components/clients/bulk-archive-wizard.tsx index 37241b37..a72d2b3d 100644 --- a/src/components/clients/bulk-archive-wizard.tsx +++ b/src/components/clients/bulk-archive-wizard.tsx @@ -169,7 +169,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
Low-stakes defaults: release available/under-offer berths, keep sold ones, cancel - reservations, leave invoices/signing envelopes alone. Yachts stay on the archived + reservations, leave invoices/signing requests alone. Yachts stay on the archived client. To customise per-client, archive that client individually instead.
diff --git a/src/components/clients/client-form.tsx b/src/components/clients/client-form.tsx index 83a636bd..5e981d0a 100644 --- a/src/components/clients/client-form.tsx +++ b/src/components/clients/client-form.tsx @@ -421,7 +421,7 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }: {(isSubmitting || mutation.isPending) && ( )} - {isEdit ? 'Save Changes' : 'Create Client'} + {isEdit ? 'Save changes' : 'Create Client'} diff --git a/src/components/clients/smart-archive-dialog.tsx b/src/components/clients/smart-archive-dialog.tsx index b3e6ed13..fc9bde5e 100644 --- a/src/components/clients/smart-archive-dialog.tsx +++ b/src/components/clients/smart-archive-dialog.tsx @@ -522,12 +522,12 @@ function SmartArchiveDialogBody({ )} - {/* In-flight signing envelopes */} + {/* In-flight signing requests */} {dossier.documents.filter((d) => d.isInFlight).length > 0 && ( - In-flight signing envelopes + In-flight signing requests @@ -546,8 +546,8 @@ function SmartArchiveDialogBody({ })) } > - - + + ))} diff --git a/src/components/companies/company-form.tsx b/src/components/companies/company-form.tsx index 5189d4d4..dae13853 100644 --- a/src/components/companies/company-form.tsx +++ b/src/components/companies/company-form.tsx @@ -461,7 +461,7 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) { {(isSubmitting || mutation.isPending) && ( )} - {isEdit ? 'Save Changes' : 'Create Company'} + {isEdit ? 'Save changes' : 'Create Company'} diff --git a/src/components/documents/document-detail.tsx b/src/components/documents/document-detail.tsx index 568ea552..50b49a24 100644 --- a/src/components/documents/document-detail.tsx +++ b/src/components/documents/document-detail.tsx @@ -161,7 +161,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) { const handleCancel = async () => { const ok = await confirm({ title: 'Cancel document', - description: 'Cancel this document? This voids the signing envelope and cannot be undone.', + description: 'Cancel this document? This cancels the signing request and cannot be undone.', confirmLabel: 'Cancel document', }); if (!ok) return; diff --git a/src/components/expenses/expense-form-dialog.tsx b/src/components/expenses/expense-form-dialog.tsx index 2d76e8c6..3480a4cd 100644 --- a/src/components/expenses/expense-form-dialog.tsx +++ b/src/components/expenses/expense-form-dialog.tsx @@ -418,7 +418,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi {(isSubmitting || mutation.isPending) && ( )} - {isEdit ? 'Save Changes' : 'Create Expense'} + {isEdit ? 'Save changes' : 'Create Expense'} diff --git a/src/components/interests/interest-contract-tab.tsx b/src/components/interests/interest-contract-tab.tsx index 81ebfb35..98da4ddc 100644 --- a/src/components/interests/interest-contract-tab.tsx +++ b/src/components/interests/interest-contract-tab.tsx @@ -23,6 +23,11 @@ import { SigningProgress } from '@/components/documents/signing-progress'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { useConfirmation } from '@/hooks/use-confirmation'; +import { + DOCUMENT_STATUS_ACTIVE, + DOCUMENT_STATUS_LABELS, + type DocumentStatus, +} from '@/lib/labels/document-status'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/ui-store'; @@ -35,7 +40,7 @@ interface DocumentRow { id: string; documentType: string; title: string; - status: 'draft' | 'sent' | 'partially_signed' | 'completed' | 'expired' | 'cancelled'; + status: DocumentStatus; createdAt: string; signers?: Array<{ status: string }>; } @@ -50,16 +55,9 @@ interface DocumentSigner { signedAt?: string | null; } -const STATUS_LABELS: Record = { - draft: 'Draft', - sent: 'Awaiting signatures', - partially_signed: 'Partially signed', - completed: 'Signed', - expired: 'Expired', - cancelled: 'Cancelled', -}; +const STATUS_LABELS = DOCUMENT_STATUS_LABELS; -const STATUS_TONES: Record = { +const STATUS_TONES: Record = { draft: 'bg-slate-100 text-slate-700', sent: 'bg-blue-100 text-blue-700', partially_signed: 'bg-amber-100 text-amber-800', @@ -68,7 +66,7 @@ const STATUS_TONES: Record = { cancelled: 'bg-slate-200 text-slate-600', }; -const ACTIVE_STATUSES = new Set(['draft', 'sent', 'partially_signed']); +const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE; /** * Dedicated Contract workspace tab. Mirrors the EOI tab pattern but diff --git a/src/components/interests/interest-detail-header.tsx b/src/components/interests/interest-detail-header.tsx index 939aee71..f3c0439e 100644 --- a/src/components/interests/interest-detail-header.tsx +++ b/src/components/interests/interest-detail-header.tsx @@ -54,8 +54,8 @@ function resolveOutcomeBadge(outcome: string | null | undefined) { const CATEGORY_LABELS: Record = { general_interest: 'General', - specific_qualified: 'Specific Qualified', - hot_lead: 'Hot Lead', + specific_qualified: 'Specific qualified', + hot_lead: 'Hot lead', }; interface InterestDetailHeaderProps { diff --git a/src/components/interests/interest-eoi-tab.tsx b/src/components/interests/interest-eoi-tab.tsx index 14064a00..317b6d4e 100644 --- a/src/components/interests/interest-eoi-tab.tsx +++ b/src/components/interests/interest-eoi-tab.tsx @@ -24,6 +24,11 @@ import { SigningProgress } from '@/components/documents/signing-progress'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { useConfirmation } from '@/hooks/use-confirmation'; +import { + DOCUMENT_STATUS_ACTIVE, + DOCUMENT_STATUS_LABELS, + type DocumentStatus, +} from '@/lib/labels/document-status'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/ui-store'; @@ -37,7 +42,7 @@ interface DocumentRow { id: string; documentType: string; title: string; - status: 'draft' | 'sent' | 'partially_signed' | 'completed' | 'expired' | 'cancelled'; + status: DocumentStatus; createdAt: string; signers?: Array<{ status: string }>; } @@ -52,16 +57,9 @@ interface DocumentSigner { signedAt?: string | null; } -const STATUS_LABELS: Record = { - draft: 'Draft', - sent: 'Awaiting signatures', - partially_signed: 'Partially signed', - completed: 'Signed', - expired: 'Expired', - cancelled: 'Cancelled', -}; +const STATUS_LABELS = DOCUMENT_STATUS_LABELS; -const STATUS_TONES: Record = { +const STATUS_TONES: Record = { draft: 'bg-slate-100 text-slate-700', sent: 'bg-blue-100 text-blue-700', partially_signed: 'bg-amber-100 text-amber-800', @@ -70,7 +68,7 @@ const STATUS_TONES: Record = { cancelled: 'bg-slate-200 text-slate-600', }; -const ACTIVE_STATUSES = new Set(['draft', 'sent', 'partially_signed']); +const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE; /** * Dedicated EOI workspace tab. The user's "where do I generate / track diff --git a/src/components/interests/interest-filters.tsx b/src/components/interests/interest-filters.tsx index 96df579c..0c74f7c8 100644 --- a/src/components/interests/interest-filters.tsx +++ b/src/components/interests/interest-filters.tsx @@ -2,9 +2,9 @@ import type { FilterDefinition } from '@/components/shared/filter-bar'; import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES } from '@/lib/constants'; const CATEGORY_LABELS: Record = { - general_interest: 'General Interest', - specific_qualified: 'Specific Qualified', - hot_lead: 'Hot Lead', + general_interest: 'General interest', + specific_qualified: 'Specific qualified', + hot_lead: 'Hot lead', }; export const interestFilterDefinitions: FilterDefinition[] = [ diff --git a/src/components/interests/interest-form.tsx b/src/components/interests/interest-form.tsx index 3dbb60ef..389a6c33 100644 --- a/src/components/interests/interest-form.tsx +++ b/src/components/interests/interest-form.tsx @@ -51,9 +51,9 @@ import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES, SOURCES } from '@/lib/c import { cn } from '@/lib/utils'; const CATEGORY_LABELS: Record = { - general_interest: 'General Interest', - specific_qualified: 'Specific Qualified', - hot_lead: 'Hot Lead', + general_interest: 'General interest', + specific_qualified: 'Specific qualified', + hot_lead: 'Hot lead', }; interface InterestFormProps { @@ -583,7 +583,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }: {(isSubmitting || mutation.isPending) && ( )} - {isEdit ? 'Save Changes' : 'Create Interest'} + {isEdit ? 'Save changes' : 'Create Interest'} diff --git a/src/components/interests/interest-reservation-tab.tsx b/src/components/interests/interest-reservation-tab.tsx index f31db964..22757ae6 100644 --- a/src/components/interests/interest-reservation-tab.tsx +++ b/src/components/interests/interest-reservation-tab.tsx @@ -23,6 +23,11 @@ import { SigningProgress } from '@/components/documents/signing-progress'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { useConfirmation } from '@/hooks/use-confirmation'; +import { + DOCUMENT_STATUS_ACTIVE, + DOCUMENT_STATUS_LABELS, + type DocumentStatus, +} from '@/lib/labels/document-status'; import { cn } from '@/lib/utils'; import { useUIStore } from '@/stores/ui-store'; @@ -35,7 +40,7 @@ interface DocumentRow { id: string; documentType: string; title: string; - status: 'draft' | 'sent' | 'partially_signed' | 'completed' | 'expired' | 'cancelled'; + status: DocumentStatus; createdAt: string; signers?: Array<{ status: string }>; } @@ -50,16 +55,9 @@ interface DocumentSigner { signedAt?: string | null; } -const STATUS_LABELS: Record = { - draft: 'Draft', - sent: 'Awaiting signatures', - partially_signed: 'Partially signed', - completed: 'Signed', - expired: 'Expired', - cancelled: 'Cancelled', -}; +const STATUS_LABELS = DOCUMENT_STATUS_LABELS; -const STATUS_TONES: Record = { +const STATUS_TONES: Record = { draft: 'bg-slate-100 text-slate-700', sent: 'bg-blue-100 text-blue-700', partially_signed: 'bg-amber-100 text-amber-800', @@ -68,7 +66,7 @@ const STATUS_TONES: Record = { cancelled: 'bg-slate-200 text-slate-600', }; -const ACTIVE_STATUSES = new Set(['draft', 'sent', 'partially_signed']); +const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE; /** * Dedicated Reservation workspace tab. Mirrors the EOI tab pattern but diff --git a/src/components/reminders/reminder-form.tsx b/src/components/reminders/reminder-form.tsx index 0c85c436..c7f173c0 100644 --- a/src/components/reminders/reminder-form.tsx +++ b/src/components/reminders/reminder-form.tsx @@ -263,7 +263,7 @@ function ReminderFormBody({ Cancel diff --git a/src/components/yachts/yacht-form.tsx b/src/components/yachts/yacht-form.tsx index aaf38ac5..55d299b9 100644 --- a/src/components/yachts/yacht-form.tsx +++ b/src/components/yachts/yacht-form.tsx @@ -361,7 +361,7 @@ export function YachtForm({ open, onOpenChange, yacht, initialOwner }: YachtForm {(isSubmitting || mutation.isPending) && ( )} - {isEdit ? 'Save Changes' : 'Create Yacht'} + {isEdit ? 'Save changes' : 'Create Yacht'} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2f3b1f0a..2c25b29d 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -184,9 +184,9 @@ const LABEL_OVERRIDES: Record = { under_offer: 'Under Offer', fixed_term: 'Fixed Term', reservation_agreement: 'Reservation Agreement', - hot_lead: 'Hot Lead', - general_interest: 'General Interest', - specific_qualified: 'Specific Qualified', + hot_lead: 'Hot lead', + general_interest: 'General interest', + specific_qualified: 'Specific qualified', }; function humanizeEnum(raw: string): string { diff --git a/src/lib/labels/document-status.ts b/src/lib/labels/document-status.ts new file mode 100644 index 00000000..0b5105da --- /dev/null +++ b/src/lib/labels/document-status.ts @@ -0,0 +1,87 @@ +/** + * Canonical labels + StatusPill tones for the signing-document lifecycle. + * + * Six surfaces previously carried divergent label sets (interest-eoi-tab, + * interest-contract-tab, interest-reservation-tab, documents-hub, + * signing-progress, notification-digest, realtime-toast). A signer would + * see "Partially signed", "partially_signed", and "EOI fully signed" for + * the same enum state across one session. This module is the single + * source of truth — import from here, do not redefine inline. + * + * If a new lifecycle state arrives in the schema, add it here once. + */ + +import type { StatusPillStatus } from '@/components/ui/status-pill'; + +export type DocumentStatus = + | 'draft' + | 'sent' + | 'partially_signed' + | 'completed' + | 'expired' + | 'cancelled'; + +/** + * Human label rendered in CRM UI (staff-facing). Use the portal-specific + * mapping in `documentStatusLabelForPortal` when rendering to clients — + * "Awaiting signatures" reads fine on the inside; clients want + * "Awaiting your signature". + */ +export const DOCUMENT_STATUS_LABELS: Record = { + draft: 'Draft', + sent: 'Awaiting signatures', + partially_signed: 'Partially signed', + completed: 'Signed', + expired: 'Expired', + cancelled: 'Cancelled', +}; + +/** + * Client-portal labels. The portal previously rendered + * `eoiStatus.replace(/_/g, ' ')` so a client saw "EOI: partially signed". + * Map to action-oriented copy that the client can act on. + */ +export const DOCUMENT_STATUS_LABELS_PORTAL: Record = { + draft: 'Pending', + sent: 'Awaiting your signature', + partially_signed: 'Signed (other parties remaining)', + completed: 'Signed', + expired: 'Expired', + cancelled: 'Cancelled', +}; + +/** + * StatusPill variant per state. Pairs with `` + * via the shared primitive so the colour palette stays consistent with + * non-document status pills (berth/user/etc). + */ +export const DOCUMENT_STATUS_PILL: Record = { + draft: 'draft', + sent: 'sent', + partially_signed: 'partial', + completed: 'completed', + expired: 'expired', + cancelled: 'cancelled', +}; + +/** + * The "in-flight" set — useful for hero treatment, banners, "we're + * waiting on action" UI. completed/expired/cancelled are terminal. + */ +export const DOCUMENT_STATUS_ACTIVE: ReadonlySet = new Set([ + 'draft', + 'sent', + 'partially_signed', +]); + +export function documentStatusLabel(status: string): string { + return DOCUMENT_STATUS_LABELS[status as DocumentStatus] ?? status; +} + +export function documentStatusLabelForPortal(status: string): string { + return DOCUMENT_STATUS_LABELS_PORTAL[status as DocumentStatus] ?? status; +} + +export function documentStatusPill(status: string): StatusPillStatus { + return DOCUMENT_STATUS_PILL[status as DocumentStatus] ?? 'pending'; +}