feat(uat-b1): ship Wave A-E of Bucket 1 audit findings
Wave A (Interest+EOI form quick wins): - Auto-select yacht after inline-create from interest form - EOI generate dialog: "View EOI" action toast - Interest form berth picker: formatBerthRange compact label - Remove "Generate EOI" button from Documents tab (clean removal) - Interest auto-assign: only sales_agent/sales_manager auto-claim ownership on create (explicit role check via user_port_roles join) - LinkedBerthRowItem dims: drop "D" suffix + "L × W" format - ExternalEoiUploadDialog: prefillSignatories prop threaded from active EOI signers - EOI signature progress on Overview milestone card footer Wave B (a11y + i18n sweeps): - aria-live on supplemental-info error state - text-[10px] -> text-xs in client-pipeline-summary - Currency formatter: locale default removed (Intl uses runtime) - en-US/en-GB hardcoded toLocaleString swept across 13 components Wave C (Primary berth always in EOI bundle): - Service guard strengthened on update path - Migration 0083 backfills historical primary rows Wave D (Onboarding super_admin discoverability): - /api/v1/admin/onboarding/status endpoint + shared service - Topbar OnboardingBanner (super_admin, session-dismissible) - OnboardingTile dashboard widget (rail group, self-hides at 100%) - Celebration toast + invalidate of shared status on last tick Wave E (Branded post-completion email idempotency): - Verified handleDocumentCompleted already owns the email fan-out - Added regression test for the polling path + idempotency Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ import { YachtForm } from '@/components/yachts/yacht-form';
|
||||
import { YachtPicker } from '@/components/yachts/yacht-picker';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { useEntityOptions } from '@/hooks/use-entity-options';
|
||||
import { formatBerthRange } from '@/lib/templates/berth-range';
|
||||
import type { z } from 'zod';
|
||||
import { createInterestSchema, type CreateInterestInput } from '@/lib/validators/interests';
|
||||
import { PIPELINE_STAGES, STAGE_LABELS, LEAD_CATEGORIES, SOURCES } from '@/lib/constants';
|
||||
@@ -438,11 +439,24 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedBerthId
|
||||
? `${selectedBerth?.label ?? interest?.berthMooringNumber ?? selectedBerthId}${
|
||||
additionalBerthIds.length > 0
|
||||
? ` + ${additionalBerthIds.length} more`
|
||||
: ''
|
||||
}`
|
||||
? (() => {
|
||||
const primaryLabel =
|
||||
selectedBerth?.label ??
|
||||
interest?.berthMooringNumber ??
|
||||
selectedBerthId;
|
||||
const additionalLabels = additionalBerthIds
|
||||
.map((id) => berthOptions.find((b) => b.value === id)?.label)
|
||||
.filter((label): label is string => Boolean(label));
|
||||
const allLabels = [primaryLabel, ...additionalLabels];
|
||||
const range = formatBerthRange(allLabels);
|
||||
// Cap at 5 segments after range collapse so "A1-A3, B5, C2, D7, E4 +N more"
|
||||
// doesn't overflow the trigger.
|
||||
const segments = range ? range.split(', ') : [];
|
||||
if (segments.length <= 5) return range || primaryLabel;
|
||||
const head = segments.slice(0, 5).join(', ');
|
||||
const overflow = segments.length - 5;
|
||||
return `${head} +${overflow} more`;
|
||||
})()
|
||||
: 'Select berths…'}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
|
||||
@@ -791,6 +805,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
|
||||
open={createYachtOpen}
|
||||
onOpenChange={setCreateYachtOpen}
|
||||
initialOwner={{ type: 'client', id: selectedClientId }}
|
||||
onCreated={(y) => setValue('yachtId', y.id, { shouldDirty: true })}
|
||||
/>
|
||||
)}
|
||||
</Sheet>
|
||||
|
||||
Reference in New Issue
Block a user