feat(uat-batch-7): Wave-2 polish — Open-in-Documents, berth label, residential, NotesList parity

- InterestEoiTab history link renamed "Open" → "Open in Documents"
  so the cross-section nav target is unambiguous.
- DocumentDetail Interest link sub-text now shows the derived
  `berthLabel` (formatBerthRange of the in-EOI-bundle subset, falling
  back to primary, then all linked berths). The link no longer
  duplicates the Client name; falls back to clientName or "No berths
  linked" when no berths exist.
- New /<port>/residential/page.tsx redirects to /residential/clients
  so the breadcrumb's Residential link works.
- Residential interests list — whole row is now a Link target (was
  hidden behind a trailing "View" link); hover + border accent on the
  full row.
- Expenses PageHeader description "Track and manage port expenses" →
  "Track and manage business expenses" (drop the redundant "port",
  same audit pattern flagged in the queue).
- DropdownMenu base content capped at `max-h-96` (was the Radix
  available-height variable, which stretched menus edge-to-edge); the
  existing internal scroll handles overflow.
- Yacht Overview Notes block: replaced the legacy single-field
  textarea with the threaded `<NotesList entityType="yachts">` for
  parity with clients/interests/companies. Legacy `yacht.notes`
  column stays in schema for EOI/contract merge-field path.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 17:41:02 +02:00
parent a673b6cec2
commit c6dcf49e18
8 changed files with 89 additions and 23 deletions

View File

@@ -93,7 +93,7 @@ interface DetailWatcher {
}
interface DetailLinked {
interest: { id: string; clientName: string | null } | null;
interest: { id: string; clientName: string | null; berthLabel: string | null } | null;
client: { id: string; fullName: string } | null;
yacht: { id: string; name: string } | null;
company: { id: string; name: string } | null;
@@ -238,7 +238,11 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
linkedRows.push({
href: `/${portSlug}/interests/${linked.interest.id}`,
label: 'Interest',
sub: linked.interest.clientName,
// Show the berth label (e.g. "A1-A3, B5-B7" or "A12") so the
// Interest link carries distinct information from the Client
// link rendered just below — otherwise both rows show the same
// client name and the Interest row reads as duplicate.
sub: linked.interest.berthLabel ?? linked.interest.clientName ?? 'No berths linked',
});
}
if (linked.client) {

View File

@@ -177,7 +177,7 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
href={`/${portSlug}/documents/${d.id}` as any}
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Open
Open in Documents
<ExternalLink className="size-3" aria-hidden />
</Link>
)}

View File

@@ -273,17 +273,16 @@ function InterestsTab({
) : (
<ul className="space-y-2">
{client.interests.map((i) => (
<li key={i.id} className="flex items-center gap-3 p-3 rounded-md border bg-muted/30">
<span className="text-xs font-medium uppercase text-muted-foreground w-32 shrink-0">
{stageLabels[i.pipelineStage] ?? i.pipelineStage}
</span>
<span className="flex-1 truncate text-sm">{i.preferences || i.notes || '-'}</span>
<li key={i.id}>
<Link
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
href={`/${portSlug}/residential/interests/${i.id}` as any}
className="text-xs text-primary hover:underline"
className="flex items-center gap-3 p-3 rounded-md border bg-muted/30 hover:bg-muted/50 hover:border-primary/40 transition-colors"
>
View
<span className="text-xs font-medium uppercase text-muted-foreground w-32 shrink-0">
{stageLabels[i.pipelineStage] ?? i.pipelineStage}
</span>
<span className="flex-1 truncate text-sm">{i.preferences || i.notes || '-'}</span>
</Link>
</li>
))}

View File

@@ -63,7 +63,10 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
// Cap at 24rem (384px) so long menus don't visually stretch
// edge-to-edge of the viewport — internal scroll handles
// overflow. Consumers can override via the `className` prop.
'z-50 max-h-96 min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)',
className,
)}

View File

@@ -88,7 +88,15 @@ function EditableRow({ label, children }: { label: string; children: React.React
);
}
function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYacht }) {
function OverviewTab({
yachtId,
yacht,
currentUserId,
}: {
yachtId: string;
yacht: YachtTabsYacht;
currentUserId?: string;
}) {
const mutation = useYachtPatch(yachtId);
const save =
(field: YachtPatchField, transform?: (v: string | null) => string | number | null) =>
@@ -224,14 +232,17 @@ function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYach
</dl>
</div>
{/* Notes */}
{/* Notes — threaded list (parity with clients/interests/companies).
The legacy single-field `yacht.notes` column stays in schema
for the EOI/contract merge-field path; OverviewTab no longer
exposes it for editing here. */}
<div className="space-y-1 md:col-span-2">
<h3 className="text-sm font-medium mb-2">Notes</h3>
<InlineEditableField
variant="textarea"
value={yacht.notes}
onSave={save('notes')}
emptyText="No notes - click to add"
<NotesList
entityType="yachts"
entityId={yachtId}
currentUserId={currentUserId}
parentInvalidateKey={['yachts', yachtId]}
/>
</div>
@@ -327,7 +338,7 @@ export function getYachtTabs({ yachtId, currentUserId, yacht }: YachtTabsOptions
{
id: 'overview',
label: 'Overview',
content: <OverviewTab yachtId={yachtId} yacht={yacht} />,
content: <OverviewTab yachtId={yachtId} yacht={yacht} currentUserId={currentUserId} />,
},
{
id: 'ownership-history',