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:
@@ -86,11 +86,10 @@ export default async function PortalInterestsPage() {
|
||||
<span className="text-sm text-gray-400">- {interest.berthArea}</span>
|
||||
)}
|
||||
</div>
|
||||
{interest.leadCategory && (
|
||||
<p className="text-sm text-gray-500 capitalize">
|
||||
{interest.leadCategory.replace(/_/g, ' ')}
|
||||
</p>
|
||||
)}
|
||||
{/* 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". */}
|
||||
<div className="flex flex-wrap gap-2 mt-2 text-xs text-gray-400">
|
||||
{interest.dateFirstContact && (
|
||||
<span>
|
||||
|
||||
@@ -319,7 +319,7 @@ function CustomFieldFormBody({ open, onOpenChange, field, onSuccess }: CustomFie
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Field'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Field'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -197,7 +197,7 @@ export function TemplateForm({ open, onOpenChange, template, onSuccess }: Templa
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !!jsonError}>
|
||||
{loading ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Template'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Template'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -208,7 +208,7 @@ function PortFormBody({ open, onOpenChange, port, onSuccess }: PortFormProps) {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !name.trim() || !slug.trim()}>
|
||||
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Port'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Port'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -307,7 +307,7 @@ function RoleFormBody({ open, onOpenChange, role, onSuccess }: RoleFormProps) {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !name.trim()}>
|
||||
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Role'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Role'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -503,7 +503,7 @@ export function SettingsManager() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{saving === setting.key ? 'Saving...' : 'Save'}
|
||||
{saving === setting.key ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -144,7 +144,7 @@ export function TagForm({ open, onOpenChange, tag, onSuccess }: TagFormProps) {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !name.trim()}>
|
||||
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Tag'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Tag'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -327,7 +327,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !displayName.trim() || !roleId}>
|
||||
{loading ? 'Saving...' : isEdit ? 'Save changes' : 'Create user'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create user'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -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'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -222,16 +222,16 @@ function StatusChangeDialog({
|
||||
</div>
|
||||
{showInterestPicker && (
|
||||
<div className="space-y-2">
|
||||
<Label>Linked prospect (optional)</Label>
|
||||
<Label>Linked interest (optional)</Label>
|
||||
<InterestLinkPicker
|
||||
value={interestId ?? null}
|
||||
options={interestOptions}
|
||||
onChange={(id) => setValue('interestId', id ?? undefined)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -240,7 +240,7 @@ function StatusChangeDialog({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Saving...' : 'Update Status'}
|
||||
{isSubmitting ? 'Saving…' : 'Update Status'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -474,7 +474,7 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
||||
{isSubmitting ? 'Saving…' : 'Save changes'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -41,9 +41,9 @@ const CATEGORY_RANK: Record<string, number> = {
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
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 {
|
||||
|
||||
@@ -461,7 +461,7 @@ export function buildBerthTabs(berth: BerthData): DetailTab[] {
|
||||
},
|
||||
{
|
||||
id: 'deal-documents',
|
||||
label: 'Deal Documents',
|
||||
label: 'Interest Documents',
|
||||
content: <BerthDealDocumentsTab berthId={berth.id} />,
|
||||
},
|
||||
// Waiting List + Maintenance Log tabs were stubs ("coming soon")
|
||||
|
||||
@@ -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">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -421,7 +421,7 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }:
|
||||
{(isSubmitting || mutation.isPending) && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{isEdit ? 'Save Changes' : 'Create Client'}
|
||||
{isEdit ? 'Save changes' : 'Create Client'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -522,12 +522,12 @@ function SmartArchiveDialogBody({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* In-flight signing envelopes */}
|
||||
{/* In-flight signing requests */}
|
||||
{dossier.documents.filter((d) => d.isInFlight).length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
@@ -546,8 +546,8 @@ function SmartArchiveDialogBody({
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="leave">Leave envelope pending</option>
|
||||
<option value="void_documenso">Void the signing envelope</option>
|
||||
<option value="leave">Leave signing request pending</option>
|
||||
<option value="void_documenso">Cancel the signing request</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -461,7 +461,7 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) {
|
||||
{(isSubmitting || mutation.isPending) && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{isEdit ? 'Save Changes' : 'Create Company'}
|
||||
{isEdit ? 'Save changes' : 'Create Company'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -418,7 +418,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
|
||||
{(isSubmitting || mutation.isPending) && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{isEdit ? 'Save Changes' : 'Create Expense'}
|
||||
{isEdit ? 'Save changes' : 'Create Expense'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -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<DocumentRow['status'], string> = {
|
||||
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<DocumentRow['status'], string> = {
|
||||
const STATUS_TONES: Record<DocumentStatus, string> = {
|
||||
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<DocumentRow['status'], string> = {
|
||||
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
|
||||
|
||||
@@ -54,8 +54,8 @@ function resolveOutcomeBadge(outcome: string | null | undefined) {
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
general_interest: 'General',
|
||||
specific_qualified: 'Specific Qualified',
|
||||
hot_lead: 'Hot Lead',
|
||||
specific_qualified: 'Specific qualified',
|
||||
hot_lead: 'Hot lead',
|
||||
};
|
||||
|
||||
interface InterestDetailHeaderProps {
|
||||
|
||||
@@ -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<DocumentRow['status'], string> = {
|
||||
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<DocumentRow['status'], string> = {
|
||||
const STATUS_TONES: Record<DocumentStatus, string> = {
|
||||
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<DocumentRow['status'], string> = {
|
||||
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
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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[] = [
|
||||
|
||||
@@ -51,9 +51,9 @@ import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES, SOURCES } from '@/lib/c
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
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) && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{isEdit ? 'Save Changes' : 'Create Interest'}
|
||||
{isEdit ? 'Save changes' : 'Create Interest'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -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<DocumentRow['status'], string> = {
|
||||
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<DocumentRow['status'], string> = {
|
||||
const STATUS_TONES: Record<DocumentStatus, string> = {
|
||||
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<DocumentRow['status'], string> = {
|
||||
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
|
||||
|
||||
@@ -263,7 +263,7 @@ function ReminderFormBody({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !title.trim() || !dueAt}>
|
||||
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Reminder'}
|
||||
{loading ? 'Saving…' : isEdit ? 'Save changes' : 'Create Reminder'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -361,7 +361,7 @@ export function YachtForm({ open, onOpenChange, yacht, initialOwner }: YachtForm
|
||||
{(isSubmitting || mutation.isPending) && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{isEdit ? 'Save Changes' : 'Create Yacht'}
|
||||
{isEdit ? 'Save changes' : 'Create Yacht'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
|
||||
@@ -184,9 +184,9 @@ const LABEL_OVERRIDES: Record<string, string> = {
|
||||
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 {
|
||||
|
||||
87
src/lib/labels/document-status.ts
Normal file
87
src/lib/labels/document-status.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user