fix(audit-wave-9): copy/terminology sweep (copy-auditor)

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:12:40 +02:00
parent eab30c194a
commit 689a114aba
29 changed files with 159 additions and 79 deletions

View File

@@ -86,11 +86,10 @@ export default async function PortalInterestsPage() {
<span className="text-sm text-gray-400">- {interest.berthArea}</span> <span className="text-sm text-gray-400">- {interest.berthArea}</span>
)} )}
</div> </div>
{interest.leadCategory && ( {/* leadCategory ("hot_lead" / "qualified_lead" / etc.)
<p className="text-sm text-gray-500 capitalize"> is a staff classification — never render to clients.
{interest.leadCategory.replace(/_/g, ' ')} Privacy + optics: we shouldn't be telling the
</p> prospect they're a "hot lead". */}
)}
<div className="flex flex-wrap gap-2 mt-2 text-xs text-gray-400"> <div className="flex flex-wrap gap-2 mt-2 text-xs text-gray-400">
{interest.dateFirstContact && ( {interest.dateFirstContact && (
<span> <span>

View File

@@ -319,7 +319,7 @@ function CustomFieldFormBody({ open, onOpenChange, field, onSuccess }: CustomFie
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading}> <Button type="submit" disabled={loading}>
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Field'} {loading ? 'Saving' : isEdit ? 'Save changes' : 'Create Field'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -197,7 +197,7 @@ export function TemplateForm({ open, onOpenChange, template, onSuccess }: Templa
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading || !!jsonError}> <Button type="submit" disabled={loading || !!jsonError}>
{loading ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Template'} {loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Template'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -208,7 +208,7 @@ function PortFormBody({ open, onOpenChange, port, onSuccess }: PortFormProps) {
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading || !name.trim() || !slug.trim()}> <Button type="submit" disabled={loading || !name.trim() || !slug.trim()}>
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Port'} {loading ? 'Saving' : isEdit ? 'Save changes' : 'Create Port'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -307,7 +307,7 @@ function RoleFormBody({ open, onOpenChange, role, onSuccess }: RoleFormProps) {
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading || !name.trim()}> <Button type="submit" disabled={loading || !name.trim()}>
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Role'} {loading ? 'Saving' : isEdit ? 'Save changes' : 'Create Role'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -503,7 +503,7 @@ export function SettingsManager() {
} }
}} }}
> >
{saving === setting.key ? 'Saving...' : 'Save'} {saving === setting.key ? 'Saving' : 'Save'}
</Button> </Button>
</div> </div>
); );

View File

@@ -144,7 +144,7 @@ export function TagForm({ open, onOpenChange, tag, onSuccess }: TagFormProps) {
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading || !name.trim()}> <Button type="submit" disabled={loading || !name.trim()}>
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Tag'} {loading ? 'Saving' : isEdit ? 'Save changes' : 'Create Tag'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -327,7 +327,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) {
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading || !displayName.trim() || !roleId}> <Button type="submit" disabled={loading || !displayName.trim() || !roleId}>
{loading ? 'Saving...' : isEdit ? 'Save changes' : 'Create user'} {loading ? 'Saving' : isEdit ? 'Save changes' : 'Create user'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -142,7 +142,7 @@ export function WebhookForm({ open, onOpenChange, webhook, onSuccess }: WebhookF
type="submit" type="submit"
disabled={loading || !name.trim() || !url.trim() || events.length === 0} disabled={loading || !name.trim() || !url.trim() || events.length === 0}
> >
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Webhook'} {loading ? 'Saving' : isEdit ? 'Save changes' : 'Create Webhook'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -222,16 +222,16 @@ function StatusChangeDialog({
</div> </div>
{showInterestPicker && ( {showInterestPicker && (
<div className="space-y-2"> <div className="space-y-2">
<Label>Linked prospect (optional)</Label> <Label>Linked interest (optional)</Label>
<InterestLinkPicker <InterestLinkPicker
value={interestId ?? null} value={interestId ?? null}
options={interestOptions} options={interestOptions}
onChange={(id) => setValue('interestId', id ?? undefined)} onChange={(id) => setValue('interestId', id ?? undefined)}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Link this status change to the prospect (interest) it relates to. The change will Link this status change to the interest it relates to. The change will appear on
appear on that interest&apos;s timeline, and the berth gets attached to the prospect that interest&apos;s timeline, and the berth gets attached to it automatically if it
automatically if it wasn&apos;t already. wasn&apos;t already.
</p> </p>
</div> </div>
)} )}
@@ -240,7 +240,7 @@ function StatusChangeDialog({
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Update Status'} {isSubmitting ? 'Saving' : 'Update Status'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -474,7 +474,7 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save Changes'} {isSubmitting ? 'Saving' : 'Save changes'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -41,9 +41,9 @@ const CATEGORY_RANK: Record<string, number> = {
}; };
const CATEGORY_LABELS: Record<string, string> = { const CATEGORY_LABELS: Record<string, string> = {
hot_lead: 'Hot Lead', hot_lead: 'Hot lead',
specific_qualified: 'Specific Qualified', specific_qualified: 'Specific qualified',
general_interest: 'General Interest', general_interest: 'General interest',
}; };
interface ListResponse { interface ListResponse {

View File

@@ -461,7 +461,7 @@ export function buildBerthTabs(berth: BerthData): DetailTab[] {
}, },
{ {
id: 'deal-documents', id: 'deal-documents',
label: 'Deal Documents', label: 'Interest Documents',
content: <BerthDealDocumentsTab berthId={berth.id} />, content: <BerthDealDocumentsTab berthId={berth.id} />,
}, },
// Waiting List + Maintenance Log tabs were stubs ("coming soon") // Waiting List + Maintenance Log tabs were stubs ("coming soon")

View File

@@ -169,7 +169,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
<div className="rounded-md border bg-muted/30 p-3 text-xs text-muted-foreground"> <div className="rounded-md border bg-muted/30 p-3 text-xs text-muted-foreground">
Low-stakes defaults: release available/under-offer berths, keep sold ones, cancel 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. client. To customise per-client, archive that client individually instead.
</div> </div>
</div> </div>

View File

@@ -421,7 +421,7 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }:
{(isSubmitting || mutation.isPending) && ( {(isSubmitting || mutation.isPending) && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
)} )}
{isEdit ? 'Save Changes' : 'Create Client'} {isEdit ? 'Save changes' : 'Create Client'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -522,12 +522,12 @@ function SmartArchiveDialogBody({
</Card> </Card>
)} )}
{/* In-flight signing envelopes */} {/* In-flight signing requests */}
{dossier.documents.filter((d) => d.isInFlight).length > 0 && ( {dossier.documents.filter((d) => d.isInFlight).length > 0 && (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<FileText className="h-4 w-4" /> In-flight signing envelopes <FileText className="h-4 w-4" /> In-flight signing requests
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
@@ -546,8 +546,8 @@ function SmartArchiveDialogBody({
})) }))
} }
> >
<option value="leave">Leave envelope pending</option> <option value="leave">Leave signing request pending</option>
<option value="void_documenso">Void the signing envelope</option> <option value="void_documenso">Cancel the signing request</option>
</select> </select>
</div> </div>
))} ))}

View File

@@ -461,7 +461,7 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) {
{(isSubmitting || mutation.isPending) && ( {(isSubmitting || mutation.isPending) && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
)} )}
{isEdit ? 'Save Changes' : 'Create Company'} {isEdit ? 'Save changes' : 'Create Company'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -161,7 +161,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
const handleCancel = async () => { const handleCancel = async () => {
const ok = await confirm({ const ok = await confirm({
title: 'Cancel document', 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', confirmLabel: 'Cancel document',
}); });
if (!ok) return; if (!ok) return;

View File

@@ -418,7 +418,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
{(isSubmitting || mutation.isPending) && ( {(isSubmitting || mutation.isPending) && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
)} )}
{isEdit ? 'Save Changes' : 'Create Expense'} {isEdit ? 'Save changes' : 'Create Expense'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -23,6 +23,11 @@ import { SigningProgress } from '@/components/documents/signing-progress';
import { apiFetch } from '@/lib/api/client'; import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error'; import { toastError } from '@/lib/api/toast-error';
import { useConfirmation } from '@/hooks/use-confirmation'; 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 { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/ui-store'; import { useUIStore } from '@/stores/ui-store';
@@ -35,7 +40,7 @@ interface DocumentRow {
id: string; id: string;
documentType: string; documentType: string;
title: string; title: string;
status: 'draft' | 'sent' | 'partially_signed' | 'completed' | 'expired' | 'cancelled'; status: DocumentStatus;
createdAt: string; createdAt: string;
signers?: Array<{ status: string }>; signers?: Array<{ status: string }>;
} }
@@ -50,16 +55,9 @@ interface DocumentSigner {
signedAt?: string | null; signedAt?: string | null;
} }
const STATUS_LABELS: Record<DocumentRow['status'], string> = { const STATUS_LABELS = DOCUMENT_STATUS_LABELS;
draft: 'Draft',
sent: 'Awaiting signatures',
partially_signed: 'Partially signed',
completed: 'Signed',
expired: 'Expired',
cancelled: 'Cancelled',
};
const STATUS_TONES: Record<DocumentRow['status'], string> = { const STATUS_TONES: Record<DocumentStatus, string> = {
draft: 'bg-slate-100 text-slate-700', draft: 'bg-slate-100 text-slate-700',
sent: 'bg-blue-100 text-blue-700', sent: 'bg-blue-100 text-blue-700',
partially_signed: 'bg-amber-100 text-amber-800', partially_signed: 'bg-amber-100 text-amber-800',
@@ -68,7 +66,7 @@ const STATUS_TONES: Record<DocumentRow['status'], string> = {
cancelled: 'bg-slate-200 text-slate-600', cancelled: 'bg-slate-200 text-slate-600',
}; };
const ACTIVE_STATUSES = new Set<DocumentRow['status']>(['draft', 'sent', 'partially_signed']); const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE;
/** /**
* Dedicated Contract workspace tab. Mirrors the EOI tab pattern but * Dedicated Contract workspace tab. Mirrors the EOI tab pattern but

View File

@@ -54,8 +54,8 @@ function resolveOutcomeBadge(outcome: string | null | undefined) {
const CATEGORY_LABELS: Record<string, string> = { const CATEGORY_LABELS: Record<string, string> = {
general_interest: 'General', general_interest: 'General',
specific_qualified: 'Specific Qualified', specific_qualified: 'Specific qualified',
hot_lead: 'Hot Lead', hot_lead: 'Hot lead',
}; };
interface InterestDetailHeaderProps { interface InterestDetailHeaderProps {

View File

@@ -24,6 +24,11 @@ import { SigningProgress } from '@/components/documents/signing-progress';
import { apiFetch } from '@/lib/api/client'; import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error'; import { toastError } from '@/lib/api/toast-error';
import { useConfirmation } from '@/hooks/use-confirmation'; 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 { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/ui-store'; import { useUIStore } from '@/stores/ui-store';
@@ -37,7 +42,7 @@ interface DocumentRow {
id: string; id: string;
documentType: string; documentType: string;
title: string; title: string;
status: 'draft' | 'sent' | 'partially_signed' | 'completed' | 'expired' | 'cancelled'; status: DocumentStatus;
createdAt: string; createdAt: string;
signers?: Array<{ status: string }>; signers?: Array<{ status: string }>;
} }
@@ -52,16 +57,9 @@ interface DocumentSigner {
signedAt?: string | null; signedAt?: string | null;
} }
const STATUS_LABELS: Record<DocumentRow['status'], string> = { const STATUS_LABELS = DOCUMENT_STATUS_LABELS;
draft: 'Draft',
sent: 'Awaiting signatures',
partially_signed: 'Partially signed',
completed: 'Signed',
expired: 'Expired',
cancelled: 'Cancelled',
};
const STATUS_TONES: Record<DocumentRow['status'], string> = { const STATUS_TONES: Record<DocumentStatus, string> = {
draft: 'bg-slate-100 text-slate-700', draft: 'bg-slate-100 text-slate-700',
sent: 'bg-blue-100 text-blue-700', sent: 'bg-blue-100 text-blue-700',
partially_signed: 'bg-amber-100 text-amber-800', partially_signed: 'bg-amber-100 text-amber-800',
@@ -70,7 +68,7 @@ const STATUS_TONES: Record<DocumentRow['status'], string> = {
cancelled: 'bg-slate-200 text-slate-600', cancelled: 'bg-slate-200 text-slate-600',
}; };
const ACTIVE_STATUSES = new Set<DocumentRow['status']>(['draft', 'sent', 'partially_signed']); const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE;
/** /**
* Dedicated EOI workspace tab. The user's "where do I generate / track * Dedicated EOI workspace tab. The user's "where do I generate / track

View File

@@ -2,9 +2,9 @@ import type { FilterDefinition } from '@/components/shared/filter-bar';
import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES } from '@/lib/constants'; import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES } from '@/lib/constants';
const CATEGORY_LABELS: Record<string, string> = { const CATEGORY_LABELS: Record<string, string> = {
general_interest: 'General Interest', general_interest: 'General interest',
specific_qualified: 'Specific Qualified', specific_qualified: 'Specific qualified',
hot_lead: 'Hot Lead', hot_lead: 'Hot lead',
}; };
export const interestFilterDefinitions: FilterDefinition[] = [ export const interestFilterDefinitions: FilterDefinition[] = [

View File

@@ -51,9 +51,9 @@ import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES, SOURCES } from '@/lib/c
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const CATEGORY_LABELS: Record<string, string> = { const CATEGORY_LABELS: Record<string, string> = {
general_interest: 'General Interest', general_interest: 'General interest',
specific_qualified: 'Specific Qualified', specific_qualified: 'Specific qualified',
hot_lead: 'Hot Lead', hot_lead: 'Hot lead',
}; };
interface InterestFormProps { interface InterestFormProps {
@@ -583,7 +583,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
{(isSubmitting || mutation.isPending) && ( {(isSubmitting || mutation.isPending) && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
)} )}
{isEdit ? 'Save Changes' : 'Create Interest'} {isEdit ? 'Save changes' : 'Create Interest'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -23,6 +23,11 @@ import { SigningProgress } from '@/components/documents/signing-progress';
import { apiFetch } from '@/lib/api/client'; import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error'; import { toastError } from '@/lib/api/toast-error';
import { useConfirmation } from '@/hooks/use-confirmation'; 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 { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/ui-store'; import { useUIStore } from '@/stores/ui-store';
@@ -35,7 +40,7 @@ interface DocumentRow {
id: string; id: string;
documentType: string; documentType: string;
title: string; title: string;
status: 'draft' | 'sent' | 'partially_signed' | 'completed' | 'expired' | 'cancelled'; status: DocumentStatus;
createdAt: string; createdAt: string;
signers?: Array<{ status: string }>; signers?: Array<{ status: string }>;
} }
@@ -50,16 +55,9 @@ interface DocumentSigner {
signedAt?: string | null; signedAt?: string | null;
} }
const STATUS_LABELS: Record<DocumentRow['status'], string> = { const STATUS_LABELS = DOCUMENT_STATUS_LABELS;
draft: 'Draft',
sent: 'Awaiting signatures',
partially_signed: 'Partially signed',
completed: 'Signed',
expired: 'Expired',
cancelled: 'Cancelled',
};
const STATUS_TONES: Record<DocumentRow['status'], string> = { const STATUS_TONES: Record<DocumentStatus, string> = {
draft: 'bg-slate-100 text-slate-700', draft: 'bg-slate-100 text-slate-700',
sent: 'bg-blue-100 text-blue-700', sent: 'bg-blue-100 text-blue-700',
partially_signed: 'bg-amber-100 text-amber-800', partially_signed: 'bg-amber-100 text-amber-800',
@@ -68,7 +66,7 @@ const STATUS_TONES: Record<DocumentRow['status'], string> = {
cancelled: 'bg-slate-200 text-slate-600', cancelled: 'bg-slate-200 text-slate-600',
}; };
const ACTIVE_STATUSES = new Set<DocumentRow['status']>(['draft', 'sent', 'partially_signed']); const ACTIVE_STATUSES = DOCUMENT_STATUS_ACTIVE;
/** /**
* Dedicated Reservation workspace tab. Mirrors the EOI tab pattern but * Dedicated Reservation workspace tab. Mirrors the EOI tab pattern but

View File

@@ -263,7 +263,7 @@ function ReminderFormBody({
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading || !title.trim() || !dueAt}> <Button type="submit" disabled={loading || !title.trim() || !dueAt}>
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Reminder'} {loading ? 'Saving' : isEdit ? 'Save changes' : 'Create Reminder'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -361,7 +361,7 @@ export function YachtForm({ open, onOpenChange, yacht, initialOwner }: YachtForm
{(isSubmitting || mutation.isPending) && ( {(isSubmitting || mutation.isPending) && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
)} )}
{isEdit ? 'Save Changes' : 'Create Yacht'} {isEdit ? 'Save changes' : 'Create Yacht'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@@ -184,9 +184,9 @@ const LABEL_OVERRIDES: Record<string, string> = {
under_offer: 'Under Offer', under_offer: 'Under Offer',
fixed_term: 'Fixed Term', fixed_term: 'Fixed Term',
reservation_agreement: 'Reservation Agreement', reservation_agreement: 'Reservation Agreement',
hot_lead: 'Hot Lead', hot_lead: 'Hot lead',
general_interest: 'General Interest', general_interest: 'General interest',
specific_qualified: 'Specific Qualified', specific_qualified: 'Specific qualified',
}; };
function humanizeEnum(raw: string): string { function humanizeEnum(raw: string): string {

View File

@@ -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<DocumentStatus, string> = {
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<DocumentStatus, string> = {
draft: 'Pending',
sent: 'Awaiting your signature',
partially_signed: 'Signed (other parties remaining)',
completed: 'Signed',
expired: 'Expired',
cancelled: 'Cancelled',
};
/**
* StatusPill variant per state. Pairs with `<StatusPill status={...}>`
* via the shared primitive so the colour palette stays consistent with
* non-document status pills (berth/user/etc).
*/
export const DOCUMENT_STATUS_PILL: Record<DocumentStatus, StatusPillStatus> = {
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<DocumentStatus> = new Set<DocumentStatus>([
'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';
}